diff --git a/pkg/iac-providers/docker/v1/load-dir.go b/pkg/iac-providers/docker/v1/load-dir.go index f653b6dd3..0fcb4d022 100644 --- a/pkg/iac-providers/docker/v1/load-dir.go +++ b/pkg/iac-providers/docker/v1/load-dir.go @@ -34,6 +34,7 @@ func (dc *DockerV1) LoadIacDir(absRootDir string, nonRecursive bool) (output.All allResourcesConfig := make(map[string][]output.ResourceConfig) + // find all the files in the folder with name `Dockerfile` fileMap, err := utils.FindFilesBySuffix(absRootDir, []string{DockerFileName}) if err != nil { zap.S().Errorf("error while searching for iac files", zap.String("root dir", absRootDir), zap.Error(err)) diff --git a/pkg/iac-providers/docker/v1/load-dir_test.go b/pkg/iac-providers/docker/v1/load-dir_test.go index eca086e9a..ba7d70d06 100644 --- a/pkg/iac-providers/docker/v1/load-dir_test.go +++ b/pkg/iac-providers/docker/v1/load-dir_test.go @@ -21,6 +21,7 @@ import ( "fmt" "os" "path/filepath" + "reflect" "syscall" "testing" @@ -68,15 +69,41 @@ func TestLoadIacDir(t *testing.T) { dockerV1: DockerV1{}, wantErr: multierror.Append(errors.New(errString)), }, + { + name: "valid dirPath having dockerfile with in-file instrumentation", + dirPath: filepath.Join(testDataDir, "valid-directory-with-in-file-instrumentation"), + dockerV1: DockerV1{}, + want: output.AllResourceConfigs{ + "cmd": []output.ResourceConfig{ + {ID: "cmd.55ceacedc5f1c0df6951723a7401a74e", + Name: "Dockerfile", + ModuleName: "", + Source: "Dockerfile", + PlanRoot: "", Line: 5, + Type: "cmd", + Config: "server", + SkipRules: []output.SkipRule{{Rule: "AWS.S3Bucket.DS.High.1041", + Comment: "This rule does not belong to dockerfile will add correct once dockerfile policy added."}}, + MaxSeverity: "None", + MinSeverity: "High"}}, + "docker": []output.ResourceConfig{{ID: "docker.96052d48e5364a05995aaec1e5d53f2d", Name: "Dockerfile", ModuleName: "", Source: "Dockerfile", PlanRoot: "", Line: 1, Type: "dockerfile", Config: []string{"from", "cmd"}, SkipRules: []output.SkipRule{{Rule: "AWS.S3Bucket.DS.High.1041", Comment: "This rule does not belong to dockerfile will add correct once dockerfile policy added."}}, MaxSeverity: "None", MinSeverity: "High"}}, + "from": []output.ResourceConfig{{ID: "from.68be487d8ad02b4e09b46d29c8dbef3b", Name: "Dockerfile", ModuleName: "", Source: "Dockerfile", PlanRoot: "", Line: 1, Type: "from", Config: "runatlantis/atlantis:v0.16.1", SkipRules: []output.SkipRule{{Rule: "AWS.S3Bucket.DS.High.1041", Comment: "This rule does not belong to dockerfile will add correct once dockerfile policy added."}}, MaxSeverity: "None", MinSeverity: "High"}}}, + wantErr: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.dockerV1.LoadIacDir(tt.dirPath, false) + got, gotErr := tt.dockerV1.LoadIacDir(tt.dirPath, false) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) } + if tt.want != nil { + if got == nil || !reflect.DeepEqual(got, tt.want) { + t.Errorf("unexpected result; got: '%#v', want: '%v'", got, tt.want) + } + } if tt.wantErr == nil { if err := me.ErrorOrNil(); err != nil { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) diff --git a/pkg/iac-providers/docker/v1/load-file.go b/pkg/iac-providers/docker/v1/load-file.go index 0cd89efe7..6e6c410e1 100644 --- a/pkg/iac-providers/docker/v1/load-file.go +++ b/pkg/iac-providers/docker/v1/load-file.go @@ -33,25 +33,33 @@ import ( const ( dockerDirectory string = "docker" resourceTypeDockerfile string = "dockerfile" + // IDConnectorString is string connector used in id creation IDConnectorString string = "." ) -// LoadIacFile loads the docker file specified +// LoadIacFile loads the docker file specified and create ResourceConfig for each dockerfile func (dc *DockerV1) LoadIacFile(absFilePath string) (allResourcesConfig output.AllResourceConfigs, err error) { allResourcesConfig = make(map[string][]output.ResourceConfig) + data, comments, err := dc.Parse(absFilePath) if err != nil { - errMsg := fmt.Sprintf("error while parsing file %s, error: %v", absFilePath, err) - zap.S().Errorf("error while parsing file %s", absFilePath, err) + errMsg := fmt.Sprintf("error while parsing dockerfile %s, error: %v", absFilePath, err) + zap.S().Errorf("error while parsing dockerfile %s", absFilePath, err) return allResourcesConfig, errors.New(errMsg) } + minSeverity, maxSeverity := utils.GetMinMaxSeverity(comments) + skipRules := utils.GetSkipRules(comments) + // create an array of all the instructions present in the docker file dockerCommand := []string{} + + // create config for each instruction of dockerfile for i := 0; i < len(data); i++ { dockerCommand = append(dockerCommand, data[i].Cmd) + config := output.ResourceConfig{ Name: filepath.Base(absFilePath), Type: data[i].Cmd, @@ -67,7 +75,9 @@ func (dc *DockerV1) LoadIacFile(absFilePath string) (allResourcesConfig output.A } - // creates config for entire dockerfile + // Creates config for entire dockerfile which has array of instructions against the Config field. + // Created to use against policies which checks for availablility of command/instruction in dockerfile + // if command is not present line no also doesnot have any importance thats why set to 1. config := output.ResourceConfig{ Name: filepath.Base(absFilePath), Type: resourceTypeDockerfile, @@ -101,6 +111,7 @@ func (dc *DockerV1) getSourceRelativePath(sourceFile string) string { } // GetresourceIdforDockerfile Generates hash of the string to be used as the reference id for docker file +// added line no in creating hash because dockerfile may have same command multiple times with same value func GetresourceIdforDockerfile(filepath string, value string, lineNumber int) (referenceID string) { hasher := md5.New() hasher.Write([]byte(filepath + value + strconv.Itoa(lineNumber))) diff --git a/pkg/iac-providers/docker/v1/load-file_test.go b/pkg/iac-providers/docker/v1/load-file_test.go index a9ef2565d..ae2ec475a 100644 --- a/pkg/iac-providers/docker/v1/load-file_test.go +++ b/pkg/iac-providers/docker/v1/load-file_test.go @@ -41,7 +41,7 @@ func TestLoadIacFile(t *testing.T) { name: "empty config file", absFilePath: filepath.Join(fileTestDataDir, "Dockerfile"), dockerV1: DockerV1{}, - wantErr: fmt.Errorf("error while parsing file %s, error: file with no instructions", filepath.Join(fileTestDataDir, "Dockerfile")), + wantErr: fmt.Errorf("error while parsing dockerfile %s, error: file with no instructions", filepath.Join(fileTestDataDir, "Dockerfile")), }, { name: "valid docker file", diff --git a/pkg/iac-providers/docker/v1/parser.go b/pkg/iac-providers/docker/v1/parser.go index 677b23bde..69d59e6ef 100644 --- a/pkg/iac-providers/docker/v1/parser.go +++ b/pkg/iac-providers/docker/v1/parser.go @@ -26,8 +26,8 @@ import ( "go.uber.org/zap" ) -// ResourceConfig holds information about individual docker instructions -type ResourceConfig struct { +// DockerConfig holds information about individual docker instructions +type DockerConfig struct { Cmd string `json:"cmd"` Value string `json:"value"` Line int `json:"line"` @@ -45,29 +45,33 @@ func (dc *DockerV1) ValidateInstruction(node *parser.Node) error { return err } -// Parse parses the given dockerfile and gives docker config. -func (dc *DockerV1) Parse(filepath string) ([]ResourceConfig, string, error) { - config := []ResourceConfig{} +// Parse parses the given dockerfile and gives docker config and string of comments present in dockerfile. +func (dc *DockerV1) Parse(filepath string) ([]DockerConfig, string, error) { + config := []DockerConfig{} comments := "" + data, err := ioutil.ReadFile(filepath) if err != nil { zap.S().Error("error loading docker file", filepath, zap.Error(err)) - return []ResourceConfig{}, "", err + return config, "", err } r := bytes.NewReader(data) res, err := parser.Parse(r) if err != nil { zap.S().Errorf("error while parsing iac file", filepath, zap.Error(err)) - return []ResourceConfig{}, "", err + return config, "", err } for _, child := range res.AST.Children { values := []string{} err = dc.ValidateInstruction(child) if err != nil { - return []ResourceConfig{}, "", err + return config, "", err } + // loop over all the comments before the instruction is found to create one single string of comments + // appending # prefix and new line since it is removed by the parser while creating the AST + // Purpose of adding them back is to use the comman function to find skiprules and min max severity. for _, comment := range child.PrevComment { comments = comments + commentPrefix + comment + newLine } @@ -75,8 +79,10 @@ func (dc *DockerV1) Parse(filepath string) ([]ResourceConfig, string, error) { for i := child.Next; i != nil; i = i.Next { values = append(values, i.Value) } + value := strings.Join(values, stringJoinCharacter) - tempConfig := ResourceConfig{ + + tempConfig := DockerConfig{ Cmd: child.Value, Value: value, Line: child.StartLine, diff --git a/pkg/iac-providers/docker/v1/parser_test.go b/pkg/iac-providers/docker/v1/parser_test.go index 7f1be902d..8784034cf 100644 --- a/pkg/iac-providers/docker/v1/parser_test.go +++ b/pkg/iac-providers/docker/v1/parser_test.go @@ -29,7 +29,7 @@ func TestParse(t *testing.T) { name string filePath string dockerv1 DockerV1 - want []ResourceConfig + want []DockerConfig wantErr error }{ { @@ -37,13 +37,13 @@ func TestParse(t *testing.T) { filePath: filepath.Join(fileTestDataDir, "dockerfile-testparse-function"), dockerv1: DockerV1{}, wantErr: nil, - want: []ResourceConfig{{Cmd: "from", Value: "runatlantis/atlantis:v0.16.1", Line: 1}, {Cmd: "maintainer", Value: "accurics", Line: 2}, {Cmd: "label", Value: "key \"value\"", Line: 3}, {Cmd: "workdir", Value: "test", Line: 4}, {Cmd: "env", Value: "DEFAULT_TERRASCAN_VERSION 1.5.1", Line: 5}, {Cmd: "env", Value: "PLANFILE tfplan", Line: 6}, {Cmd: "add", Value: "setup.sh terrascan.sh launch-atlantis.sh entrypoint.sh /usr/local/bin/", Line: 7}, {Cmd: "run", Value: "mkdir -p /etc/atlantis/ && chmod +x /usr/local/bin/*.sh && /usr/local/bin/setup.sh", Line: 8}, {Cmd: "copy", Value: "terrascan-workflow.yaml /etc/atlantis/workflow.yaml", Line: 11}, {Cmd: "user", Value: "atlantis", Line: 13}, {Cmd: "arg", Value: "name=defaultValue", Line: 14}, {Cmd: "run", Value: "terrascan init", Line: 15}, {Cmd: "volume", Value: "/temp", Line: 16}, {Cmd: "healthcheck", Value: "CMD executable", Line: 17}, {Cmd: "entrypoint", Value: "/bin/bash entrypoint.sh", Line: 18}, {Cmd: "shell", Value: "cd", Line: 19}, {Cmd: "onbuild", Value: "", Line: 20}, {Cmd: "expose", Value: "9090", Line: 21}, {Cmd: "stopsignal", Value: "1", Line: 22}, {Cmd: "cmd", Value: "server", Line: 23}}, + want: []DockerConfig{{Cmd: "from", Value: "runatlantis/atlantis:v0.16.1", Line: 1}, {Cmd: "maintainer", Value: "accurics", Line: 2}, {Cmd: "label", Value: "key \"value\"", Line: 3}, {Cmd: "workdir", Value: "test", Line: 4}, {Cmd: "env", Value: "DEFAULT_TERRASCAN_VERSION 1.5.1", Line: 5}, {Cmd: "env", Value: "PLANFILE tfplan", Line: 6}, {Cmd: "add", Value: "setup.sh terrascan.sh launch-atlantis.sh entrypoint.sh /usr/local/bin/", Line: 7}, {Cmd: "run", Value: "mkdir -p /etc/atlantis/ && chmod +x /usr/local/bin/*.sh && /usr/local/bin/setup.sh", Line: 8}, {Cmd: "copy", Value: "terrascan-workflow.yaml /etc/atlantis/workflow.yaml", Line: 11}, {Cmd: "user", Value: "atlantis", Line: 13}, {Cmd: "arg", Value: "name=defaultValue", Line: 14}, {Cmd: "run", Value: "terrascan init", Line: 15}, {Cmd: "volume", Value: "/temp", Line: 16}, {Cmd: "healthcheck", Value: "CMD executable", Line: 17}, {Cmd: "entrypoint", Value: "/bin/bash entrypoint.sh", Line: 18}, {Cmd: "shell", Value: "cd", Line: 19}, {Cmd: "onbuild", Value: "", Line: 20}, {Cmd: "expose", Value: "9090", Line: 21}, {Cmd: "stopsignal", Value: "1", Line: 22}, {Cmd: "cmd", Value: "server", Line: 23}}, }, { name: "invalid docker file path", filePath: filepath.Join(fileTestDataDir, "dockerfile-testparse-function1"), dockerv1: DockerV1{}, - want: []ResourceConfig{}, + want: []DockerConfig{}, wantErr: fmt.Errorf("open %s: no such file or directory", filepath.Join(fileTestDataDir, "dockerfile-testparse-function1")), }, } diff --git a/pkg/iac-providers/docker/v1/testdata/valid-directory-with-in-file-instrumentation/Dockerfile b/pkg/iac-providers/docker/v1/testdata/valid-directory-with-in-file-instrumentation/Dockerfile new file mode 100644 index 000000000..66f2b02e3 --- /dev/null +++ b/pkg/iac-providers/docker/v1/testdata/valid-directory-with-in-file-instrumentation/Dockerfile @@ -0,0 +1,5 @@ +FROM runatlantis/atlantis:v0.16.1 +#ts:minseverity=High +#ts:maxseverity=None +#ts:skip=AWS.S3Bucket.DS.High.1041 This rule does not belong to dockerfile will add correct once dockerfile policy added. +CMD ["server"] diff --git a/pkg/iac-providers/docker/v1/testdata/valid-directory/Dockerfile b/pkg/iac-providers/docker/v1/testdata/valid-directory/Dockerfile index 284cb084e..b5d71074d 100644 --- a/pkg/iac-providers/docker/v1/testdata/valid-directory/Dockerfile +++ b/pkg/iac-providers/docker/v1/testdata/valid-directory/Dockerfile @@ -1,4 +1,7 @@ FROM runatlantis/atlantis:v0.16.1 +#ts:minseverity=High +#ts:maxseverity=None +#ts:skip=AWS.S3Bucket.DS.High.1041 This rule does not belong to dockerfile will add correct once dockerfile policy added.\n" ENV DEFAULT_TERRASCAN_VERSION=1.5.1 ENV PLANFILE tfplan ADD setup.sh terrascan.sh launch-atlantis.sh entrypoint.sh /usr/local/bin/