diff --git a/pkg/cli/run.go b/pkg/cli/run.go index 2b64178a1..971be5c7b 100644 --- a/pkg/cli/run.go +++ b/pkg/cli/run.go @@ -92,6 +92,9 @@ type ScanOptions struct { // showPassedRules indicates whether to display passed rules or not showPassedRules bool + + // nonRecursive enables recursive scan for the terraform iac provider + nonRecursive bool } // NewScanOptions returns a new pointer to ScanOptions @@ -176,7 +179,7 @@ func (s *ScanOptions) Run() error { // create a new runtime executor for processing IaC executor, err := runtime.NewExecutor(s.iacType, s.iacVersion, s.policyType, - s.iacFilePath, s.iacDirPath, s.policyPath, s.scanRules, s.skipRules, s.categories, s.severity) + s.iacFilePath, s.iacDirPath, s.policyPath, s.scanRules, s.skipRules, s.categories, s.severity, s.nonRecursive) if err != nil { return err } diff --git a/pkg/cli/run_test.go b/pkg/cli/run_test.go index fdcfdfde0..33fb05021 100644 --- a/pkg/cli/run_test.go +++ b/pkg/cli/run_test.go @@ -94,6 +94,17 @@ func TestRun(t *testing.T) { outputType: "json", }, }, + { + name: "terraform run with --non-recursive flag", + scanOptions: &ScanOptions{ + iacType: "terraform", + policyType: []string{"all"}, + iacDirPath: testDataDir, + outputType: "json", + nonRecursive: true, + }, + wantErr: true, + }, { name: "normal k8s run", scanOptions: &ScanOptions{ diff --git a/pkg/cli/scan.go b/pkg/cli/scan.go index c7c1b3b61..70fb5edd5 100644 --- a/pkg/cli/scan.go +++ b/pkg/cli/scan.go @@ -68,5 +68,6 @@ func init() { scanCmd.Flags().StringVar(&scanOptions.severity, "severity", "", "minimum severity level of the policy violations to be reported by terrascan") scanCmd.Flags().StringSliceVarP(&scanOptions.categories, "categories", "", []string{}, "list of categories of violations to be reported by terrascan (example: --categories=\"category1,category2\")") scanCmd.Flags().BoolVarP(&scanOptions.showPassedRules, "show-passed", "", false, "display passed rules, along with violations") + scanCmd.Flags().BoolVarP(&scanOptions.nonRecursive, "non-recursive", "", false, "do not scan directories and modules recursively") RegisterCommand(rootCmd, scanCmd) } diff --git a/pkg/http-server/file-scan.go b/pkg/http-server/file-scan.go index 98c52e318..3ab6e36cc 100644 --- a/pkg/http-server/file-scan.go +++ b/pkg/http-server/file-scan.go @@ -151,10 +151,10 @@ func (g *APIHandler) scanFile(w http.ResponseWriter, r *http.Request) { var executor *runtime.Executor if g.test { executor, err = runtime.NewExecutor(iacType, iacVersion, cloudType, - tempFile.Name(), "", []string{"./testdata/testpolicies"}, scanRules, skipRules, categories, severity) + tempFile.Name(), "", []string{"./testdata/testpolicies"}, scanRules, skipRules, categories, severity, false) } else { executor, err = runtime.NewExecutor(iacType, iacVersion, cloudType, - tempFile.Name(), "", getPolicyPathFromConfig(), scanRules, skipRules, categories, severity) + tempFile.Name(), "", getPolicyPathFromConfig(), scanRules, skipRules, categories, severity, false) } if err != nil { zap.S().Error(err) diff --git a/pkg/http-server/remote-repo.go b/pkg/http-server/remote-repo.go index 622a44f3b..06ca9c2ef 100644 --- a/pkg/http-server/remote-repo.go +++ b/pkg/http-server/remote-repo.go @@ -35,15 +35,16 @@ import ( // scanRemoteRepoReq contains request body for remote repository scanning type scanRemoteRepoReq struct { - RemoteType string `json:"remote_type"` - RemoteURL string `json:"remote_url"` - ConfigOnly bool `json:"config_only"` - ScanRules []string `json:"scan_rules"` - SkipRules []string `json:"skip_rules"` - Categories []string `json:"categories"` - Severity string `json:"severity"` - ShowPassed bool `json:"show_passed"` - d downloader.Downloader + RemoteType string `json:"remote_type"` + RemoteURL string `json:"remote_url"` + ConfigOnly bool `json:"config_only"` + ScanRules []string `json:"scan_rules"` + SkipRules []string `json:"skip_rules"` + Categories []string `json:"categories"` + Severity string `json:"severity"` + ShowPassed bool `json:"show_passed"` + NonRecursive bool `json:"non_recursive"` + d downloader.Downloader } // scanRemoteRepo downloads the remote Iac repository and scans it for @@ -126,7 +127,7 @@ func (s *scanRemoteRepoReq) ScanRemoteRepo(iacType, iacVersion string, cloudType // create a new runtime executor for scanning the remote repo executor, err := runtime.NewExecutor(iacType, iacVersion, cloudType, - "", iacDirPath, policyPath, s.ScanRules, s.SkipRules, s.Categories, s.Severity) + "", iacDirPath, policyPath, s.ScanRules, s.SkipRules, s.Categories, s.Severity, s.NonRecursive) if err != nil { zap.S().Error(err) return output, isAdmissionDenied, err diff --git a/pkg/http-server/remote-repo_test.go b/pkg/http-server/remote-repo_test.go index 5bb4fa81d..429d18acf 100644 --- a/pkg/http-server/remote-repo_test.go +++ b/pkg/http-server/remote-repo_test.go @@ -92,17 +92,18 @@ func TestScanRemoteRepoHandler(t *testing.T) { testCloudType := "aws" table := []struct { - name string - iacType string - iacVersion string - cloudType string - remoteURL string - remoteType string - scanRules []string - skipRules []string - showPassed bool - configOnly bool - wantStatus int + name string + iacType string + iacVersion string + cloudType string + remoteURL string + remoteType string + scanRules []string + skipRules []string + showPassed bool + configOnly bool + nonRecursive bool + wantStatus int }{ { name: "empty url and type", @@ -140,6 +141,16 @@ func TestScanRemoteRepoHandler(t *testing.T) { remoteType: "git", wantStatus: http.StatusOK, }, + { + name: "iac type terraform with non-recursive scan", + iacType: testIacType, + iacVersion: testIacVersion, + cloudType: testCloudType, + remoteURL: validRepo, + remoteType: "git", + nonRecursive: true, + wantStatus: http.StatusOK, + }, { name: "valid url and type with scan and skip rules", iacType: testIacType, @@ -175,12 +186,13 @@ func TestScanRemoteRepoHandler(t *testing.T) { // request body s := scanRemoteRepoReq{ - RemoteURL: tt.remoteURL, - RemoteType: tt.remoteType, - ScanRules: tt.scanRules, - SkipRules: tt.skipRules, - ShowPassed: tt.showPassed, - ConfigOnly: tt.configOnly, + RemoteURL: tt.remoteURL, + RemoteType: tt.remoteType, + ScanRules: tt.scanRules, + SkipRules: tt.skipRules, + ShowPassed: tt.showPassed, + ConfigOnly: tt.configOnly, + NonRecursive: tt.nonRecursive, } reqBody, _ := json.Marshal(s) diff --git a/pkg/iac-providers/helm/v3/load-dir.go b/pkg/iac-providers/helm/v3/load-dir.go index 2c961dca5..9f66cc28b 100644 --- a/pkg/iac-providers/helm/v3/load-dir.go +++ b/pkg/iac-providers/helm/v3/load-dir.go @@ -43,7 +43,7 @@ var ( ) // LoadIacDir loads all helm charts under the specified directory -func (h *HelmV3) LoadIacDir(absRootDir string) (output.AllResourceConfigs, error) { +func (h *HelmV3) LoadIacDir(absRootDir string, nonRecursive bool) (output.AllResourceConfigs, error) { allResourcesConfig := make(map[string][]output.ResourceConfig) diff --git a/pkg/iac-providers/helm/v3/load-dir_test.go b/pkg/iac-providers/helm/v3/load-dir_test.go index ffd9712a2..32072e9b1 100644 --- a/pkg/iac-providers/helm/v3/load-dir_test.go +++ b/pkg/iac-providers/helm/v3/load-dir_test.go @@ -79,7 +79,7 @@ func TestLoadIacDir(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - resources, gotErr := tt.helmv3.LoadIacDir(tt.dirPath) + resources, gotErr := tt.helmv3.LoadIacDir(tt.dirPath, false) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) diff --git a/pkg/iac-providers/interface.go b/pkg/iac-providers/interface.go index 8e5d50927..a962c8ea3 100644 --- a/pkg/iac-providers/interface.go +++ b/pkg/iac-providers/interface.go @@ -24,5 +24,5 @@ import ( // to claim support in terrascan type IacProvider interface { LoadIacFile(string) (output.AllResourceConfigs, error) - LoadIacDir(string) (output.AllResourceConfigs, error) + LoadIacDir(string, bool) (output.AllResourceConfigs, error) } diff --git a/pkg/iac-providers/kubernetes/v1/load-dir.go b/pkg/iac-providers/kubernetes/v1/load-dir.go index 208b81f78..019342185 100644 --- a/pkg/iac-providers/kubernetes/v1/load-dir.go +++ b/pkg/iac-providers/kubernetes/v1/load-dir.go @@ -29,7 +29,7 @@ func (*K8sV1) getFileType(file string) string { } // LoadIacDir loads all k8s files in the current directory -func (k *K8sV1) LoadIacDir(absRootDir string) (output.AllResourceConfigs, error) { +func (k *K8sV1) LoadIacDir(absRootDir string, nonRecursive bool) (output.AllResourceConfigs, error) { allResourcesConfig := make(map[string][]output.ResourceConfig) diff --git a/pkg/iac-providers/kubernetes/v1/load-dir_test.go b/pkg/iac-providers/kubernetes/v1/load-dir_test.go index e1e2f711e..1f15fc927 100644 --- a/pkg/iac-providers/kubernetes/v1/load-dir_test.go +++ b/pkg/iac-providers/kubernetes/v1/load-dir_test.go @@ -88,7 +88,7 @@ func TestLoadIacDir(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.k8sV1.LoadIacDir(tt.dirPath) + _, gotErr := tt.k8sV1.LoadIacDir(tt.dirPath, false) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) diff --git a/pkg/iac-providers/kustomize/v3/load-dir.go b/pkg/iac-providers/kustomize/v3/load-dir.go index 9cced6a23..02e7b87d1 100644 --- a/pkg/iac-providers/kustomize/v3/load-dir.go +++ b/pkg/iac-providers/kustomize/v3/load-dir.go @@ -24,7 +24,7 @@ var ( ) // LoadIacDir loads the kustomize directory and returns the ResourceConfig mapping which is evaluated by the policy engine -func (k *KustomizeV3) LoadIacDir(absRootDir string) (output.AllResourceConfigs, error) { +func (k *KustomizeV3) LoadIacDir(absRootDir string, nonRecursive bool) (output.AllResourceConfigs, error) { allResourcesConfig := make(map[string][]output.ResourceConfig) diff --git a/pkg/iac-providers/kustomize/v3/load-dir_test.go b/pkg/iac-providers/kustomize/v3/load-dir_test.go index 867af13c7..c3932cc09 100644 --- a/pkg/iac-providers/kustomize/v3/load-dir_test.go +++ b/pkg/iac-providers/kustomize/v3/load-dir_test.go @@ -90,7 +90,7 @@ func TestLoadIacDir(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - resourceMap, gotErr := tt.kustomize.LoadIacDir(tt.dirPath) + resourceMap, gotErr := tt.kustomize.LoadIacDir(tt.dirPath, false) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) diff --git a/pkg/iac-providers/output/types.go b/pkg/iac-providers/output/types.go index b7e576e34..6b55059e8 100644 --- a/pkg/iac-providers/output/types.go +++ b/pkg/iac-providers/output/types.go @@ -24,12 +24,13 @@ import ( // ResourceConfig describes a resource present in IaC type ResourceConfig struct { - ID string `json:"id"` - Name string `json:"name"` - Source string `json:"source"` - Line int `json:"line"` - Type string `json:"type"` - Config interface{} `json:"config"` + ID string `json:"id"` + Name string `json:"name"` + Source string `json:"source"` + PlanRoot string `json:"plan_root,omitempty" yaml:"plan_root,omitempty" ` + Line int `json:"line"` + Type string `json:"type"` + Config interface{} `json:"config"` // SkipRules will hold the rules to be skipped for the resource. // Each iac provider should append the rules to be skipped for a resource, // while extracting resource from the iac files @@ -47,6 +48,9 @@ type AllResourceConfigs map[string][]ResourceConfig // FindResourceByID Finds a given resource within the resource map and returns a reference to that resource func (a AllResourceConfigs) FindResourceByID(resourceID string) (*ResourceConfig, error) { + if len(a) == 0 { + return nil, fmt.Errorf("AllResourceConfigs is nil or doesn't contain any resource type") + } resTypeName := strings.Split(resourceID, ".") if len(resTypeName) < 2 { return nil, fmt.Errorf("resource ID has an invalid format %s", resourceID) @@ -72,11 +76,38 @@ func (a AllResourceConfigs) FindResourceByID(resourceID string) (*ResourceConfig return &resource, nil } +// FindAllResourcesByID Finds all resources within the resource map +func (a AllResourceConfigs) FindAllResourcesByID(resourceID string) ([]*ResourceConfig, error) { + if len(a) == 0 { + return nil, fmt.Errorf("AllResourceConfigs is nil or doesn't contain any resource type") + } + resTypeName := strings.Split(resourceID, ".") + if len(resTypeName) < 2 { + return nil, fmt.Errorf("resource ID has an invalid format %s", resourceID) + } + + resourceType := resTypeName[0] + + resources := make([]*ResourceConfig, 0) + resourceTypeList := a[resourceType] + for i := range resourceTypeList { + if resourceTypeList[i].ID == resourceID { + resources = append(resources, &resourceTypeList[i]) + } + } + + return resources, nil +} + // GetResourceCount gives out the total number of resources present in a output.ResourceConfig object. // Since the ResourceConfig mapping stores resources in lists which can be located resourceMapping[Type], // `len(resourceMapping)` does not give the count of the resources but only gives out the total number of // the type of resources inside the object. func (a AllResourceConfigs) GetResourceCount() (count int) { + // handles nil map + if len(a) == 0 { + return 0 + } count = 0 for _, list := range a { count = count + len(list) @@ -86,6 +117,13 @@ func (a AllResourceConfigs) GetResourceCount() (count int) { // UpdateResourceConfigs adds a resource of given type if it is not present in allResources func (a AllResourceConfigs) UpdateResourceConfigs(resourceType string, resources []ResourceConfig) { + if _, ok := a[resourceType]; !ok { + if len(a) == 0 { + a = make(AllResourceConfigs) + } + a[resourceType] = resources + return + } for _, res := range resources { if !IsConfigPresent(a[resourceType], res) { a[resourceType] = append(a[resourceType], res) diff --git a/pkg/iac-providers/output/types_test.go b/pkg/iac-providers/output/types_test.go new file mode 100644 index 000000000..d88a325f6 --- /dev/null +++ b/pkg/iac-providers/output/types_test.go @@ -0,0 +1,404 @@ +/* + Copyright (C) 2020 Accurics, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package output + +import ( + "reflect" + "testing" +) + +func TestAllResourceConfigsFindResourceByID(t *testing.T) { + testResourceConfig := ResourceConfig{ + ID: "s3.my_s3_bucket", + } + + type args struct { + resourceID string + } + tests := []struct { + name string + a AllResourceConfigs + args args + want *ResourceConfig + wantErr bool + }{ + { + name: "nil AllResourceConfigs", + a: nil, + args: args{}, + want: nil, + wantErr: true, + }, + { + name: "invalid resource id", + a: AllResourceConfigs{ + "key": {}, + }, + args: args{ + resourceID: "id", + }, + want: nil, + wantErr: true, + }, + { + name: "resource present in AllResourceConfigs", + a: AllResourceConfigs{ + "s3": { + testResourceConfig, + }, + }, + args: args{ + resourceID: "s3.my_s3_bucket", + }, + want: &testResourceConfig, + }, + { + name: "resource not present in AllResourceConfigs", + a: AllResourceConfigs{ + "s3": { + testResourceConfig, + }, + }, + args: args{ + resourceID: "ec2.test_instance", + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.a.FindResourceByID(tt.args.resourceID) + if (err != nil) != tt.wantErr { + t.Errorf("AllResourceConfigs.FindResourceByID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("AllResourceConfigs.FindResourceByID() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAllResourceConfigsGetResourceCount(t *testing.T) { + tests := []struct { + name string + a AllResourceConfigs + wantCount int + }{ + { + name: "nil AllResourceConfigs", + a: nil, + wantCount: 0, + }, + { + name: "non nil AllResourceConfigs", + a: AllResourceConfigs{ + "key1": { + ResourceConfig{}, + ResourceConfig{}, + }, + "key2": { + ResourceConfig{}, + ResourceConfig{}, + }, + "key3": { + ResourceConfig{}, + ResourceConfig{}, + }, + "key4": nil, + }, + wantCount: 6, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotCount := tt.a.GetResourceCount(); gotCount != tt.wantCount { + t.Errorf("AllResourceConfigs.GetResourceCount() = %v, want %v", gotCount, tt.wantCount) + } + }) + } +} + +func TestAllResourceConfigsUpdateResourceConfigs(t *testing.T) { + type args struct { + resourceType string + resources []ResourceConfig + } + tests := []struct { + name string + a AllResourceConfigs + args args + wantLength int + }{ + { + name: "empty resources", + args: args{ + resourceType: "s3", + }, + a: nil, + wantLength: 0, + }, + { + name: "resource present", + args: args{ + resourceType: "s3", + resources: []ResourceConfig{ + { + Name: "s3_bucket", + Source: "s3_bucket.tf", + }, + }, + }, + a: AllResourceConfigs{ + "s3": []ResourceConfig{ + { + Name: "s3_bucket", + Source: "s3_bucket.tf", + }, + }, + }, + wantLength: 1, + }, + { + name: "resource not present, but resource type", + args: args{ + resourceType: "pod", + resources: []ResourceConfig{ + { + Name: "terra_controller", + Source: "terra_controller.yml", + }, + }, + }, + a: AllResourceConfigs{ + "s3": []ResourceConfig{ + { + Name: "s3_bucket", + Source: "s3_bucket.tf", + }, + }, + "pod": []ResourceConfig{ + { + Name: "some_pod", + Source: "some_pod.yml", + }, + }, + }, + wantLength: 3, + }, + { + name: "resource and resource type both not present", + args: args{ + resourceType: "job", + resources: []ResourceConfig{ + { + Name: "cron_job", + Source: "cron_job.yaml", + }, + }, + }, + a: AllResourceConfigs{ + "s3": []ResourceConfig{ + { + Name: "s3_bucket", + Source: "s3_bucket.tf", + }, + { + Name: "zods_s3_bucket", + Source: "zods_s3_bucket.tf", + }, + }, + "pod": []ResourceConfig{ + { + Name: "some_pod", + Source: "some_pod.yml", + }, + }, + }, + wantLength: 4, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.a.UpdateResourceConfigs(tt.args.resourceType, tt.args.resources) + if tt.wantLength != tt.a.GetResourceCount() { + t.Errorf("expected length of all resource config is %d, got %d", tt.wantLength, tt.a.GetResourceCount()) + } + }) + } +} + +func TestIsConfigPresent(t *testing.T) { + testResourceConfig := ResourceConfig{ + Name: "s3_bucket_hulk", + Source: "test.tf", + Config: map[string]interface{}{ + "key1": "smash", + }, + } + + type args struct { + resources []ResourceConfig + resourceConfig ResourceConfig + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "empty resources", + args: args{ + resourceConfig: ResourceConfig{}, + }, + want: false, + }, + { + name: "resource name and source are not same", + args: args{ + resources: []ResourceConfig{ + { + Name: "s3_bucket_thor", + Source: "test.yaml", + }, + { + Name: "ec2_instance_bruce", + Source: "test.yaml", + }, + }, + resourceConfig: testResourceConfig, + }, + want: false, + }, + { + name: "resource name and source are not same", + args: args{ + resources: []ResourceConfig{ + testResourceConfig, + { + Name: "ec2_instance_bruce", + Source: "test.yaml", + }, + }, + resourceConfig: testResourceConfig, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsConfigPresent(tt.args.resources, tt.args.resourceConfig); got != tt.want { + t.Errorf("IsConfigPresent() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAllResourceConfigsFindAllResourcesByID(t *testing.T) { + testS3ResourceConfig := ResourceConfig{ + ID: "s3.my_s3_bucket", + } + + testResourceConfigList := []*ResourceConfig{&testS3ResourceConfig} + + type args struct { + resourceID string + } + tests := []struct { + name string + a AllResourceConfigs + args args + want []*ResourceConfig + wantErr bool + }{ + { + name: "nil AllResourceConfigs", + a: nil, + args: args{}, + want: nil, + wantErr: true, + }, + { + name: "invalid resource id", + a: AllResourceConfigs{ + "key": {}, + }, + args: args{ + resourceID: "id", + }, + want: nil, + wantErr: true, + }, + { + name: "one resource present in AllResourceConfigs", + a: AllResourceConfigs{ + "s3": { + testS3ResourceConfig, + }, + }, + args: args{ + resourceID: "s3.my_s3_bucket", + }, + want: testResourceConfigList, + }, + { + name: "multiple resources present in AllResourceConfigs", + a: AllResourceConfigs{ + "s3": { + testS3ResourceConfig, + testS3ResourceConfig, + }, + "ingress": { + ResourceConfig{ + ID: "allow_ssh", + }, + }, + }, + args: args{ + resourceID: "s3.my_s3_bucket", + }, + want: []*ResourceConfig{ + &testS3ResourceConfig, + &testS3ResourceConfig, + }, + }, + { + name: "resource not present in AllResourceConfigs", + a: AllResourceConfigs{ + "s3": { + testS3ResourceConfig, + }, + }, + args: args{ + resourceID: "ec2.test_instance", + }, + want: []*ResourceConfig{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.a.FindAllResourcesByID(tt.args.resourceID) + if (err != nil) != tt.wantErr { + t.Errorf("AllResourceConfigs.FindAllResourcesByID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("AllResourceConfigs.FindAllResourcesByID() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/iac-providers/terraform/commons/load-dir.go b/pkg/iac-providers/terraform/commons/load-dir.go index 90fd1ca07..8465beb8b 100644 --- a/pkg/iac-providers/terraform/commons/load-dir.go +++ b/pkg/iac-providers/terraform/commons/load-dir.go @@ -36,8 +36,7 @@ import ( var ( // ErrBuildTFConfigDir error - ErrBuildTFConfigDir = fmt.Errorf("failed to build terraform allResourcesConfig") - errIacLoadDirs *multierror.Error = nil + ErrBuildTFConfigDir = fmt.Errorf("failed to build terraform allResourcesConfig") ) // ModuleConfig contains the *hclConfigs.Config for every module in the @@ -49,76 +48,198 @@ type ModuleConfig struct { ParentModuleCall *hclConfigs.ModuleCall } +// TerraformDirectoryLoader implements terraform directory loading +type TerraformDirectoryLoader struct { + absRootDir string + nonRecursive bool + remoteDownloader downloader.ModuleDownloader + parser *hclConfigs.Parser + errIacLoadDirs *multierror.Error +} + +// NewTerraformDirectoryLoader creates a new terraformDirectoryLoader +func NewTerraformDirectoryLoader(rootDirectory string, nonRecursive bool) TerraformDirectoryLoader { + return TerraformDirectoryLoader{ + absRootDir: rootDirectory, + nonRecursive: nonRecursive, + remoteDownloader: downloader.NewRemoteDownloader(), + parser: hclConfigs.NewParser(afero.NewOsFs()), + } +} + // LoadIacDir starts traversing from the given rootDir and traverses through // all the descendant modules present to create an output list of all the // resources present in rootDir and descendant modules -func LoadIacDir(absRootDir string) (allResourcesConfig output.AllResourceConfigs, err error) { +func (t TerraformDirectoryLoader) LoadIacDir() (allResourcesConfig output.AllResourceConfigs, err error) { - // create a new config parser - parser := hclConfigs.NewParser(afero.NewOsFs()) + defer t.remoteDownloader.CleanUp() - // check if the directory has any tf config files (.tf or .tf.json) - if !parser.IsConfigDir(absRootDir) { - errMessage := fmt.Sprintf("directory '%s' has no terraform config files", absRootDir) - zap.S().Debug(errMessage) - return allResourcesConfig, multierror.Append(errIacLoadDirs, results.DirScanErr{IacType: "terraform", Directory: absRootDir, ErrMessage: errMessage}) + if t.nonRecursive { + return t.loadDirNonRecursive() } - // load root config directory - rootMod, diags := parser.LoadConfigDir(absRootDir) - if diags.HasErrors() { - errMessage := fmt.Sprintf("failed to load terraform config dir '%s'. error from terraform:\n%+v\n", absRootDir, getErrorMessagesFromDiagnostics(diags)) - zap.S().Debug(errMessage) - return allResourcesConfig, multierror.Append(errIacLoadDirs, results.DirScanErr{IacType: "terraform", Directory: absRootDir, ErrMessage: errMessage}) + // Walk the file path and find all directories + dirList, err := utils.FindAllDirectories(t.absRootDir) + if err != nil { + return nil, multierror.Append(t.errIacLoadDirs, err) } + dirList = utils.FilterHiddenDirectories(dirList, t.absRootDir) - // create a new downloader to install remote modules - r := downloader.NewRemoteDownloader() - defer r.CleanUp() + return t.loadDirRecursive(dirList) +} - // using the BuildConfig and ModuleWalkerFunc to traverse through all - // descendant modules from the root module and create a unified - // configuration of type *configs.Config - // Note: currently, only Local paths are supported for Module Sources - versionI := 0 - unified, diags := hclConfigs.BuildConfig(rootMod, hclConfigs.ModuleWalkerFunc( - func(req *hclConfigs.ModuleRequest) (*hclConfigs.Module, *version.Version, hcl.Diagnostics) { +func (t TerraformDirectoryLoader) loadDirRecursive(dirList []string) (output.AllResourceConfigs, error) { - // figure out path sub module directory, if it's remote then download it locally - var pathToModule string - if downloader.IsLocalSourceAddr(req.SourceAddr) { + // initialize normalized output + allResourcesConfig := make(map[string][]output.ResourceConfig) + + for _, dir := range dirList { + // check if the directory has any tf config files (.tf or .tf.json) + if !t.parser.IsConfigDir(dir) { + // log a debug message and continue with other directories + errMessage := fmt.Sprintf("directory '%s' has no terraform config files", dir) + zap.S().Debug(errMessage) + t.addError(errMessage, dir) + continue + } - pathToModule = processLocalSource(req) - zap.S().Debugf("processing local module %q", pathToModule) - } else if downloader.IsRegistrySourceAddr(req.SourceAddr) { - // temp dir to download the remote repo - tempDir := generateTempDir() + // load current config directory + rootMod, diags := t.parser.LoadConfigDir(dir) + if diags.HasErrors() { + // log a debug message and continue with other directories + errMessage := fmt.Sprintf("failed to load terraform config dir '%s'. error from terraform:\n%+v\n", dir, getErrorMessagesFromDiagnostics(diags)) + zap.S().Debug(errMessage) + t.addError(errMessage, dir) + continue + } + + // get unified config for the current directory + unified, diags := t.buildUnifiedConfig(rootMod, dir) + + if diags.HasErrors() { + // log a warn message in this case because there are errors in + // loading the config dir, and continue with other directories + errMessage := fmt.Sprintf("failed to build unified config. errors:\n%+v\n", diags) + zap.S().Warnf(errMessage) + t.addError(errMessage, dir) + continue + } - pathToModule, err = processTerraformRegistrySource(req, tempDir, r) + /* + The "unified" config created from BuildConfig in the previous step + represents a tree structure with rootDir module being at its root and + all the sub modules being its children, and these children can have + more children and so on... + + Now, using BFS we traverse through all the submodules using the classic + approach of using a queue data structure + */ + + // queue of for BFS, add root module config to it + root := &ModuleConfig{Config: unified.Root} + configsQ := []*ModuleConfig{root} + + // using BFS traverse through all modules in the unified config tree + zap.S().Debug("traversing through all modules in config tree") + for len(configsQ) > 0 { + + // pop first element from the queue + current := configsQ[0] + configsQ = configsQ[1:] + + // reference resolver + r := NewRefResolver(current.Config, current.ParentModuleCall) + + // traverse through all current's resources + for _, managedResource := range current.Config.Module.ManagedResources { + + // create output.ResourceConfig from hclConfigs.Resource + resourceConfig, err := CreateResourceConfig(managedResource) if err != nil { - zap.S().Errorf("failed to download remote module %q. error: '%v'", req.SourceAddr, err) + t.addError(err.Error(), dir) + continue } - } else { - // temp dir to download the remote repo - tempDir := generateTempDir() - // Download remote module - pathToModule, err = r.DownloadModule(req.SourceAddr, tempDir) + // resolve references + resourceConfig.Config = r.ResolveRefs(resourceConfig.Config.(jsonObj)) + + // source file path + resourceConfig.Source, err = filepath.Rel(t.absRootDir, resourceConfig.Source) if err != nil { - zap.S().Errorf("failed to download remote module %q. error: '%v'", req.SourceAddr, err) + t.addError(err.Error(), dir) + continue + } + + // tf plan directory relative path + planRoot, err := filepath.Rel(t.absRootDir, dir) + if err != nil { + t.addError(err.Error(), dir) + continue + } + if t.absRootDir == dir { + planRoot = fmt.Sprintf(".%s", string(os.PathSeparator)) + } + resourceConfig.PlanRoot = planRoot + + // append to normalized output + if _, present := allResourcesConfig[resourceConfig.Type]; !present { + allResourcesConfig[resourceConfig.Type] = []output.ResourceConfig{resourceConfig} + } else { + resources := allResourcesConfig[resourceConfig.Type] + if !output.IsConfigPresent(resources, resourceConfig) { + allResourcesConfig[resourceConfig.Type] = append(allResourcesConfig[resourceConfig.Type], resourceConfig) + } } } - // load sub module directory - subMod, diags := parser.LoadConfigDir(pathToModule) - version, _ := version.NewVersion(fmt.Sprintf("1.0.%d", versionI)) - versionI++ - return subMod, version, diags - }, - )) + // add all current's children to the queue + for childName, childModule := range current.Config.Children { + childModuleConfig := &ModuleConfig{ + Config: childModule, + ParentModuleCall: current.Config.Module.ModuleCalls[childName], + } + configsQ = append(configsQ, childModuleConfig) + } + } + } + + // successful + return allResourcesConfig, t.errIacLoadDirs +} + +// loadDirNonRecursive has duplicate code +// this function will be removed when we deprecate non recursive scan +func (t TerraformDirectoryLoader) loadDirNonRecursive() (output.AllResourceConfigs, error) { + + // initialize normalized output + allResourcesConfig := make(map[string][]output.ResourceConfig) + + // check if the directory has any tf config files (.tf or .tf.json) + if !t.parser.IsConfigDir(t.absRootDir) { + // log a debug message and continue with other directories + errMessage := fmt.Sprintf("directory '%s' has no terraform config files", t.absRootDir) + zap.S().Debug(errMessage) + return nil, multierror.Append(t.errIacLoadDirs, results.DirScanErr{IacType: "terraform", Directory: t.absRootDir, ErrMessage: errMessage}) + } + + // load current config directory + rootMod, diags := t.parser.LoadConfigDir(t.absRootDir) + if diags.HasErrors() { + // log a debug message and continue with other directories + errMessage := fmt.Sprintf("failed to load terraform config dir '%s'. error from terraform:\n%+v\n", t.absRootDir, getErrorMessagesFromDiagnostics(diags)) + zap.S().Debug(errMessage) + return nil, multierror.Append(t.errIacLoadDirs, results.DirScanErr{IacType: "terraform", Directory: t.absRootDir, ErrMessage: errMessage}) + } + + // get unified config for the current directory + unified, diags := t.buildUnifiedConfig(rootMod, t.absRootDir) + if diags.HasErrors() { - zap.S().Errorf("failed to build unified config. errors:\n%+v\n", diags) - return allResourcesConfig, multierror.Append(errIacLoadDirs, results.DirScanErr{IacType: "terraform", Directory: absRootDir, ErrMessage: ErrBuildTFConfigDir.Error()}) + // log a warn message in this case because there are errors in + // loading the config dir, and continue with other directories + errMessage := fmt.Sprintf("failed to build unified config. errors:\n%+v\n", diags) + zap.S().Warnf(errMessage) + return nil, multierror.Append(t.errIacLoadDirs, results.DirScanErr{IacType: "terraform", Directory: t.absRootDir, ErrMessage: ErrBuildTFConfigDir.Error()}) } /* @@ -135,9 +256,6 @@ func LoadIacDir(absRootDir string) (allResourcesConfig output.AllResourceConfigs root := &ModuleConfig{Config: unified.Root} configsQ := []*ModuleConfig{root} - // initialize normalized output - allResourcesConfig = make(map[string][]output.ResourceConfig) - // using BFS traverse through all modules in the unified config tree zap.S().Debug("traversing through all modules in config tree") for len(configsQ) > 0 { @@ -155,25 +273,30 @@ func LoadIacDir(absRootDir string) (allResourcesConfig output.AllResourceConfigs // create output.ResourceConfig from hclConfigs.Resource resourceConfig, err := CreateResourceConfig(managedResource) if err != nil { - errMsg := fmt.Sprintf("failed to create ResourceConfig. err: %v", err) - return allResourcesConfig, multierror.Append(errIacLoadDirs, results.DirScanErr{IacType: "terraform", Directory: absRootDir, ErrMessage: errMsg}) + return allResourcesConfig, multierror.Append(t.errIacLoadDirs, results.DirScanErr{IacType: "terraform", Directory: t.absRootDir, ErrMessage: "failed to create ResourceConfig"}) } // resolve references resourceConfig.Config = r.ResolveRefs(resourceConfig.Config.(jsonObj)) // source file path - resourceConfig.Source, err = filepath.Rel(absRootDir, resourceConfig.Source) + resourceConfig.Source, err = filepath.Rel(t.absRootDir, resourceConfig.Source) if err != nil { - errMsg := fmt.Sprintf("failed to get resource: %s", err) - return allResourcesConfig, multierror.Append(errIacLoadDirs, results.DirScanErr{IacType: "terraform", Directory: absRootDir, ErrMessage: errMsg}) + errMessage := fmt.Sprintf("failed to get resource's filepath: %v", err) + return allResourcesConfig, multierror.Append(t.errIacLoadDirs, results.DirScanErr{IacType: "terraform", Directory: t.absRootDir, ErrMessage: errMessage}) } + // add tf plan directory relative path + resourceConfig.PlanRoot = fmt.Sprintf(".%s", string(os.PathSeparator)) + // append to normalized output if _, present := allResourcesConfig[resourceConfig.Type]; !present { allResourcesConfig[resourceConfig.Type] = []output.ResourceConfig{resourceConfig} } else { - allResourcesConfig[resourceConfig.Type] = append(allResourcesConfig[resourceConfig.Type], resourceConfig) + resources := allResourcesConfig[resourceConfig.Type] + if !output.IsConfigPresent(resources, resourceConfig) { + allResourcesConfig[resourceConfig.Type] = append(allResourcesConfig[resourceConfig.Type], resourceConfig) + } } } @@ -188,14 +311,59 @@ func LoadIacDir(absRootDir string) (allResourcesConfig output.AllResourceConfigs } // successful - return allResourcesConfig, errIacLoadDirs + return allResourcesConfig, t.errIacLoadDirs +} + +// buildUnifiedConfig builds a unified config from *hclConfigs.Module object specified for a dir +func (t TerraformDirectoryLoader) buildUnifiedConfig(rootMod *hclConfigs.Module, dir string) (*hclConfigs.Config, hcl.Diagnostics) { + // using the BuildConfig and ModuleWalkerFunc to traverse through all + // descendant modules from the root module and create a unified + // configuration of type *configs.Config + versionI := 0 + return hclConfigs.BuildConfig(rootMod, hclConfigs.ModuleWalkerFunc( + func(req *hclConfigs.ModuleRequest) (*hclConfigs.Module, *version.Version, hcl.Diagnostics) { + + // figure out path sub module directory, if it's remote then download it locally + var pathToModule string + var err error + if downloader.IsLocalSourceAddr(req.SourceAddr) { + + pathToModule = t.processLocalSource(req) + zap.S().Debugf("processing local module %q", pathToModule) + } else if downloader.IsRegistrySourceAddr(req.SourceAddr) { + // temp dir to download the remote repo + tempDir := utils.GenerateTempDir() + + pathToModule, err = t.processTerraformRegistrySource(req, tempDir) + if err != nil { + zap.S().Errorf("failed to download remote module %q. error: '%v'", req.SourceAddr, err) + } + } else { + // temp dir to download the remote repo + tempDir := utils.GenerateTempDir() + + // Download remote module + pathToModule, err = t.remoteDownloader.DownloadModule(req.SourceAddr, tempDir) + if err != nil { + zap.S().Errorf("failed to download remote module %q. error: '%v'", req.SourceAddr, err) + } + } + + // load sub module directory + subMod, diags := t.parser.LoadConfigDir(pathToModule) + version, _ := version.NewVersion(fmt.Sprintf("1.0.%d", versionI)) + versionI++ + return subMod, version, diags + }, + )) } -func generateTempDir() string { - return filepath.Join(os.TempDir(), utils.GenRandomString(6)) +// addError adds error load dir errors +func (t *TerraformDirectoryLoader) addError(errMessage, directory string) { + t.errIacLoadDirs = multierror.Append(t.errIacLoadDirs, results.DirScanErr{IacType: "terraform", Directory: directory, ErrMessage: errMessage}) } -func processLocalSource(req *hclConfigs.ModuleRequest) string { +func (t TerraformDirectoryLoader) processLocalSource(req *hclConfigs.ModuleRequest) string { // determine the absolute path from root module to the sub module // while building the unified config, recursive calls are made for all the paths resolved, @@ -209,12 +377,12 @@ func processLocalSource(req *hclConfigs.ModuleRequest) string { return filepath.Join(callDirPath, req.SourceAddr) } -func processTerraformRegistrySource(req *hclConfigs.ModuleRequest, tempDir string, m downloader.ModuleDownloader) (string, error) { +func (t TerraformDirectoryLoader) processTerraformRegistrySource(req *hclConfigs.ModuleRequest, tempDir string) (string, error) { // regsrc.ParseModuleSource func returns a terraform registry module source // error check is not required as the source address is already validated module, _ := regsrc.ParseModuleSource(req.SourceAddr) - pathToModule, err := m.DownloadRemoteModule(req.VersionConstraint, tempDir, module) + pathToModule, err := t.remoteDownloader.DownloadRemoteModule(req.VersionConstraint, tempDir, module) if err != nil { return pathToModule, err } diff --git a/pkg/iac-providers/terraform/commons/load-dir_test.go b/pkg/iac-providers/terraform/commons/load-dir_test.go index b2ffc79f9..9525fc5e5 100644 --- a/pkg/iac-providers/terraform/commons/load-dir_test.go +++ b/pkg/iac-providers/terraform/commons/load-dir_test.go @@ -22,6 +22,7 @@ import ( "testing" "github.com/accurics/terrascan/pkg/downloader" + "github.com/accurics/terrascan/pkg/utils" "github.com/hashicorp/hcl/v2" hclConfigs "github.com/hashicorp/terraform/configs" ) @@ -59,7 +60,8 @@ func TestProcessLocalSource(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := processLocalSource(tt.args.req); got != tt.want { + dl := NewTerraformDirectoryLoader("", false) + if got := dl.processLocalSource(tt.args.req); got != tt.want { t.Errorf("processLocalSource() got = %v, want = %v", got, tt.want) } }) @@ -67,7 +69,7 @@ func TestProcessLocalSource(t *testing.T) { } func TestProcessTerraformRegistrySource(t *testing.T) { - testTempDir := generateTempDir() + testTempDir := utils.GenerateTempDir() type args struct { req *hclConfigs.ModuleRequest @@ -88,7 +90,7 @@ func TestProcessTerraformRegistrySource(t *testing.T) { SourceAddr: "test.com/test/eks/aws", }, remoteModPaths: make(map[string]string), - tempDir: generateTempDir(), + tempDir: utils.GenerateTempDir(), m: downloader.NewRemoteDownloader(), }, wantErr: true, @@ -109,7 +111,8 @@ func TestProcessTerraformRegistrySource(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { defer os.RemoveAll(tt.args.tempDir) - got, err := processTerraformRegistrySource(tt.args.req, tt.args.tempDir, tt.args.m) + dl := NewTerraformDirectoryLoader("", false) + got, err := dl.processTerraformRegistrySource(tt.args.req, tt.args.tempDir) if (err != nil) != tt.wantErr { t.Errorf("processTerraformRegistrySource() got error = %v, wantErr = %v", err, tt.wantErr) return diff --git a/pkg/iac-providers/terraform/v12/load-dir.go b/pkg/iac-providers/terraform/v12/load-dir.go index 654f0376d..471a08465 100644 --- a/pkg/iac-providers/terraform/v12/load-dir.go +++ b/pkg/iac-providers/terraform/v12/load-dir.go @@ -25,7 +25,7 @@ import ( // LoadIacDir starts traversing from the given rootDir and traverses through // all the descendant modules present to create an output list of all the // resources present in rootDir and descendant modules -func (*TfV12) LoadIacDir(absRootDir string) (allResourcesConfig output.AllResourceConfigs, err error) { +func (*TfV12) LoadIacDir(absRootDir string, nonRecursive bool) (allResourcesConfig output.AllResourceConfigs, err error) { zap.S().Warn("There may be a few breaking changes while working with terraform v0.12 files. For further information, refer to https://github.com/accurics/terrascan/releases/v1.3.0") - return commons.LoadIacDir(absRootDir) + return commons.NewTerraformDirectoryLoader(absRootDir, nonRecursive).LoadIacDir() } diff --git a/pkg/iac-providers/terraform/v12/load-dir_test.go b/pkg/iac-providers/terraform/v12/load-dir_test.go index b398e6a7c..df5e344c7 100644 --- a/pkg/iac-providers/terraform/v12/load-dir_test.go +++ b/pkg/iac-providers/terraform/v12/load-dir_test.go @@ -20,8 +20,9 @@ import ( "encoding/json" "fmt" "io/ioutil" + "os" "path/filepath" - "reflect" + "syscall" "testing" "github.com/accurics/terrascan/pkg/iac-providers/output" @@ -31,6 +32,11 @@ import ( ) func TestLoadIacDir(t *testing.T) { + var nilMultiErr *multierror.Error = nil + + destroyProvisionersDir := filepath.Join(testDataDir, "destroy-provisioners") + destroyProvisionersMainFile := filepath.Join(destroyProvisionersDir, "main.tf") + testErrorString1 := fmt.Sprintf(`failed to load terraform config dir '%s'. error from terraform: %s:1,21-2,1: Invalid block definition; A block definition must have block content delimited by "{" and "}", starting on the same line as the block header. %s:1,1-5: Unsupported block type; Blocks of type "some" are not expected here. @@ -42,45 +48,113 @@ func TestLoadIacDir(t *testing.T) { %s:2,3-21: Duplicate required providers configuration; A module may have only one required providers configuration. The required providers were previously configured at %s:2,3-21. `, multipleProvidersDir, filepath.Join(multipleProvidersDir, "b.tf"), filepath.Join(multipleProvidersDir, "a.tf")) + errStringInvalidModuleConfigs := fmt.Sprintf(`failed to build unified config. errors: +: Failed to read module directory; Module directory %s does not exist or cannot be read. +`, filepath.Join(testDataDir, "invalid-moduleconfigs", "cloudfront", "sub-cloudfront")) + + errStringDestroyProvisioners := fmt.Sprintf(`failed to load terraform config dir '%s'. error from terraform: +%s:8,12-22: Invalid reference from destroy provisioner; Destroy-time provisioners and their connection configurations may only reference attributes of the related resource, via 'self', 'count.index', or 'each.key'. + +References to other resources during the destroy phase can cause dependency cycles and interact poorly with create_before_destroy. +%s:42,15-35: Invalid reference from destroy provisioner; Destroy-time provisioners and their connection configurations may only reference attributes of the related resource, via 'self', 'count.index', or 'each.key'. + +References to other resources during the destroy phase can cause dependency cycles and interact poorly with create_before_destroy. +%s:39,14-24: Invalid reference from destroy provisioner; Destroy-time provisioners and their connection configurations may only reference attributes of the related resource, via 'self', 'count.index', or 'each.key'. + +References to other resources during the destroy phase can cause dependency cycles and interact poorly with create_before_destroy. +%s:34,1-29: Duplicate resource "null_resource" configuration; A null_resource resource named "b" was already declared at testdata/destroy-provisioners/main.tf:23,1-29. Resource names must be unique per type in each module. +`, destroyProvisionersDir, destroyProvisionersMainFile, destroyProvisionersMainFile, destroyProvisionersMainFile, destroyProvisionersMainFile) + testDirPath1 := "not-there" testDirPath2 := filepath.Join(testDataDir, "testfile") invalidDirErrStringTemplate := "directory '%s' has no terraform config files" + pathErr := &os.PathError{Op: "lstat", Path: "not-there", Err: syscall.ENOENT} + if utils.IsWindowsPlatform() { + pathErr = &os.PathError{Op: "CreateFile", Path: "not-there", Err: syscall.ENOENT} + } + table := []struct { - name string - dirPath string - tfv12 TfV12 - want output.AllResourceConfigs - wantErr error + name string + dirPath string + tfv12 TfV12 + want output.AllResourceConfigs + nonRecursive bool + wantErr error }{ { - name: "invalid dirPath", + name: "invalid dirPath", + dirPath: testDirPath1, + tfv12: TfV12{}, + nonRecursive: true, + wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath1)), + }, + { + name: "invalid dirPath recursive", dirPath: testDirPath1, tfv12: TfV12{}, - wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath1)), + wantErr: multierror.Append(pathErr), }, { - name: "empty config", + name: "empty config", + dirPath: testDirPath2, + tfv12: TfV12{}, + nonRecursive: true, + wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath2)), + }, + { + name: "empty config recursive", dirPath: testDirPath2, tfv12: TfV12{}, - wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath2)), + wantErr: nilMultiErr, }, { - name: "incorrect module structure", + name: "incorrect module structure", + dirPath: filepath.Join(testDataDir, "invalid-moduleconfigs"), + tfv12: TfV12{}, + nonRecursive: true, + wantErr: multierror.Append(fmt.Errorf("failed to build terraform allResourcesConfig")), + }, + { + name: "incorrect module structure recursive", dirPath: filepath.Join(testDataDir, "invalid-moduleconfigs"), tfv12: TfV12{}, - wantErr: multierror.Append(fmt.Errorf("failed to build terraform allResourcesConfig")), + // same error is loaded two times because, both root module and a child module will generated same error + wantErr: multierror.Append(fmt.Errorf(errStringInvalidModuleConfigs), fmt.Errorf(errStringInvalidModuleConfigs)), + }, + { + name: "load invalid config dir", + dirPath: testDataDir, + tfv12: TfV12{}, + nonRecursive: true, + wantErr: multierror.Append(fmt.Errorf(testErrorString1)), }, { - name: "load invalid config dir", + name: "load invalid config dir recursive", dirPath: testDataDir, tfv12: TfV12{}, - wantErr: multierror.Append(fmt.Errorf(testErrorString1)), + wantErr: multierror.Append(fmt.Errorf(testErrorString1), + fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "deep-modules", "modules")), + fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "deep-modules", "modules", "m4", "modules")), + fmt.Errorf(errStringDestroyProvisioners), + fmt.Errorf(errStringInvalidModuleConfigs), + fmt.Errorf(errStringInvalidModuleConfigs), + fmt.Errorf(testErrorString2), + fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "relative-moduleconfigs")), + fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "tfjson")), + ), + }, + { + name: "load multiple provider config dir", + dirPath: multipleProvidersDir, + tfv12: TfV12{}, + nonRecursive: true, + wantErr: multierror.Append(fmt.Errorf(testErrorString2)), }, { - name: "load invalid config dir", + name: "load multiple provider config dir recursive", dirPath: multipleProvidersDir, tfv12: TfV12{}, wantErr: multierror.Append(fmt.Errorf(testErrorString2)), @@ -89,42 +163,71 @@ func TestLoadIacDir(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.tfv12.LoadIacDir(tt.dirPath) - if gotErr.Error() != tt.wantErr.Error() { + _, gotErr := tt.tfv12.LoadIacDir(tt.dirPath, tt.nonRecursive) + me, ok := gotErr.(*multierror.Error) + if !ok { + t.Errorf("expected multierror.Error, got %T", gotErr) + } + if tt.wantErr == nilMultiErr { + if err := me.ErrorOrNil(); err != nil { + t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) + } + } else if me.Error() != tt.wantErr.Error() { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } }) } - var nilMultiErr *multierror.Error = nil + nestedModuleErr1 := fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "deep-modules", "modules")) + nestedModuleErr2 := fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "deep-modules", "modules", "m4", "modules")) table2 := []struct { - name string - tfConfigDir string - tfJSONFile string - tfv12 TfV12 - wantErr error + name string + tfConfigDir string + tfJSONFile string + tfv12 TfV12 + nonRecursive bool + wantErr error }{ { - name: "config1", + name: "config1", + tfConfigDir: filepath.Join(testDataDir, "tfconfigs"), + tfJSONFile: filepath.Join(tfJSONDir, "fullconfig.json"), + tfv12: TfV12{}, + nonRecursive: true, + wantErr: nilMultiErr, + }, + { + name: "config1 recursive", tfConfigDir: filepath.Join(testDataDir, "tfconfigs"), - tfJSONFile: filepath.Join(tfJSONDir, "fullconfig.json"), - tfv12: TfV12{}, - wantErr: nilMultiErr, + // no change in the output expected as the config dir doesn't contain subfolder + tfJSONFile: filepath.Join(tfJSONDir, "fullconfig.json"), + tfv12: TfV12{}, + wantErr: nilMultiErr, }, { - name: "module directory", + name: "module directory", + tfConfigDir: filepath.Join(testDataDir, "moduleconfigs"), + tfJSONFile: filepath.Join(tfJSONDir, "moduleconfigs.json"), + tfv12: TfV12{}, + nonRecursive: true, + wantErr: nilMultiErr, + }, + { + name: "module directory recursive", tfConfigDir: filepath.Join(testDataDir, "moduleconfigs"), - tfJSONFile: filepath.Join(tfJSONDir, "moduleconfigs.json"), - tfv12: TfV12{}, - wantErr: nilMultiErr, + // no change in the output expected as the config dir doesn't contain subfolder + tfJSONFile: filepath.Join(tfJSONDir, "moduleconfigs.json"), + tfv12: TfV12{}, + wantErr: nilMultiErr, }, { - name: "nested module directory", - tfConfigDir: filepath.Join(testDataDir, "deep-modules"), - tfJSONFile: filepath.Join(tfJSONDir, "deep-modules.json"), - tfv12: TfV12{}, - wantErr: nilMultiErr, + name: "nested module directory", + tfConfigDir: filepath.Join(testDataDir, "deep-modules"), + tfJSONFile: filepath.Join(tfJSONDir, "deep-modules.json"), + tfv12: TfV12{}, + nonRecursive: true, + wantErr: nilMultiErr, }, { name: "variables of list type", @@ -133,12 +236,27 @@ func TestLoadIacDir(t *testing.T) { tfv12: TfV12{}, wantErr: nilMultiErr, }, + { + name: "nested module directory recursive", + tfConfigDir: filepath.Join(testDataDir, "deep-modules"), + tfJSONFile: filepath.Join(tfJSONDir, "deep-modules-recursive.json"), + tfv12: TfV12{}, + wantErr: multierror.Append(nestedModuleErr1, nestedModuleErr2), + }, } for _, tt := range table2 { t.Run(tt.name, func(t *testing.T) { - got, gotErr := tt.tfv12.LoadIacDir(tt.tfConfigDir) - if !reflect.DeepEqual(gotErr, tt.wantErr) { + got, gotErr := tt.tfv12.LoadIacDir(tt.tfConfigDir, tt.nonRecursive) + me, ok := gotErr.(*multierror.Error) + if !ok { + t.Errorf("expected multierror.Error, got %T", gotErr) + } + if tt.wantErr == nilMultiErr { + if err := me.ErrorOrNil(); err != nil { + t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) + } + } else if me.Error() != tt.wantErr.Error() { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } diff --git a/pkg/iac-providers/terraform/v12/testdata/tfjson/deep-modules-recursive.json b/pkg/iac-providers/terraform/v12/testdata/tfjson/deep-modules-recursive.json new file mode 100644 index 000000000..33a4e710c --- /dev/null +++ b/pkg/iac-providers/terraform/v12/testdata/tfjson/deep-modules-recursive.json @@ -0,0 +1,56 @@ +{ + "aws_s3_bucket": [ + { + "id": "aws_s3_bucket.bucket", + "name": "bucket", + "source": "modules/m1/main.tf", + "plan_root": "./", + "line": 20, + "type": "aws_s3_bucket", + "config": { + "bucket": "${module.m3.fullbucketname}", + "policy": "${module.m2.fullbucketpolicy}" + }, + "skip_rules": null + }, + { + "id": "aws_s3_bucket.bucket", + "name": "bucket", + "source": "modules/m4/main.tf", + "plan_root": "./", + "line": 11, + "type": "aws_s3_bucket", + "config": { + "bucket": "tf-test-project-2", + "policy": "${module.m4a.fullbucketpolicy}" + }, + "skip_rules": null + }, + { + "id": "aws_s3_bucket.bucket4a", + "name": "bucket4a", + "source": "modules/m4/modules/m4a/main.tf", + "plan_root": "./", + "line": 20, + "type": "aws_s3_bucket", + "config": { + "bucket": "${module.m4c.fullbucketname}", + "policy": "${module.m4b.fullbucketpolicy}" + }, + "skip_rules": null + }, + { + "id": "aws_s3_bucket.bucket", + "name": "bucket", + "source": "modules/m4/main.tf", + "plan_root": "modules/m4", + "line": 11, + "type": "aws_s3_bucket", + "config": { + "bucket": "asdfasdf", + "policy": "${module.m4a.fullbucketpolicy}" + }, + "skip_rules": null + } + ] +} \ No newline at end of file diff --git a/pkg/iac-providers/terraform/v12/testdata/tfjson/deep-modules.json b/pkg/iac-providers/terraform/v12/testdata/tfjson/deep-modules.json index a2b624d18..78626e6c5 100644 --- a/pkg/iac-providers/terraform/v12/testdata/tfjson/deep-modules.json +++ b/pkg/iac-providers/terraform/v12/testdata/tfjson/deep-modules.json @@ -1,37 +1,40 @@ { - "aws_s3_bucket": [ - { - "id": "aws_s3_bucket.bucket", - "name": "bucket", - "source": "modules/m1/main.tf", - "line": 20, - "type": "aws_s3_bucket", - "config": { - "bucket": "${module.m3.fullbucketname}", - "policy": "${module.m2.fullbucketpolicy}" + "aws_s3_bucket": [ + { + "id": "aws_s3_bucket.bucket", + "name": "bucket", + "source": "modules/m1/main.tf", + "plan_root": "./", + "line": 20, + "type": "aws_s3_bucket", + "config": { + "bucket": "${module.m3.fullbucketname}", + "policy": "${module.m2.fullbucketpolicy}" + } + }, + { + "id": "aws_s3_bucket.bucket", + "name": "bucket", + "source": "modules/m4/main.tf", + "plan_root": "./", + "line": 11, + "type": "aws_s3_bucket", + "config": { + "bucket": "tf-test-project-2", + "policy": "${module.m4a.fullbucketpolicy}" + } + }, + { + "id": "aws_s3_bucket.bucket4a", + "name": "bucket4a", + "source": "modules/m4/modules/m4a/main.tf", + "plan_root": "./", + "line": 20, + "type": "aws_s3_bucket", + "config": { + "bucket": "${module.m4c.fullbucketname}", + "policy": "${module.m4b.fullbucketpolicy}" + } } - }, - { - "id": "aws_s3_bucket.bucket", - "name": "bucket", - "source": "modules/m4/main.tf", - "line": 11, - "type": "aws_s3_bucket", - "config": { - "bucket": "tf-test-project-2", - "policy": "${module.m4a.fullbucketpolicy}" - } - }, - { - "id": "aws_s3_bucket.bucket4a", - "name": "bucket4a", - "source": "modules/m4/modules/m4a/main.tf", - "line": 20, - "type": "aws_s3_bucket", - "config": { - "bucket": "${module.m4c.fullbucketname}", - "policy": "${module.m4b.fullbucketpolicy}" - } - } - ] -} + ] + } \ No newline at end of file diff --git a/pkg/iac-providers/terraform/v12/testdata/tfjson/fullconfig.json b/pkg/iac-providers/terraform/v12/testdata/tfjson/fullconfig.json index 307ab6619..07d176b78 100644 --- a/pkg/iac-providers/terraform/v12/testdata/tfjson/fullconfig.json +++ b/pkg/iac-providers/terraform/v12/testdata/tfjson/fullconfig.json @@ -4,6 +4,7 @@ "id": "aws_instance.instance_playground", "name": "instance_playground", "source": "config1.tf", + "plan_root": "./", "line": 77, "type": "aws_instance", "config": { @@ -38,7 +39,8 @@ "vpc_security_group_ids": [ "${aws_security_group.sg_playground.id}" ] - } + }, + "skip_rules": null } ], "aws_internet_gateway": [ @@ -46,6 +48,7 @@ "id": "aws_internet_gateway.igw_playground", "name": "igw_playground", "source": "config1.tf", + "plan_root": "./", "line": 14, "type": "aws_internet_gateway", "config": { @@ -53,7 +56,8 @@ "Environment": "${var.environment_tag}" }, "vpc_id": "${aws_vpc.vpc_playground.id}" - } + }, + "skip_rules": null } ], "aws_key_pair": [ @@ -61,12 +65,14 @@ "id": "aws_key_pair.ec2key_playground", "name": "ec2key_playground", "source": "config1.tf", + "plan_root": "./", "line": 72, "type": "aws_key_pair", "config": { "key_name": "testKey", "public_key": "${file(var.public_key_path)}" - } + }, + "skip_rules": null } ], "aws_route_table": [ @@ -74,6 +80,7 @@ "id": "aws_route_table.rtb_public_playground", "name": "rtb_public_playground", "source": "config1.tf", + "plan_root": "./", "line": 30, "type": "aws_route_table", "config": { @@ -87,7 +94,8 @@ "Environment": "${var.environment_tag}" }, "vpc_id": "${aws_vpc.vpc_playground.id}" - } + }, + "skip_rules": null } ], "aws_route_table_association": [ @@ -95,12 +103,14 @@ "id": "aws_route_table_association.rta_subnet_public_playground", "name": "rta_subnet_public_playground", "source": "config1.tf", + "plan_root": "./", "line": 41, "type": "aws_route_table_association", "config": { "route_table_id": "${aws_route_table.rtb_public_playground.id}", "subnet_id": "${aws_subnet.subnet_public_playground.id}" - } + }, + "skip_rules": null } ], "aws_security_group": [ @@ -108,6 +118,7 @@ "id": "aws_security_group.sg_playground", "name": "sg_playground", "source": "config1.tf", + "plan_root": "./", "line": 46, "type": "aws_security_group", "config": { @@ -144,7 +155,8 @@ "Environment": "${var.environment_tag}" }, "vpc_id": "${aws_vpc.vpc_playground.id}" - } + }, + "skip_rules": null } ], "aws_subnet": [ @@ -152,6 +164,7 @@ "id": "aws_subnet.subnet_public_playground", "name": "subnet_public_playground", "source": "config1.tf", + "plan_root": "./", "line": 21, "type": "aws_subnet", "config": { @@ -161,7 +174,8 @@ "Environment": "${var.environment_tag}" }, "vpc_id": "${aws_vpc.vpc_playground.id}" - } + }, + "skip_rules": null } ], "aws_vpc": [ @@ -169,6 +183,7 @@ "id": "aws_vpc.vpc_playground", "name": "vpc_playground", "source": "config1.tf", + "plan_root": "./", "line": 5, "type": "aws_vpc", "config": { @@ -178,7 +193,8 @@ "tags": { "Environment": "${var.environment_tag}" } - } + }, + "skip_rules": null } ] -} +} \ No newline at end of file diff --git a/pkg/iac-providers/terraform/v12/testdata/tfjson/list-vars-test.json b/pkg/iac-providers/terraform/v12/testdata/tfjson/list-vars-test.json index af26a1a08..ab2bf764c 100644 --- a/pkg/iac-providers/terraform/v12/testdata/tfjson/list-vars-test.json +++ b/pkg/iac-providers/terraform/v12/testdata/tfjson/list-vars-test.json @@ -4,6 +4,7 @@ "id": "aws_instance.app", "name": "app", "source": "main.tf", + "plan_root": "./", "line": 5, "type": "aws_instance", "config": { diff --git a/pkg/iac-providers/terraform/v12/testdata/tfjson/moduleconfigs.json b/pkg/iac-providers/terraform/v12/testdata/tfjson/moduleconfigs.json index 24ca4acce..b94df2244 100644 --- a/pkg/iac-providers/terraform/v12/testdata/tfjson/moduleconfigs.json +++ b/pkg/iac-providers/terraform/v12/testdata/tfjson/moduleconfigs.json @@ -4,6 +4,7 @@ "id": "aws_cloudfront_distribution.s3-distribution-TLS-v1", "name": "s3-distribution-TLS-v1", "source": "cloudfront/main.tf", + "plan_root": "./", "line": 6, "type": "aws_cloudfront_distribution", "config": { @@ -124,7 +125,8 @@ "minimum_protocol_version": "TLSv1" } ] - } + }, + "skip_rules": null } ], "aws_cloudtrail": [ @@ -132,6 +134,7 @@ "id": "aws_cloudtrail.missing-multi-region", "name": "missing-multi-region", "source": "cloudtrail/main.tf", + "plan_root": "./", "line": 1, "type": "aws_cloudtrail", "config": { @@ -139,7 +142,8 @@ "name": "tf-trail-foobar", "s3_bucket_name": "some-s3-bucket", "s3_key_prefix": "prefix" - } + }, + "skip_rules": null } ], "aws_ecs_task_definition": [ @@ -147,13 +151,15 @@ "id": "aws_ecs_task_definition.instanceNotInVpc", "name": "instanceNotInVpc", "source": "ecs/main.tf", + "plan_root": "./", "line": 1, "type": "aws_ecs_task_definition", "config": { "container_definitions": "${file(\"ecs/service.json\")}", "family": "service", "network_mode": "bridge" - } + }, + "skip_rules": null } ], "aws_efs_file_system": [ @@ -161,6 +167,7 @@ "id": "aws_efs_file_system.efsNotEncrypted", "name": "efsNotEncrypted", "source": "efs/main.tf", + "plan_root": "./", "line": 1, "type": "aws_efs_file_system", "config": { @@ -168,7 +175,8 @@ "tags": { "Name": "not-encrypted" } - } + }, + "skip_rules": null } ], "aws_elasticache_cluster": [ @@ -176,6 +184,7 @@ "id": "aws_elasticache_cluster.noMemcachedInElastiCache", "name": "noMemcachedInElastiCache", "source": "../relative-moduleconfigs/elasticcache/main.tf", + "plan_root": "./", "line": 1, "type": "aws_elasticache_cluster", "config": { @@ -185,7 +194,8 @@ "num_cache_nodes": 2, "parameter_group_name": "default.memcached1.4", "port": 11211 - } + }, + "skip_rules": null } ], "aws_guardduty_detector": [ @@ -193,11 +203,13 @@ "id": "aws_guardduty_detector.gaurdDutyDisabled", "name": "gaurdDutyDisabled", "source": "guardduty/main.tf", + "plan_root": "./", "line": 1, "type": "aws_guardduty_detector", "config": { "enable": false - } + }, + "skip_rules": null } ], "aws_iam_access_key": [ @@ -205,13 +217,15 @@ "id": "aws_iam_access_key.noAccessKeyForRootAccount", "name": "noAccessKeyForRootAccount", "source": "iam/main.tf", + "plan_root": "./", "line": 1, "type": "aws_iam_access_key", "config": { "pgp_key": "keybase:some_person_that_exists", "status": "Inactive", "user": "root" - } + }, + "skip_rules": null } ], "aws_kinesis_stream": [ @@ -219,6 +233,7 @@ "id": "aws_kinesis_stream.kinesisEncryptedWithKms", "name": "kinesisEncryptedWithKms", "source": "kinesis/main.tf", + "plan_root": "./", "line": 1, "type": "aws_kinesis_stream", "config": { @@ -234,7 +249,8 @@ "tags": { "Environment": "kinesisEncryptedWithKms" } - } + }, + "skip_rules": null } ], "aws_kms_key": [ @@ -242,6 +258,7 @@ "id": "aws_kms_key.kmsKeyDisabled", "name": "kmsKeyDisabled", "source": "cloudfront/sub-cloudfront/main.tf", + "plan_root": "./", "line": 1, "type": "aws_kms_key", "config": { @@ -251,7 +268,8 @@ "Name": "kmsKeyDisabled", "Setup": "self-healing" } - } + }, + "skip_rules": null } ], "aws_load_balancer_policy": [ @@ -259,6 +277,7 @@ "id": "aws_load_balancer_policy.elbWeakCipher", "name": "elbWeakCipher", "source": "elb/main.tf", + "plan_root": "./", "line": 1, "type": "aws_load_balancer_policy", "config": { @@ -271,7 +290,8 @@ ], "policy_name": "wu-tang-ssl", "policy_type_name": "SSLNegotiationPolicyType" - } + }, + "skip_rules": null } ], "aws_s3_bucket": [ @@ -279,6 +299,7 @@ "id": "aws_s3_bucket.noS3BucketSseRules", "name": "noS3BucketSseRules", "source": "s3/main.tf", + "plan_root": "./", "line": 1, "type": "aws_s3_bucket", "config": { @@ -288,7 +309,8 @@ "Environment": "Dev", "Name": "nos3BucketSseRules" } - } + }, + "skip_rules": null } ], "aws_security_group": [ @@ -296,6 +318,7 @@ "id": "aws_security_group.acme_web", "name": "acme_web", "source": "sg/main.tf", + "plan_root": "./", "line": 1, "type": "aws_security_group", "config": { @@ -327,7 +350,8 @@ "Name": "acme_web" }, "vpc_id": "some_dummy_vpc" - } + }, + "skip_rules": null } ], "aws_sqs_queue": [ @@ -335,6 +359,7 @@ "id": "aws_sqs_queue.sqsQueueExposed", "name": "sqsQueueExposed", "source": "sqs/main.tf", + "plan_root": "./", "line": 1, "type": "aws_sqs_queue", "config": { @@ -342,7 +367,8 @@ "kms_master_key_id": "alias/aws/sqs", "name": "terraform-example-queue", "policy": "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [{\n \"Sid\":\"Queue1_AnonymousAccess_AllActions_WhitelistIP\",\n \"Effect\": \"Allow\",\n \"Principal\": \"*\",\n \"Action\": \"sqs:*\",\n \"Resource\": \"arn:aws:sqs:*:111122223333:queue1\"\n }] \n}\n" - } + }, + "skip_rules": null } ] -} +} \ No newline at end of file diff --git a/pkg/iac-providers/terraform/v14/load-dir.go b/pkg/iac-providers/terraform/v14/load-dir.go index e7bf1d929..9dac6d8ef 100644 --- a/pkg/iac-providers/terraform/v14/load-dir.go +++ b/pkg/iac-providers/terraform/v14/load-dir.go @@ -25,6 +25,6 @@ import ( // LoadIacDir starts traversing from the given rootDir and traverses through // all the descendant modules present to create an output list of all the // resources present in rootDir and descendant modules -func (*TfV14) LoadIacDir(absRootDir string) (allResourcesConfig output.AllResourceConfigs, err error) { - return commons.LoadIacDir(absRootDir) +func (*TfV14) LoadIacDir(absRootDir string, nonRecursive bool) (allResourcesConfig output.AllResourceConfigs, err error) { + return commons.NewTerraformDirectoryLoader(absRootDir, nonRecursive).LoadIacDir() } diff --git a/pkg/iac-providers/terraform/v14/load-dir_test.go b/pkg/iac-providers/terraform/v14/load-dir_test.go index 8d123ee72..bd6b23d36 100644 --- a/pkg/iac-providers/terraform/v14/load-dir_test.go +++ b/pkg/iac-providers/terraform/v14/load-dir_test.go @@ -20,8 +20,9 @@ import ( "encoding/json" "fmt" "io/ioutil" + "os" "path/filepath" - "reflect" + "syscall" "testing" "github.com/accurics/terrascan/pkg/iac-providers/output" @@ -31,116 +32,215 @@ import ( ) func TestLoadIacDir(t *testing.T) { + var nilMultiErr *multierror.Error = nil testErrorMessage := fmt.Sprintf(`failed to load terraform config dir '%s'. error from terraform: %s:1,21-2,1: Invalid block definition; A block definition must have block content delimited by "{" and "}", starting on the same line as the block header. %s:1,1-5: Unsupported block type; Blocks of type "some" are not expected here. `, testDataDir, emptyTfFilePath, emptyTfFilePath) + errStringInvalidModuleConfigs := fmt.Sprintf(`failed to build unified config. errors: +: Failed to read module directory; Module directory %s does not exist or cannot be read. +`, filepath.Join(testDataDir, "invalid-moduleconfigs", "cloudfront", "sub-cloudfront")) + + errStringDependsOnDir := fmt.Sprintf(`failed to build unified config. errors: +: Failed to read module directory; Module directory %s does not exist or cannot be read., and 1 other diagnostic(s) +`, filepath.Join(testDataDir, "depends_on", "live", "log")) + testDirPath1 := "not-there" testDirPath2 := filepath.Join(testDataDir, "testfile") invalidDirErrStringTemplate := "directory '%s' has no terraform config files" + pathErr := &os.PathError{Op: "lstat", Path: "not-there", Err: syscall.ENOENT} + if utils.IsWindowsPlatform() { + pathErr = &os.PathError{Op: "CreateFile", Path: "not-there", Err: syscall.ENOENT} + } + table := []struct { - name string - dirPath string - tfv14 TfV14 - want output.AllResourceConfigs - wantErr error + name string + dirPath string + tfv14 TfV14 + want output.AllResourceConfigs + nonRecursive bool + wantErr error }{ { - name: "invalid dirPath", + name: "invalid dirPath", + dirPath: testDirPath1, + tfv14: TfV14{}, + nonRecursive: true, + wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath1)), + }, + { + name: "invalid dirPath recursive", dirPath: testDirPath1, tfv14: TfV14{}, - wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath1)), + wantErr: multierror.Append(pathErr), }, { - name: "empty config", + name: "empty config", + dirPath: testDirPath2, + tfv14: TfV14{}, + nonRecursive: true, + wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath2)), + }, + { + name: "empty config recursive", dirPath: testDirPath2, tfv14: TfV14{}, - wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath2)), + wantErr: nilMultiErr, + }, + { + name: "incorrect module structure", + dirPath: filepath.Join(testDataDir, "invalid-moduleconfigs"), + tfv14: TfV14{}, + nonRecursive: true, + wantErr: multierror.Append(fmt.Errorf("failed to build terraform allResourcesConfig")), }, { - name: "incorrect module structure", + name: "incorrect module structure recursive", dirPath: filepath.Join(testDataDir, "invalid-moduleconfigs"), tfv14: TfV14{}, - wantErr: multierror.Append(fmt.Errorf("failed to build terraform allResourcesConfig")), + // same error is loaded two times because, both root module and a child module will generated same error + wantErr: multierror.Append(fmt.Errorf(errStringInvalidModuleConfigs), fmt.Errorf(errStringInvalidModuleConfigs)), }, { - name: "load invalid config dir", + name: "load invalid config dir", + dirPath: testDataDir, + tfv14: TfV14{}, + nonRecursive: true, + wantErr: multierror.Append(fmt.Errorf(testErrorMessage)), + }, + { + name: "load invalid config dir recursive", dirPath: testDataDir, tfv14: TfV14{}, - wantErr: multierror.Append(fmt.Errorf(testErrorMessage)), + wantErr: multierror.Append(fmt.Errorf(testErrorMessage), + fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "deep-modules", "modules")), + fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "deep-modules", "modules", "m4", "modules")), + fmt.Errorf(errStringDependsOnDir), + fmt.Errorf(errStringInvalidModuleConfigs), + fmt.Errorf(errStringInvalidModuleConfigs), + fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "relative-moduleconfigs")), + fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "tfjson")), + ), }, } for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.tfv14.LoadIacDir(tt.dirPath) - if gotErr.Error() != tt.wantErr.Error() { + _, gotErr := tt.tfv14.LoadIacDir(tt.dirPath, tt.nonRecursive) + me, ok := gotErr.(*multierror.Error) + if !ok { + t.Errorf("expected multierror.Error, got %T", gotErr) + } + if tt.wantErr == nilMultiErr { + if err := me.ErrorOrNil(); err != nil { + t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) + } + } else if me.Error() != tt.wantErr.Error() { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } }) } - var nilMultiErr *multierror.Error = nil tfJSONDir := filepath.Join(testDataDir, "tfjson") + nestedModuleErr1 := fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "deep-modules", "modules")) + nestedModuleErr2 := fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "deep-modules", "modules", "m4", "modules")) table2 := []struct { - name string - tfConfigDir string - tfJSONFile string - tfv14 TfV14 - wantErr error + name string + tfConfigDir string + tfJSONFile string + tfv14 TfV14 + nonRecursive bool + wantErr error }{ { - name: "config1", + name: "config1", + tfConfigDir: filepath.Join(testDataDir, "tfconfigs"), + tfJSONFile: filepath.Join(tfJSONDir, "fullconfig.json"), + tfv14: TfV14{}, + nonRecursive: true, + wantErr: nilMultiErr, + }, + { + name: "config1 recursive", tfConfigDir: filepath.Join(testDataDir, "tfconfigs"), - tfJSONFile: filepath.Join(tfJSONDir, "fullconfig.json"), - tfv14: TfV14{}, - wantErr: nilMultiErr, + // no change in the output expected as the config dir doesn't contain subfolder + tfJSONFile: filepath.Join(tfJSONDir, "fullconfig.json"), + tfv14: TfV14{}, + wantErr: nilMultiErr, }, { - name: "module directory", + name: "module directory", + tfConfigDir: filepath.Join(testDataDir, "moduleconfigs"), + tfJSONFile: filepath.Join(tfJSONDir, "moduleconfigs.json"), + tfv14: TfV14{}, + nonRecursive: true, + wantErr: nilMultiErr, + }, + { + name: "module directory recursive", tfConfigDir: filepath.Join(testDataDir, "moduleconfigs"), - tfJSONFile: filepath.Join(tfJSONDir, "moduleconfigs.json"), - tfv14: TfV14{}, - wantErr: nilMultiErr, + // no change in the output expected as the config dir doesn't contain subfolder + tfJSONFile: filepath.Join(tfJSONDir, "moduleconfigs.json"), + tfv14: TfV14{}, + wantErr: nilMultiErr, }, { - name: "nested module directory", + name: "nested module directory", + tfConfigDir: filepath.Join(testDataDir, "deep-modules"), + tfJSONFile: filepath.Join(tfJSONDir, "deep-modules.json"), + tfv14: TfV14{}, + nonRecursive: true, + wantErr: nilMultiErr, + }, + { + name: "nested module directory recursive", tfConfigDir: filepath.Join(testDataDir, "deep-modules"), - tfJSONFile: filepath.Join(tfJSONDir, "deep-modules.json"), + tfJSONFile: filepath.Join(tfJSONDir, "deep-modules-recursive.json"), tfv14: TfV14{}, - wantErr: nilMultiErr, + wantErr: multierror.Append(nestedModuleErr1, nestedModuleErr2), }, { - name: "complex variables", - tfConfigDir: filepath.Join(testDataDir, "complex-variables"), - tfJSONFile: filepath.Join(tfJSONDir, "complex-variables.json"), - tfv14: TfV14{}, - wantErr: nilMultiErr, + name: "complex variables", + tfConfigDir: filepath.Join(testDataDir, "complex-variables"), + tfJSONFile: filepath.Join(tfJSONDir, "complex-variables.json"), + tfv14: TfV14{}, + nonRecursive: true, + wantErr: nilMultiErr, }, { - name: "recursive loop while resolving variables", - tfConfigDir: filepath.Join(testDataDir, "recursive-loop-variables"), - tfJSONFile: filepath.Join(tfJSONDir, "recursive-loop-variables.json"), - tfv14: TfV14{}, - wantErr: nilMultiErr, + name: "recursive loop while resolving variables", + tfConfigDir: filepath.Join(testDataDir, "recursive-loop-variables"), + tfJSONFile: filepath.Join(tfJSONDir, "recursive-loop-variables.json"), + tfv14: TfV14{}, + nonRecursive: true, + wantErr: nilMultiErr, }, { - name: "recursive loop while resolving locals", - tfConfigDir: filepath.Join(testDataDir, "recursive-loop-locals"), - tfJSONFile: filepath.Join(tfJSONDir, "recursive-loop-locals.json"), - tfv14: TfV14{}, - wantErr: nilMultiErr, + name: "recursive loop while resolving locals", + tfConfigDir: filepath.Join(testDataDir, "recursive-loop-locals"), + tfJSONFile: filepath.Join(tfJSONDir, "recursive-loop-locals.json"), + tfv14: TfV14{}, + nonRecursive: true, + wantErr: nilMultiErr, }, } for _, tt := range table2 { t.Run(tt.name, func(t *testing.T) { - got, gotErr := tt.tfv14.LoadIacDir(tt.tfConfigDir) - if !reflect.DeepEqual(gotErr, tt.wantErr) { + got, gotErr := tt.tfv14.LoadIacDir(tt.tfConfigDir, tt.nonRecursive) + me, ok := gotErr.(*multierror.Error) + if !ok { + t.Errorf("expected multierror.Error, got %T", gotErr) + } + if tt.wantErr == nilMultiErr { + if err := me.ErrorOrNil(); err != nil { + t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) + } + } else if me.Error() != tt.wantErr.Error() { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } diff --git a/pkg/iac-providers/terraform/v14/testdata/tfjson/complex-variables.json b/pkg/iac-providers/terraform/v14/testdata/tfjson/complex-variables.json index a46c5c4d5..3450e6c89 100644 --- a/pkg/iac-providers/terraform/v14/testdata/tfjson/complex-variables.json +++ b/pkg/iac-providers/terraform/v14/testdata/tfjson/complex-variables.json @@ -4,6 +4,7 @@ "id": "complex_var_resource.complex", "name": "complex", "source": "main.tf", + "plan_root": "./", "line": 1, "type": "complex_var_resource", "config": { diff --git a/pkg/iac-providers/terraform/v14/testdata/tfjson/deep-modules-recursive.json b/pkg/iac-providers/terraform/v14/testdata/tfjson/deep-modules-recursive.json new file mode 100644 index 000000000..33a4e710c --- /dev/null +++ b/pkg/iac-providers/terraform/v14/testdata/tfjson/deep-modules-recursive.json @@ -0,0 +1,56 @@ +{ + "aws_s3_bucket": [ + { + "id": "aws_s3_bucket.bucket", + "name": "bucket", + "source": "modules/m1/main.tf", + "plan_root": "./", + "line": 20, + "type": "aws_s3_bucket", + "config": { + "bucket": "${module.m3.fullbucketname}", + "policy": "${module.m2.fullbucketpolicy}" + }, + "skip_rules": null + }, + { + "id": "aws_s3_bucket.bucket", + "name": "bucket", + "source": "modules/m4/main.tf", + "plan_root": "./", + "line": 11, + "type": "aws_s3_bucket", + "config": { + "bucket": "tf-test-project-2", + "policy": "${module.m4a.fullbucketpolicy}" + }, + "skip_rules": null + }, + { + "id": "aws_s3_bucket.bucket4a", + "name": "bucket4a", + "source": "modules/m4/modules/m4a/main.tf", + "plan_root": "./", + "line": 20, + "type": "aws_s3_bucket", + "config": { + "bucket": "${module.m4c.fullbucketname}", + "policy": "${module.m4b.fullbucketpolicy}" + }, + "skip_rules": null + }, + { + "id": "aws_s3_bucket.bucket", + "name": "bucket", + "source": "modules/m4/main.tf", + "plan_root": "modules/m4", + "line": 11, + "type": "aws_s3_bucket", + "config": { + "bucket": "asdfasdf", + "policy": "${module.m4a.fullbucketpolicy}" + }, + "skip_rules": null + } + ] +} \ No newline at end of file diff --git a/pkg/iac-providers/terraform/v14/testdata/tfjson/deep-modules.json b/pkg/iac-providers/terraform/v14/testdata/tfjson/deep-modules.json index a2b624d18..78626e6c5 100644 --- a/pkg/iac-providers/terraform/v14/testdata/tfjson/deep-modules.json +++ b/pkg/iac-providers/terraform/v14/testdata/tfjson/deep-modules.json @@ -1,37 +1,40 @@ { - "aws_s3_bucket": [ - { - "id": "aws_s3_bucket.bucket", - "name": "bucket", - "source": "modules/m1/main.tf", - "line": 20, - "type": "aws_s3_bucket", - "config": { - "bucket": "${module.m3.fullbucketname}", - "policy": "${module.m2.fullbucketpolicy}" + "aws_s3_bucket": [ + { + "id": "aws_s3_bucket.bucket", + "name": "bucket", + "source": "modules/m1/main.tf", + "plan_root": "./", + "line": 20, + "type": "aws_s3_bucket", + "config": { + "bucket": "${module.m3.fullbucketname}", + "policy": "${module.m2.fullbucketpolicy}" + } + }, + { + "id": "aws_s3_bucket.bucket", + "name": "bucket", + "source": "modules/m4/main.tf", + "plan_root": "./", + "line": 11, + "type": "aws_s3_bucket", + "config": { + "bucket": "tf-test-project-2", + "policy": "${module.m4a.fullbucketpolicy}" + } + }, + { + "id": "aws_s3_bucket.bucket4a", + "name": "bucket4a", + "source": "modules/m4/modules/m4a/main.tf", + "plan_root": "./", + "line": 20, + "type": "aws_s3_bucket", + "config": { + "bucket": "${module.m4c.fullbucketname}", + "policy": "${module.m4b.fullbucketpolicy}" + } } - }, - { - "id": "aws_s3_bucket.bucket", - "name": "bucket", - "source": "modules/m4/main.tf", - "line": 11, - "type": "aws_s3_bucket", - "config": { - "bucket": "tf-test-project-2", - "policy": "${module.m4a.fullbucketpolicy}" - } - }, - { - "id": "aws_s3_bucket.bucket4a", - "name": "bucket4a", - "source": "modules/m4/modules/m4a/main.tf", - "line": 20, - "type": "aws_s3_bucket", - "config": { - "bucket": "${module.m4c.fullbucketname}", - "policy": "${module.m4b.fullbucketpolicy}" - } - } - ] -} + ] + } \ No newline at end of file diff --git a/pkg/iac-providers/terraform/v14/testdata/tfjson/fullconfig.json b/pkg/iac-providers/terraform/v14/testdata/tfjson/fullconfig.json index 307ab6619..07d176b78 100644 --- a/pkg/iac-providers/terraform/v14/testdata/tfjson/fullconfig.json +++ b/pkg/iac-providers/terraform/v14/testdata/tfjson/fullconfig.json @@ -4,6 +4,7 @@ "id": "aws_instance.instance_playground", "name": "instance_playground", "source": "config1.tf", + "plan_root": "./", "line": 77, "type": "aws_instance", "config": { @@ -38,7 +39,8 @@ "vpc_security_group_ids": [ "${aws_security_group.sg_playground.id}" ] - } + }, + "skip_rules": null } ], "aws_internet_gateway": [ @@ -46,6 +48,7 @@ "id": "aws_internet_gateway.igw_playground", "name": "igw_playground", "source": "config1.tf", + "plan_root": "./", "line": 14, "type": "aws_internet_gateway", "config": { @@ -53,7 +56,8 @@ "Environment": "${var.environment_tag}" }, "vpc_id": "${aws_vpc.vpc_playground.id}" - } + }, + "skip_rules": null } ], "aws_key_pair": [ @@ -61,12 +65,14 @@ "id": "aws_key_pair.ec2key_playground", "name": "ec2key_playground", "source": "config1.tf", + "plan_root": "./", "line": 72, "type": "aws_key_pair", "config": { "key_name": "testKey", "public_key": "${file(var.public_key_path)}" - } + }, + "skip_rules": null } ], "aws_route_table": [ @@ -74,6 +80,7 @@ "id": "aws_route_table.rtb_public_playground", "name": "rtb_public_playground", "source": "config1.tf", + "plan_root": "./", "line": 30, "type": "aws_route_table", "config": { @@ -87,7 +94,8 @@ "Environment": "${var.environment_tag}" }, "vpc_id": "${aws_vpc.vpc_playground.id}" - } + }, + "skip_rules": null } ], "aws_route_table_association": [ @@ -95,12 +103,14 @@ "id": "aws_route_table_association.rta_subnet_public_playground", "name": "rta_subnet_public_playground", "source": "config1.tf", + "plan_root": "./", "line": 41, "type": "aws_route_table_association", "config": { "route_table_id": "${aws_route_table.rtb_public_playground.id}", "subnet_id": "${aws_subnet.subnet_public_playground.id}" - } + }, + "skip_rules": null } ], "aws_security_group": [ @@ -108,6 +118,7 @@ "id": "aws_security_group.sg_playground", "name": "sg_playground", "source": "config1.tf", + "plan_root": "./", "line": 46, "type": "aws_security_group", "config": { @@ -144,7 +155,8 @@ "Environment": "${var.environment_tag}" }, "vpc_id": "${aws_vpc.vpc_playground.id}" - } + }, + "skip_rules": null } ], "aws_subnet": [ @@ -152,6 +164,7 @@ "id": "aws_subnet.subnet_public_playground", "name": "subnet_public_playground", "source": "config1.tf", + "plan_root": "./", "line": 21, "type": "aws_subnet", "config": { @@ -161,7 +174,8 @@ "Environment": "${var.environment_tag}" }, "vpc_id": "${aws_vpc.vpc_playground.id}" - } + }, + "skip_rules": null } ], "aws_vpc": [ @@ -169,6 +183,7 @@ "id": "aws_vpc.vpc_playground", "name": "vpc_playground", "source": "config1.tf", + "plan_root": "./", "line": 5, "type": "aws_vpc", "config": { @@ -178,7 +193,8 @@ "tags": { "Environment": "${var.environment_tag}" } - } + }, + "skip_rules": null } ] -} +} \ No newline at end of file diff --git a/pkg/iac-providers/terraform/v14/testdata/tfjson/moduleconfigs.json b/pkg/iac-providers/terraform/v14/testdata/tfjson/moduleconfigs.json index 24ca4acce..b94df2244 100644 --- a/pkg/iac-providers/terraform/v14/testdata/tfjson/moduleconfigs.json +++ b/pkg/iac-providers/terraform/v14/testdata/tfjson/moduleconfigs.json @@ -4,6 +4,7 @@ "id": "aws_cloudfront_distribution.s3-distribution-TLS-v1", "name": "s3-distribution-TLS-v1", "source": "cloudfront/main.tf", + "plan_root": "./", "line": 6, "type": "aws_cloudfront_distribution", "config": { @@ -124,7 +125,8 @@ "minimum_protocol_version": "TLSv1" } ] - } + }, + "skip_rules": null } ], "aws_cloudtrail": [ @@ -132,6 +134,7 @@ "id": "aws_cloudtrail.missing-multi-region", "name": "missing-multi-region", "source": "cloudtrail/main.tf", + "plan_root": "./", "line": 1, "type": "aws_cloudtrail", "config": { @@ -139,7 +142,8 @@ "name": "tf-trail-foobar", "s3_bucket_name": "some-s3-bucket", "s3_key_prefix": "prefix" - } + }, + "skip_rules": null } ], "aws_ecs_task_definition": [ @@ -147,13 +151,15 @@ "id": "aws_ecs_task_definition.instanceNotInVpc", "name": "instanceNotInVpc", "source": "ecs/main.tf", + "plan_root": "./", "line": 1, "type": "aws_ecs_task_definition", "config": { "container_definitions": "${file(\"ecs/service.json\")}", "family": "service", "network_mode": "bridge" - } + }, + "skip_rules": null } ], "aws_efs_file_system": [ @@ -161,6 +167,7 @@ "id": "aws_efs_file_system.efsNotEncrypted", "name": "efsNotEncrypted", "source": "efs/main.tf", + "plan_root": "./", "line": 1, "type": "aws_efs_file_system", "config": { @@ -168,7 +175,8 @@ "tags": { "Name": "not-encrypted" } - } + }, + "skip_rules": null } ], "aws_elasticache_cluster": [ @@ -176,6 +184,7 @@ "id": "aws_elasticache_cluster.noMemcachedInElastiCache", "name": "noMemcachedInElastiCache", "source": "../relative-moduleconfigs/elasticcache/main.tf", + "plan_root": "./", "line": 1, "type": "aws_elasticache_cluster", "config": { @@ -185,7 +194,8 @@ "num_cache_nodes": 2, "parameter_group_name": "default.memcached1.4", "port": 11211 - } + }, + "skip_rules": null } ], "aws_guardduty_detector": [ @@ -193,11 +203,13 @@ "id": "aws_guardduty_detector.gaurdDutyDisabled", "name": "gaurdDutyDisabled", "source": "guardduty/main.tf", + "plan_root": "./", "line": 1, "type": "aws_guardduty_detector", "config": { "enable": false - } + }, + "skip_rules": null } ], "aws_iam_access_key": [ @@ -205,13 +217,15 @@ "id": "aws_iam_access_key.noAccessKeyForRootAccount", "name": "noAccessKeyForRootAccount", "source": "iam/main.tf", + "plan_root": "./", "line": 1, "type": "aws_iam_access_key", "config": { "pgp_key": "keybase:some_person_that_exists", "status": "Inactive", "user": "root" - } + }, + "skip_rules": null } ], "aws_kinesis_stream": [ @@ -219,6 +233,7 @@ "id": "aws_kinesis_stream.kinesisEncryptedWithKms", "name": "kinesisEncryptedWithKms", "source": "kinesis/main.tf", + "plan_root": "./", "line": 1, "type": "aws_kinesis_stream", "config": { @@ -234,7 +249,8 @@ "tags": { "Environment": "kinesisEncryptedWithKms" } - } + }, + "skip_rules": null } ], "aws_kms_key": [ @@ -242,6 +258,7 @@ "id": "aws_kms_key.kmsKeyDisabled", "name": "kmsKeyDisabled", "source": "cloudfront/sub-cloudfront/main.tf", + "plan_root": "./", "line": 1, "type": "aws_kms_key", "config": { @@ -251,7 +268,8 @@ "Name": "kmsKeyDisabled", "Setup": "self-healing" } - } + }, + "skip_rules": null } ], "aws_load_balancer_policy": [ @@ -259,6 +277,7 @@ "id": "aws_load_balancer_policy.elbWeakCipher", "name": "elbWeakCipher", "source": "elb/main.tf", + "plan_root": "./", "line": 1, "type": "aws_load_balancer_policy", "config": { @@ -271,7 +290,8 @@ ], "policy_name": "wu-tang-ssl", "policy_type_name": "SSLNegotiationPolicyType" - } + }, + "skip_rules": null } ], "aws_s3_bucket": [ @@ -279,6 +299,7 @@ "id": "aws_s3_bucket.noS3BucketSseRules", "name": "noS3BucketSseRules", "source": "s3/main.tf", + "plan_root": "./", "line": 1, "type": "aws_s3_bucket", "config": { @@ -288,7 +309,8 @@ "Environment": "Dev", "Name": "nos3BucketSseRules" } - } + }, + "skip_rules": null } ], "aws_security_group": [ @@ -296,6 +318,7 @@ "id": "aws_security_group.acme_web", "name": "acme_web", "source": "sg/main.tf", + "plan_root": "./", "line": 1, "type": "aws_security_group", "config": { @@ -327,7 +350,8 @@ "Name": "acme_web" }, "vpc_id": "some_dummy_vpc" - } + }, + "skip_rules": null } ], "aws_sqs_queue": [ @@ -335,6 +359,7 @@ "id": "aws_sqs_queue.sqsQueueExposed", "name": "sqsQueueExposed", "source": "sqs/main.tf", + "plan_root": "./", "line": 1, "type": "aws_sqs_queue", "config": { @@ -342,7 +367,8 @@ "kms_master_key_id": "alias/aws/sqs", "name": "terraform-example-queue", "policy": "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [{\n \"Sid\":\"Queue1_AnonymousAccess_AllActions_WhitelistIP\",\n \"Effect\": \"Allow\",\n \"Principal\": \"*\",\n \"Action\": \"sqs:*\",\n \"Resource\": \"arn:aws:sqs:*:111122223333:queue1\"\n }] \n}\n" - } + }, + "skip_rules": null } ] -} +} \ No newline at end of file diff --git a/pkg/iac-providers/terraform/v14/testdata/tfjson/recursive-loop-locals.json b/pkg/iac-providers/terraform/v14/testdata/tfjson/recursive-loop-locals.json index 3e539d6c3..43ab81362 100644 --- a/pkg/iac-providers/terraform/v14/testdata/tfjson/recursive-loop-locals.json +++ b/pkg/iac-providers/terraform/v14/testdata/tfjson/recursive-loop-locals.json @@ -4,6 +4,7 @@ "id": "aws_iam_user.lb", "name": "lb", "source": "dummy/main.tf", + "plan_root": "./", "line": 13, "type": "aws_iam_user", "config": { diff --git a/pkg/iac-providers/terraform/v14/testdata/tfjson/recursive-loop-variables.json b/pkg/iac-providers/terraform/v14/testdata/tfjson/recursive-loop-variables.json index 0917e30d2..e3a3c6d97 100644 --- a/pkg/iac-providers/terraform/v14/testdata/tfjson/recursive-loop-variables.json +++ b/pkg/iac-providers/terraform/v14/testdata/tfjson/recursive-loop-variables.json @@ -4,6 +4,7 @@ "id": "null_resource.example", "name": "example", "source": "dummy/main.tf", + "plan_root": "./", "line": 5, "type": "null_resource", "config": { diff --git a/pkg/iac-providers/tfplan/v1/load-dir.go b/pkg/iac-providers/tfplan/v1/load-dir.go index 4ae8b1259..2b7e070f2 100644 --- a/pkg/iac-providers/tfplan/v1/load-dir.go +++ b/pkg/iac-providers/tfplan/v1/load-dir.go @@ -28,6 +28,6 @@ var ( // LoadIacDir is not supported for tfplan IacType. Terraform plan should always // be a file and not a directory -func (k *TFPlan) LoadIacDir(absRootDir string) (output.AllResourceConfigs, error) { +func (k *TFPlan) LoadIacDir(absRootDir string, nonRecursive bool) (output.AllResourceConfigs, error) { return output.AllResourceConfigs{}, errIacDirNotSupport } diff --git a/pkg/iac-providers/tfplan/v1/load-dir_test.go b/pkg/iac-providers/tfplan/v1/load-dir_test.go index b3d575c3e..147a24e29 100644 --- a/pkg/iac-providers/tfplan/v1/load-dir_test.go +++ b/pkg/iac-providers/tfplan/v1/load-dir_test.go @@ -29,7 +29,7 @@ func TestLoadIacDir(t *testing.T) { tfplan = TFPlan{} wantErr = errIacDirNotSupport ) - _, err := tfplan.LoadIacDir(dirPath) + _, err := tfplan.LoadIacDir(dirPath, false) if !reflect.DeepEqual(wantErr, err) { t.Errorf("error want: '%v', got: '%v'", wantErr, err) } diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go index e1e0f5445..3658b278e 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -193,10 +193,10 @@ func (w ValidatingWebhook) scanK8sFile(filePath string) (runtime.Output, error) if flag.Lookup("test.v") != nil { executor, err = runtime.NewExecutor("k8s", "v1", []string{"k8s"}, - filePath, "", []string{testPoliciesPath}, []string{}, []string{}, []string{}, "") + filePath, "", []string{testPoliciesPath}, []string{}, []string{}, []string{}, "", false) } else { executor, err = runtime.NewExecutor("k8s", "v1", []string{"k8s"}, - filePath, "", []string{}, []string{}, []string{}, []string{}, "") + filePath, "", []string{}, []string{}, []string{}, []string{}, "", false) } if err != nil { zap.S().Errorf("failed to create runtime executer: '%v'", err) diff --git a/pkg/policy/opa/engine.go b/pkg/policy/opa/engine.go index ff6af74bc..5b7b79727 100644 --- a/pkg/policy/opa/engine.go +++ b/pkg/policy/opa/engine.go @@ -297,6 +297,7 @@ func (e *Engine) reportViolation(regoData *RegoData, resource *output.ResourceCo ResourceType: resource.Type, ResourceData: resource.Config, File: resource.Source, + PlanRoot: resource.PlanRoot, LineNumber: resource.Line, } @@ -389,40 +390,44 @@ func (e *Engine) Evaluate(engineInput policy.EngineInput) (policy.EngineOutput, } // Locate the resource details within the input map - var resource *output.ResourceConfig - resource, err = engineInput.InputData.FindResourceByID(resourceID) + resources, err := engineInput.InputData.FindAllResourcesByID(resourceID) if err != nil { zap.S().Error(err) continue } - // add to skipped violations if rule is skipped for resource - if len(resource.SkipRules) > 0 { - found := false - var skipComment string - for _, rule := range resource.SkipRules { - if strings.EqualFold(k, rule.Rule) { - found = true - skipComment = rule.Comment - break - } - } - if found { - e.reportViolation(e.regoDataMap[k], resource, true, skipComment) - zap.S().Debugf("rule: %s skipped for resource: %s", k, resource.Name) - continue - } - } - - if resource == nil { + if len(resources) == 0 { zap.S().Warn("resource was not found", zap.String("resource id", resourceID)) continue } - zap.S().Debug("violation found for rule with rego", zap.String("rego", string("\n")+string(e.regoDataMap[k].RawRego)+string("\n"))) + for _, resource := range resources { + // add to skipped violations if rule is skipped for resource + if len(resource.SkipRules) > 0 { + found := false + var skipComment string + for _, rule := range resource.SkipRules { + if strings.EqualFold(k, rule.Rule) { + found = true + skipComment = rule.Comment + break + } + } + if found { + // report skipped + e.reportViolation(e.regoDataMap[k], resource, true, skipComment) + zap.S().Debugf("rule: %s skipped for resource: %s", k, resource.Name) + } else { + // Report the violation + e.reportViolation(e.regoDataMap[k], resource, false, "") + } + } else { + // Report the violation + e.reportViolation(e.regoDataMap[k], resource, false, "") + } + } - // Report the violation - e.reportViolation(e.regoDataMap[k], resource, false, "") + zap.S().Debug("violation found for rule with rego", zap.String("rego", string("\n")+string(e.regoDataMap[k].RawRego)+string("\n"))) } } diff --git a/pkg/results/types.go b/pkg/results/types.go index 5f57685e5..b92f8d6db 100644 --- a/pkg/results/types.go +++ b/pkg/results/types.go @@ -35,6 +35,7 @@ type Violation struct { ResourceType string `json:"resource_type" yaml:"resource_type" xml:"resource_type,attr"` ResourceData interface{} `json:"-" yaml:"-" xml:"-"` File string `json:"file,omitempty" yaml:"file,omitempty" xml:"file,attr,omitempty"` + PlanRoot string `json:"plan_root,omitempty" yaml:"plan_root,omitempty" xml:"plan_root,omitempty,attr"` LineNumber int `json:"line,omitempty" yaml:"line,omitempty" xml:"line,attr,omitempty"` } diff --git a/pkg/runtime/executor.go b/pkg/runtime/executor.go index 7e3598182..c450cb206 100644 --- a/pkg/runtime/executor.go +++ b/pkg/runtime/executor.go @@ -44,10 +44,11 @@ type Executor struct { notifiers []notifications.Notifier categories []string severity string + nonRecursive bool } // NewExecutor creates a runtime object -func NewExecutor(iacType, iacVersion string, cloudType []string, filePath, dirPath string, policyPath, scanRules, skipRules, categories []string, severity string) (e *Executor, err error) { +func NewExecutor(iacType, iacVersion string, cloudType []string, filePath, dirPath string, policyPath, scanRules, skipRules, categories []string, severity string, nonRecursive bool) (e *Executor, err error) { e = &Executor{ filePath: filePath, dirPath: dirPath, @@ -56,6 +57,7 @@ func NewExecutor(iacType, iacVersion string, cloudType []string, filePath, dirPa iacType: iacType, iacVersion: iacVersion, iacProviders: make([]iacProvider.IacProvider, 0), + nonRecursive: nonRecursive, } // read config file and update scan and skip rules @@ -112,6 +114,7 @@ func (e *Executor) Init() error { zap.S().Errorf("failed to create a new IacProvider for iacType '%s'. error: '%s'", e.iacType, err) return err } + e.iacProviders = append(e.iacProviders, iacP) } } else { @@ -120,6 +123,7 @@ func (e *Executor) Init() error { zap.S().Errorf("failed to create a new IacProvider for iacType '%s'. error: '%s'", e.iacType, err) return err } + e.iacProviders = append(e.iacProviders, iacP) } @@ -177,7 +181,7 @@ func (e *Executor) Execute() (results Output, err error) { // for the iac providers that don't implement sub folder scanning // return the error to the caller - if !implementsSubFolderScan(e.iacType) { + if !implementsSubFolderScan(e.iacType, e.nonRecursive) { if err := merr.ErrorOrNil(); err != nil { return results, err } @@ -226,7 +230,7 @@ func (e *Executor) getResourceConfigs() (output.AllResourceConfigs, *multierror. // create results output from Iac provider[s] for _, iacP := range e.iacProviders { go func(ip iacProvider.IacProvider) { - rc, err := ip.LoadIacDir(e.dirPath) + rc, err := ip.LoadIacDir(e.dirPath, e.nonRecursive) scanRespChan <- dirScanResp{err, rc} }(iacP) } @@ -278,11 +282,15 @@ func (e *Executor) findViolations(results *Output) error { } // implementsSubFolderScan checks if given iac type supports sub folder scanning -func implementsSubFolderScan(iacType string) bool { +func implementsSubFolderScan(iacType string, nonRecursive bool) bool { // iac providers that support sub folder scanning // this needs be updated when other iac providers implement // sub folder scanning - iacWithSubFolderScan := []string{"all", "k8s", "helm"} + if nonRecursive && iacType == "terraform" { + return false + } + + iacWithSubFolderScan := []string{"all", "k8s", "helm", "terraform"} for _, v := range iacWithSubFolderScan { if v == iacType { return true diff --git a/pkg/runtime/executor_test.go b/pkg/runtime/executor_test.go index 2b20dd843..1dec71ccd 100644 --- a/pkg/runtime/executor_test.go +++ b/pkg/runtime/executor_test.go @@ -54,7 +54,7 @@ type MockIacProvider struct { err error } -func (m MockIacProvider) LoadIacDir(dir string) (output.AllResourceConfigs, error) { +func (m MockIacProvider) LoadIacDir(dir string, nonRecursive bool) (output.AllResourceConfigs, error) { return m.output, m.err } @@ -540,7 +540,7 @@ func TestNewExecutor(t *testing.T) { t.Run(tt.name, func(t *testing.T) { config.LoadGlobalConfig(tt.configfile) - gotExecutor, gotErr := NewExecutor(tt.flags.iacType, tt.flags.iacVersion, tt.flags.cloudType, tt.flags.filePath, tt.flags.dirPath, tt.flags.policyPath, tt.flags.scanRules, tt.flags.skipRules, tt.flags.categories, tt.flags.severity) + gotExecutor, gotErr := NewExecutor(tt.flags.iacType, tt.flags.iacVersion, tt.flags.cloudType, tt.flags.filePath, tt.flags.dirPath, tt.flags.policyPath, tt.flags.scanRules, tt.flags.skipRules, tt.flags.categories, tt.flags.severity, false) if !reflect.DeepEqual(tt.wantErr, gotErr) { t.Errorf("Mismatch in error => got: '%v', want: '%v'", gotErr, tt.wantErr) diff --git a/pkg/termcolor/colorpatterns.go b/pkg/termcolor/colorpatterns.go index eb7f4616e..23094fa0b 100644 --- a/pkg/termcolor/colorpatterns.go +++ b/pkg/termcolor/colorpatterns.go @@ -59,6 +59,7 @@ var defaultColorPatterns = map[FieldSpec]FieldStyle{ {`[rR]esource[_\s][nN]ame`, defaultValuePattern}: {"", "Fg#0ff|Bold"}, {`[rR]esource[_\s][tT]ype`, defaultValuePattern}: {"", "Fg#0cc"}, {"[fF]ile", defaultValuePattern}: {"", "Fg#00768B|Bold"}, + {`[rR]oot[_\s][pP]ath`, defaultValuePattern}: {"", "Fg#00768B|Bold"}, {"[lL]ow", `\d+`}: {"Fg#cc0", "Fg#cc0"}, {"[mM]edium", `\d+`}: {"Fg#c84", "Fg#c84"}, {"[hH]igh", `\d+`}: {"Fg#f00", "Fg#f00"}, diff --git a/pkg/utils/dir.go b/pkg/utils/dir.go index 62c17ed5e..63fdbb0e5 100644 --- a/pkg/utils/dir.go +++ b/pkg/utils/dir.go @@ -1,6 +1,9 @@ package utils import ( + "os" + "path/filepath" + "github.com/mitchellh/go-homedir" "go.uber.org/zap" ) @@ -17,3 +20,8 @@ func GetHomeDir() (terrascanDir string) { return } + +// GenerateTempDir generates a temporary directory +func GenerateTempDir() string { + return filepath.Join(os.TempDir(), GenRandomString(6)) +} diff --git a/pkg/utils/path.go b/pkg/utils/path.go index 9f37d54da..52b7ce2b5 100644 --- a/pkg/utils/path.go +++ b/pkg/utils/path.go @@ -126,3 +126,30 @@ func FindFilesBySuffixInDir(basePath string, suffixes []string) ([]*string, erro func AddFileExtension(file, ext string) string { return fmt.Sprintf("%v.%v", file, ext) } + +// FilterHiddenDirectories filters hidden directories from a list of directories +// TODO: filtering hidden directories on windows +func FilterHiddenDirectories(dirList []string, rootDir string) []string { + filteredDirs := make([]string, 0) + for _, d := range dirList { + // we need to find a hidden folder in a specific root directory + dirName := strings.Replace(d, rootDir, "", 1) + + // get all directory names in a path + dirNames := strings.Split(dirName, string(os.PathSeparator)) + + hasHiddenFolder := false + for _, n := range dirNames { + // on unix based systems, hidden folder names start with a '.' character + if strings.HasPrefix(n, ".") { + hasHiddenFolder = true + break + } + } + + if !hasHiddenFolder { + filteredDirs = append(filteredDirs, d) + } + } + return filteredDirs +} diff --git a/pkg/writer/human_readable.go b/pkg/writer/human_readable.go index 6e35d7715..7603d09fc 100644 --- a/pkg/writer/human_readable.go +++ b/pkg/writer/human_readable.go @@ -94,15 +94,22 @@ func HumanReadbleWriter(data interface{}, writer io.Writer) error { } func defaultViolations(v results.Violation, isSkipped bool) string { - out := fmt.Sprintf("%-15v:\t%s\n\t%-15v:\t%s\n\t%-15v:\t%d\n\t%-15v:\t%s\n\t", + part := fmt.Sprintf("%-15v:\t%s\n\t%-15v:\t%s\n\t", "Description", v.Description, - "File", v.File, + "File", v.File) + if v.PlanRoot != "" { + planRoot := fmt.Sprintf("%-15v:\t%s\n\t", "Plan Root", v.PlanRoot) + part = part + planRoot + } + out := fmt.Sprintf("%-15v:\t%d\n\t%-15v:\t%s\n\t", "Line", v.LineNumber, "Severity", v.Severity) if isSkipped { skipComment := fmt.Sprintf("%-15v:\t%s\n\t", "Skip Comment", v.Comment) out = out + skipComment } + out = part + out + return out } diff --git a/test/e2e/help/golden/help_scan.txt b/test/e2e/help/golden/help_scan.txt index d27b7255f..1575302a7 100644 --- a/test/e2e/help/golden/help_scan.txt +++ b/test/e2e/help/golden/help_scan.txt @@ -13,6 +13,7 @@ Flags: -f, --iac-file string path to a single IaC file -i, --iac-type string iac type (helm, k8s, kustomize, terraform, tfplan) --iac-version string iac version (helm: v3, k8s: v1, kustomize: v3, terraform: v12, v13, v14, tfplan: v1) + --non-recursive do not scan directories and modules recursively -p, --policy-path stringArray policy path directory -t, --policy-type strings policy type (all, aws, azure, gcp, github, k8s) (default [all]) -r, --remote-type string type of remote backend (git, s3, gcs, http, terraform-registry) diff --git a/test/e2e/scan/golden/resource_skipping/terraform_file_resource_skipping.txt b/test/e2e/scan/golden/resource_skipping/terraform_file_resource_skipping.txt index 8bfabd69b..8da85deb7 100644 --- a/test/e2e/scan/golden/resource_skipping/terraform_file_resource_skipping.txt +++ b/test/e2e/scan/golden/resource_skipping/terraform_file_resource_skipping.txt @@ -10,6 +10,7 @@ "resource_name": "PtShGgAdi6", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 85 }, { @@ -21,6 +22,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -32,6 +34,7 @@ "resource_name": "PtShGgAdi1", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 10 }, { @@ -43,6 +46,7 @@ "resource_name": "PtShGgAdi2", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 25 }, { @@ -54,6 +58,7 @@ "resource_name": "PtShGgAdi5", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 71 }, { @@ -65,6 +70,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -76,6 +82,7 @@ "resource_name": "PtShGgAdi6", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 85 }, { @@ -87,6 +94,7 @@ "resource_name": "PtShGgAdi2", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 25 }, { @@ -98,6 +106,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -109,6 +118,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -120,6 +130,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 } ], @@ -134,6 +145,7 @@ "resource_name": "PtShGgAdi4", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 55 }, { @@ -146,6 +158,7 @@ "resource_name": "PtShGgAdi4", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 55 }, { @@ -158,6 +171,7 @@ "resource_name": "PtShGgAdi5", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 71 } ], diff --git a/test/e2e/scan/golden/rules_filtering/scan_and_skip_rules.txt b/test/e2e/scan/golden/rules_filtering/scan_and_skip_rules.txt index b2ade5dbb..a2cf8c564 100644 --- a/test/e2e/scan/golden/rules_filtering/scan_and_skip_rules.txt +++ b/test/e2e/scan/golden/rules_filtering/scan_and_skip_rules.txt @@ -10,6 +10,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -21,6 +22,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 } ], diff --git a/test/e2e/scan/golden/rules_filtering/scan_multiple_rules.txt b/test/e2e/scan/golden/rules_filtering/scan_multiple_rules.txt index 369bee55a..b83dc836b 100644 --- a/test/e2e/scan/golden/rules_filtering/scan_multiple_rules.txt +++ b/test/e2e/scan/golden/rules_filtering/scan_multiple_rules.txt @@ -10,6 +10,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -21,6 +22,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -32,6 +34,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -43,6 +46,7 @@ "resource_name": "PtShGgAdi2", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 25 }, { @@ -54,6 +58,7 @@ "resource_name": "PtShGgAdi4", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 55 }, { @@ -65,6 +70,7 @@ "resource_name": "PtShGgAdi5", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 69 }, { @@ -76,6 +82,7 @@ "resource_name": "PtShGgAdi6", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 82 }, { @@ -87,6 +94,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 } ], diff --git a/test/e2e/scan/golden/rules_filtering/scan_single_rule.txt b/test/e2e/scan/golden/rules_filtering/scan_single_rule.txt index c038cb82f..bc9714ea9 100644 --- a/test/e2e/scan/golden/rules_filtering/scan_single_rule.txt +++ b/test/e2e/scan/golden/rules_filtering/scan_single_rule.txt @@ -10,6 +10,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 } ], diff --git a/test/e2e/scan/golden/rules_filtering/skip_multiple_rules.txt b/test/e2e/scan/golden/rules_filtering/skip_multiple_rules.txt index 2ef253697..742441bb1 100644 --- a/test/e2e/scan/golden/rules_filtering/skip_multiple_rules.txt +++ b/test/e2e/scan/golden/rules_filtering/skip_multiple_rules.txt @@ -10,6 +10,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 } ], diff --git a/test/e2e/scan/golden/rules_filtering/skip_single_rule.txt b/test/e2e/scan/golden/rules_filtering/skip_single_rule.txt index cc9e8bc68..b053f71e4 100644 --- a/test/e2e/scan/golden/rules_filtering/skip_single_rule.txt +++ b/test/e2e/scan/golden/rules_filtering/skip_single_rule.txt @@ -10,6 +10,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -21,6 +22,7 @@ "resource_name": "PtShGgAdi6", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 82 }, { @@ -32,6 +34,7 @@ "resource_name": "PtShGgAdi2", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 25 }, { @@ -43,6 +46,7 @@ "resource_name": "PtShGgAdi5", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 69 }, { @@ -54,6 +58,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -65,6 +70,7 @@ "resource_name": "PtShGgAdi4", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 55 }, { @@ -76,6 +82,7 @@ "resource_name": "PtShGgAdi1", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 10 }, { @@ -87,6 +94,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 } ], diff --git a/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_human.txt b/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_human.txt index 0f7ade119..bfe0192da 100644 --- a/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_human.txt +++ b/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_human.txt @@ -2,6 +2,7 @@ Violation Details - Description : Enable AWS AMI Encryption File : main.tf + Plan Root : ./ Line : 5 Severity : MEDIUM diff --git a/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_human_verbose.txt b/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_human_verbose.txt index b518165c2..c1313c44a 100644 --- a/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_human_verbose.txt +++ b/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_human_verbose.txt @@ -2,6 +2,7 @@ Violation Details - Description : Enable AWS AMI Encryption File : main.tf + Plan Root : ./ Line : 5 Severity : MEDIUM Rule Name : amiNotEncrypted diff --git a/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_json.txt b/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_json.txt index edbe60c88..529907af8 100644 --- a/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_json.txt +++ b/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_json.txt @@ -10,6 +10,7 @@ "resource_name": "awsAmiEncrypted", "resource_type": "aws_ami", "file": "main.tf", + "plan_root": "./", "line": 5 } ], diff --git a/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_json_all.txt b/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_json_all.txt index c33f70f0e..9713e8c4b 100644 --- a/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_json_all.txt +++ b/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_json_all.txt @@ -22,6 +22,7 @@ "resource_name": "awsAmiEncrypted", "resource_type": "aws_ami", "file": "main.tf", + "plan_root": "./", "line": 5 } ], diff --git a/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_json_recursive.txt b/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_json_recursive.txt new file mode 100644 index 000000000..54b01bd40 --- /dev/null +++ b/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_json_recursive.txt @@ -0,0 +1,41 @@ +{ + "results": { + "scan_errors": [ + { + "iac_type": "terraform", + "directory": "/Users/pankajpatil/go/src/github.com/patilpankaj212/terrascan/test/e2e/test_data/iac/terraform_recursive", + "errMsg": "directory '/Users/pankajpatil/go/src/github.com/patilpankaj212/terrascan/test/e2e/test_data/iac/terraform_recursive' has no terraform config files" + }, + { + "iac_type": "terraform", + "directory": "/Users/pankajpatil/go/src/github.com/patilpankaj212/terrascan/test/e2e/test_data/iac/terraform_recursive/subFolder1", + "errMsg": "directory '/Users/pankajpatil/go/src/github.com/patilpankaj212/terrascan/test/e2e/test_data/iac/terraform_recursive/subFolder1' has no terraform config files" + } + ], + "violations": [ + { + "rule_name": "amiNotEncrypted", + "description": "Enable AWS AMI Encryption", + "rule_id": "AWS.EC2.Encryption\u0026KeyManagement.Medium.0688", + "severity": "MEDIUM", + "category": "Encryption \u0026 KeyManagement", + "resource_name": "awsAmiEncrypted", + "resource_type": "aws_ami", + "file": "subFolder1/subFolder2/main.tf", + "plan_root": "subFolder1/subFolder2", + "line": 5 + } + ], + "skipped_violations": null, + "scan_summary": { + "file/folder": "/Users/pankajpatil/go/src/github.com/patilpankaj212/terrascan/test/e2e/test_data/iac/terraform_recursive", + "iac_type": "terraform", + "scanned_at": "2021-04-18 12:45:51.597994 +0000 UTC", + "policies_validated": 7, + "violated_policies": 1, + "low": 0, + "medium": 1, + "high": 0 + } + } +} \ No newline at end of file diff --git a/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_xml.txt b/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_xml.txt index e1d945cd2..ee7626527 100644 --- a/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_xml.txt +++ b/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_xml.txt @@ -2,7 +2,7 @@ - + diff --git a/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_yaml.txt b/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_yaml.txt index 4f5cf721a..afaa372d3 100644 --- a/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_yaml.txt +++ b/test/e2e/scan/golden/terraform_scans/aws/aws_ami_violations/aws_ami_violation_yaml.txt @@ -8,6 +8,7 @@ results: resource_name: awsAmiEncrypted resource_type: aws_ami file: main.tf + plan_root: ./ line: 5 skipped_violations: [] scan_summary: diff --git a/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_json.txt b/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_json.txt index c89f5fe6b..2c6534c0b 100644 --- a/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_json.txt +++ b/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_json.txt @@ -10,6 +10,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -21,6 +22,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -32,6 +34,7 @@ "resource_name": "PtShGgAdi6", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 82 }, { @@ -43,6 +46,7 @@ "resource_name": "PtShGgAdi4", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 55 }, { @@ -54,6 +58,7 @@ "resource_name": "PtShGgAdi5", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 69 }, { @@ -65,6 +70,7 @@ "resource_name": "PtShGgAdi1", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 10 }, { @@ -76,6 +82,7 @@ "resource_name": "PtShGgAdi2", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 25 }, { @@ -87,6 +94,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -98,6 +106,7 @@ "resource_name": "PtShGgAdi4", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 55 }, { @@ -109,6 +118,7 @@ "resource_name": "PtShGgAdi5", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 69 }, { @@ -120,6 +130,7 @@ "resource_name": "PtShGgAdi6", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 82 }, { @@ -131,6 +142,7 @@ "resource_name": "PtShGgAdi2", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 25 }, { @@ -142,6 +154,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -153,6 +166,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 } ], diff --git a/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_json_show_passed.txt b/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_json_show_passed.txt index 37026d7cc..92ade3e97 100644 --- a/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_json_show_passed.txt +++ b/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_json_show_passed.txt @@ -33,6 +33,7 @@ "resource_name": "PtShGgAdi6", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 82 }, { @@ -44,6 +45,7 @@ "resource_name": "PtShGgAdi2", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 25 }, { @@ -55,6 +57,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -66,6 +69,7 @@ "resource_name": "PtShGgAdi1", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 10 }, { @@ -77,6 +81,7 @@ "resource_name": "PtShGgAdi4", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 55 }, { @@ -88,6 +93,7 @@ "resource_name": "PtShGgAdi5", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 69 }, { @@ -99,6 +105,7 @@ "resource_name": "PtShGgAdi2", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 25 }, { @@ -110,6 +117,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -121,6 +129,7 @@ "resource_name": "PtShGgAdi6", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 82 }, { @@ -132,6 +141,7 @@ "resource_name": "PtShGgAdi4", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 55 }, { @@ -143,6 +153,7 @@ "resource_name": "PtShGgAdi5", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 69 }, { @@ -154,6 +165,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -165,6 +177,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 }, { @@ -176,6 +189,7 @@ "resource_name": "PtShGgAdi3", "resource_type": "aws_db_instance", "file": "main.tf", + "root_path": "./", "line": 39 } ], diff --git a/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_xml.txt b/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_xml.txt index 159503f5f..244324cde 100644 --- a/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_xml.txt +++ b/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_xml.txt @@ -2,20 +2,20 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_yaml.txt b/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_yaml.txt index c2179a6f7..3ec76b582 100644 --- a/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_yaml.txt +++ b/test/e2e/scan/golden/terraform_scans/aws/aws_db_instance_violations/aws_db_instance_yaml.txt @@ -8,6 +8,7 @@ results: resource_name: PtShGgAdi3 resource_type: aws_db_instance file: main.tf + root_path: ./ line: 39 - rule_name: rdsAutoMinorVersionUpgradeEnabled description: RDS Instance Auto Minor Version Upgrade flag disabled @@ -17,6 +18,7 @@ results: resource_name: PtShGgAdi3 resource_type: aws_db_instance file: main.tf + root_path: ./ line: 39 - rule_name: rdsHasStorageEncrypted description: Ensure that your RDS database instances encrypt the underlying storage. Encrypted RDS instances use the industry standard AES-256 encryption algorithm to encrypt data on the server that hosts RDS DB instances. After data is encrypted, RDS handles authentication of access and descryption of data transparently with minimal impact on performance. @@ -26,6 +28,7 @@ results: resource_name: PtShGgAdi6 resource_type: aws_db_instance file: main.tf + root_path: ./ line: 82 - rule_name: rdsHasStorageEncrypted description: Ensure that your RDS database instances encrypt the underlying storage. Encrypted RDS instances use the industry standard AES-256 encryption algorithm to encrypt data on the server that hosts RDS DB instances. After data is encrypted, RDS handles authentication of access and descryption of data transparently with minimal impact on performance. @@ -35,6 +38,7 @@ results: resource_name: PtShGgAdi4 resource_type: aws_db_instance file: main.tf + root_path: ./ line: 55 - rule_name: rdsHasStorageEncrypted description: Ensure that your RDS database instances encrypt the underlying storage. Encrypted RDS instances use the industry standard AES-256 encryption algorithm to encrypt data on the server that hosts RDS DB instances. After data is encrypted, RDS handles authentication of access and descryption of data transparently with minimal impact on performance. @@ -44,6 +48,7 @@ results: resource_name: PtShGgAdi5 resource_type: aws_db_instance file: main.tf + root_path: ./ line: 69 - rule_name: rdsHasStorageEncrypted description: Ensure that your RDS database instances encrypt the underlying storage. Encrypted RDS instances use the industry standard AES-256 encryption algorithm to encrypt data on the server that hosts RDS DB instances. After data is encrypted, RDS handles authentication of access and descryption of data transparently with minimal impact on performance. @@ -53,6 +58,7 @@ results: resource_name: PtShGgAdi1 resource_type: aws_db_instance file: main.tf + root_path: ./ line: 10 - rule_name: rdsHasStorageEncrypted description: Ensure that your RDS database instances encrypt the underlying storage. Encrypted RDS instances use the industry standard AES-256 encryption algorithm to encrypt data on the server that hosts RDS DB instances. After data is encrypted, RDS handles authentication of access and descryption of data transparently with minimal impact on performance. @@ -62,6 +68,7 @@ results: resource_name: PtShGgAdi2 resource_type: aws_db_instance file: main.tf + root_path: ./ line: 25 - rule_name: rdsHasStorageEncrypted description: Ensure that your RDS database instances encrypt the underlying storage. Encrypted RDS instances use the industry standard AES-256 encryption algorithm to encrypt data on the server that hosts RDS DB instances. After data is encrypted, RDS handles authentication of access and descryption of data transparently with minimal impact on performance. @@ -71,6 +78,7 @@ results: resource_name: PtShGgAdi3 resource_type: aws_db_instance file: main.tf + root_path: ./ line: 39 - rule_name: rdsIamAuthEnabled description: Ensure that your RDS database has IAM Authentication enabled. @@ -80,6 +88,7 @@ results: resource_name: PtShGgAdi6 resource_type: aws_db_instance file: main.tf + root_path: ./ line: 82 - rule_name: rdsIamAuthEnabled description: Ensure that your RDS database has IAM Authentication enabled. @@ -89,6 +98,7 @@ results: resource_name: PtShGgAdi4 resource_type: aws_db_instance file: main.tf + root_path: ./ line: 55 - rule_name: rdsIamAuthEnabled description: Ensure that your RDS database has IAM Authentication enabled. @@ -98,6 +108,7 @@ results: resource_name: PtShGgAdi5 resource_type: aws_db_instance file: main.tf + root_path: ./ line: 69 - rule_name: rdsIamAuthEnabled description: Ensure that your RDS database has IAM Authentication enabled. @@ -107,6 +118,7 @@ results: resource_name: PtShGgAdi2 resource_type: aws_db_instance file: main.tf + root_path: ./ line: 25 - rule_name: rdsIamAuthEnabled description: Ensure that your RDS database has IAM Authentication enabled. @@ -116,6 +128,7 @@ results: resource_name: PtShGgAdi3 resource_type: aws_db_instance file: main.tf + root_path: ./ line: 39 - rule_name: rdsIamAuthEnabled description: Ensure that your RDS database has IAM Authentication enabled. @@ -125,6 +138,7 @@ results: resource_name: PtShGgAdi3 resource_type: aws_db_instance file: main.tf + root_path: ./ line: 39 skipped_violations: [] scan_summary: diff --git a/test/e2e/scan/golden/terraform_scans/scanned_with_only_aws_policies.txt b/test/e2e/scan/golden/terraform_scans/scanned_with_only_aws_policies.txt index 2b4f999ad..3b0ef3cc1 100644 --- a/test/e2e/scan/golden/terraform_scans/scanned_with_only_aws_policies.txt +++ b/test/e2e/scan/golden/terraform_scans/scanned_with_only_aws_policies.txt @@ -10,6 +10,7 @@ "resource_name": "awsAmiEncrypted", "resource_type": "aws_ami", "file": "main.tf", + "plan_root": "./", "line": 5 } ], diff --git a/test/e2e/scan/scan_test.go b/test/e2e/scan/scan_test.go index 4f1f5ca5c..5d59c7aa8 100644 --- a/test/e2e/scan/scan_test.go +++ b/test/e2e/scan/scan_test.go @@ -77,7 +77,7 @@ var _ = Describe("Scan", func() { }) }) - Describe("scan command is run without any flags", func() { + Describe("scan command is run", func() { Context("when no iac type is provided, terrascan scans with all iac providers", func() { Context("no tf files are present in the working directory", func() { It("scan the directory and display results", func() { @@ -85,21 +85,25 @@ var _ = Describe("Scan", func() { session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...) helper.ValidateExitCode(session, scanUtils.ScanTimeout, helper.ExitCodeZero) }) - Context("iac loading errors would be displayed in the output", func() { + Context("iac loading errors would be displayed in the output, output type is json", func() { It("scan the directory and display results", func() { scanArgs := []string{scanUtils.ScanCommand, "-o", "json", "-p", policyRootRelPath} // these errors would come from terraform, helm, and kustomize iac providers - errString1 := "has no terraform config files" - errString2 := "kustomization.y(a)ml file not found in the directory" - errString3 := "no helm charts found in directory" + errString1 := "kustomization.y(a)ml file not found in the directory" + errString2 := "no helm charts found in directory" session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...) helper.ContainsDirScanErrorSubString(session, errString1) helper.ContainsDirScanErrorSubString(session, errString2) - helper.ContainsDirScanErrorSubString(session, errString3) + }) + }) + When("iac type is terraform and --non-recursive flag is used", func() { + It("should error out if no terraform files are present in working directory", func() { + scanArgs := []string{scanUtils.ScanCommand, "-i", "terraform", "--non-recursive"} + session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...) + Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeOne)) }) }) }) - Context("tf files are present in the working directory", func() { It("should scan the directory, return results and exit with status code 3", func() { workDir, err := filepath.Abs(filepath.Join(awsIacRelPath, "aws_ami_violation")) diff --git a/test/e2e/scan/scan_tf_files_test.go b/test/e2e/scan/scan_tf_files_test.go index 514faa144..d2308b9a7 100644 --- a/test/e2e/scan/scan_tf_files_test.go +++ b/test/e2e/scan/scan_tf_files_test.go @@ -117,10 +117,12 @@ var _ = Describe("Scan is run for terraform files", func() { }) }) - When("output type is json and all iac scan is used", func() { - It("should display violations in json format, and should have iac type as 'all'", func() { - scanArgs := []string{"-p", policyDir, "-d", iacDir, "-o", "json"} - scanUtils.RunScanAndAssertGoldenOutputRegex(terrascanBinaryPath, filepath.Join(tfAwsAmiGoldenRelPath, "aws_ami_violation_json_all.txt"), helper.ExitCodeThree, false, true, outWriter, errWriter, scanArgs...) + When("output type is json and no iac type is specified", func() { + Context("when iac type is not specified and a directory is specified, it will be scanned will all iac providers", func() { + It("should display violations in json format, and should have iac type as 'all'", func() { + scanArgs := []string{"-p", policyDir, "-d", iacDir, "-o", "json"} + scanUtils.RunScanAndAssertGoldenOutputRegex(terrascanBinaryPath, filepath.Join(tfAwsAmiGoldenRelPath, "aws_ami_violation_json_all.txt"), helper.ExitCodeThree, false, true, outWriter, errWriter, scanArgs...) + }) }) }) @@ -174,33 +176,43 @@ var _ = Describe("Scan is run for terraform files", func() { policyDir, err = filepath.Abs(policyRootRelPath) }) - When("when output type is json", func() { + When("output type is json", func() { It("should display violations in json format", func() { scanArgs := []string{"-p", policyDir, "-i", "terraform", "-d", iacDir, "-o", "json"} scanUtils.RunScanAndAssertJSONOutput(terrascanBinaryPath, filepath.Join(tfAwsDBInstanceGoldenRelPath, "aws_db_instance_json.txt"), helper.ExitCodeThree, false, true, outWriter, errWriter, scanArgs...) }) }) - When("when output type is yaml", func() { + When("output type is yaml", func() { It("should display violations in yaml format", func() { scanArgs := []string{"-p", policyDir, "-i", "terraform", "-d", iacDir, "-o", "yaml"} scanUtils.RunScanAndAssertYAMLOutput(terrascanBinaryPath, filepath.Join(tfAwsDBInstanceGoldenRelPath, "aws_db_instance_yaml.txt"), helper.ExitCodeThree, false, true, outWriter, errWriter, scanArgs...) }) }) - When("when output type is xml", func() { + When("output type is xml", func() { It("should display violations in xml format", func() { scanArgs := []string{"-p", policyDir, "-i", "terraform", "-d", iacDir, "-o", "xml"} scanUtils.RunScanAndAssertXMLOutput(terrascanBinaryPath, filepath.Join(tfAwsDBInstanceGoldenRelPath, "aws_db_instance_xml.txt"), helper.ExitCodeThree, false, true, outWriter, errWriter, scanArgs...) }) }) - When("when --show-passed option is used", func() { + When("--show-passed option is used", func() { It("should display passed rules in the output", func() { scanArgs := []string{"-p", policyDir, "-i", "terraform", "-d", iacDir, "-o", "json", "--show-passed"} scanUtils.RunScanAndAssertJSONOutput(terrascanBinaryPath, filepath.Join(tfAwsDBInstanceGoldenRelPath, "aws_db_instance_json_show_passed.txt"), helper.ExitCodeThree, false, true, outWriter, errWriter, scanArgs...) }) }) }) + + Context("when --non-recursive flag is not used, all sub folders will be scanned in the specified directory", func() { + When("output type is json", func() { + It("should display violations in json format", func() { + iacDir := filepath.Join(iacRootRelPath, "terraform_recursive") + scanArgs := []string{"-i", "terraform", "-p", policyDir, "-d", iacDir, "-o", "json"} + scanUtils.RunScanAndAssertGoldenOutputRegex(terrascanBinaryPath, filepath.Join(tfAwsAmiGoldenRelPath, "aws_ami_violation_json_recursive.txt"), helper.ExitCodeThree, false, true, outWriter, errWriter, scanArgs...) + }) + }) + }) }) }) diff --git a/test/e2e/scan/scan_utils.go b/test/e2e/scan/scan_utils.go index 44262b078..1fa41f711 100644 --- a/test/e2e/scan/scan_utils.go +++ b/test/e2e/scan/scan_utils.go @@ -33,7 +33,7 @@ const ( ScanTimeout int = 3 // RemoteScanTimeout is default scan command remote execution timeout - RemoteScanTimeout int = 20 + RemoteScanTimeout int = 30 ) // RunScanAndAssertGoldenOutputRegex runs the scan command with supplied paramters and compares actual and golden output diff --git a/test/e2e/server/server_remote_scan_test.go b/test/e2e/server/server_remote_scan_test.go index 240ecc22a..6c4eac844 100644 --- a/test/e2e/server/server_remote_scan_test.go +++ b/test/e2e/server/server_remote_scan_test.go @@ -143,6 +143,32 @@ var _ = Describe("Server Remote Scan", func() { }) }) + When("non_recursive attribute is present in body", func() { + Context("remote url contains terraform files", func() { + It("should receive 200 OK response", func() { + + bodyAttrs := make(map[string]interface{}) + bodyAttrs["remote_type"] = "git" + bodyAttrs["remote_url"] = awsAmiRepoURL + bodyAttrs["non_recursive"] = true + + serverUtils.MakeRemoteScanRequest(requestURL, bodyAttrs, http.StatusOK) + }) + }) + + Context("remote url doesn't not contain terraform files", func() { + It("should receive 400 bad request response", func() { + + bodyAttrs := make(map[string]interface{}) + bodyAttrs["remote_type"] = "git" + bodyAttrs["remote_url"] = "https://github.com/accurics/terrascan//test/e2e/test_data/iac/k8s/kubernetes_ingress_violation" + bodyAttrs["non_recursive"] = true + + serverUtils.MakeRemoteScanRequest(requestURL, bodyAttrs, http.StatusBadRequest) + }) + }) + }) + Context("remote_type or remote_url is not present in the request", func() { errMessage := "remote url or destination dir path cannot be empty" @@ -239,6 +265,19 @@ var _ = Describe("Server Remote Scan", func() { }) }) + When("non_recursive value is invalid", func() { + It("should receive a 400 bad request response", func() { + errMessage := "json: cannot unmarshal string into Go struct field scanRemoteRepoReq.non_recursive of type bool" + bodyAttrs := make(map[string]interface{}) + bodyAttrs["remote_type"] = "git" + bodyAttrs["remote_url"] = awsAmiRepoURL + bodyAttrs["non_recursive"] = "invalid" + + responseBytes := serverUtils.MakeRemoteScanRequest(requestURL, bodyAttrs, http.StatusBadRequest) + Expect(string(bytes.TrimSpace(responseBytes))).To(Equal(errMessage)) + }) + }) + When("severity value is invalid", func() { It("should receive a 400 bad request response", func() { errMessage := "json: cannot unmarshal number into Go struct field scanRemoteRepoReq.severity of type string" diff --git a/test/e2e/test_data/iac/terraform_recursive/subFolder1/subFolder2/main.tf b/test/e2e/test_data/iac/terraform_recursive/subFolder1/subFolder2/main.tf new file mode 100644 index 000000000..d8d65db63 --- /dev/null +++ b/test/e2e/test_data/iac/terraform_recursive/subFolder1/subFolder2/main.tf @@ -0,0 +1,12 @@ +provider "aws" { + region = "us-west-2" +} + +resource "aws_ami" "awsAmiEncrypted" { + name = "some-name" + + ebs_block_device { + device_name = "dev-name" + encrypted = "false" + } +} \ No newline at end of file diff --git a/test/helper/helper.go b/test/helper/helper.go index 87358c875..f486ad768 100644 --- a/test/helper/helper.go +++ b/test/helper/helper.go @@ -54,6 +54,9 @@ var ( // fileFolderPattern is regex for 'file/folder' attribute in violations output fileFolderPattern = regexp.MustCompile(`["]*[fF]ile[\/_][fF]older["]*[ \t]*[:=][ \t]*["]*(.+)[\\\/](.+)["]*`) + // planRootPattern is regex for 'file/folder' attribute in violations output + planRootPattern = regexp.MustCompile(`["]*[pP]lan[\/_][rR]oot["]*[ \t]*[:=][ \t]*["]*(.+)[\\\/](.+)["]*`) + // filePattern is regex for 'file' attribute in violations output filePattern = regexp.MustCompile(`["]*[fF]ile["]*[ \t]*[:=][ \t]*["]*(.+)[\\\/](.+)["]*`) @@ -161,6 +164,9 @@ func CompareActualWithGoldenSummaryRegex(session *gexec.Session, goldenFileAbsPa sessionOutput = errMsgPattern.ReplaceAllString(sessionOutput, "") fileContents = errMsgPattern.ReplaceAllString(fileContents, "") + + sessionOutput = planRootPattern.ReplaceAllString(sessionOutput, "") + fileContents = planRootPattern.ReplaceAllString(fileContents, "") } gomega.Expect(sessionOutput).Should(gomega.BeIdenticalTo(fileContents)) @@ -297,8 +303,8 @@ func CompareSummaryAndViolations(sessionEngineOutput, fileDataEngineOutput polic // 1. sort actual and golden violations and remove "file" attribute sort.Sort(actualViolations) sort.Sort(expectedViolations) - removeFileFromViolations(actualViolations) - removeFileFromViolations(expectedViolations) + removeFileAndRoothFromViolations(actualViolations) + removeFileAndRoothFromViolations(expectedViolations) actualSkippedViolations = sessionEngineOutput.ViolationStore.SkippedViolations expectedSkippedViolations = fileDataEngineOutput.ViolationStore.SkippedViolations @@ -306,8 +312,8 @@ func CompareSummaryAndViolations(sessionEngineOutput, fileDataEngineOutput polic // 2. sort actual and golden skipped violations and remove "file" attribute sort.Sort(actualSkippedViolations) sort.Sort(expectedSkippedViolations) - removeFileFromViolations(actualSkippedViolations) - removeFileFromViolations(expectedSkippedViolations) + removeFileAndRoothFromViolations(actualSkippedViolations) + removeFileAndRoothFromViolations(expectedSkippedViolations) actualPassedRules = sessionEngineOutput.ViolationStore.PassedRules expectedPassedRules = fileDataEngineOutput.ViolationStore.PassedRules @@ -337,11 +343,12 @@ func removeTimestampAndResourcePath(summary *results.ScanSummary) { summary.ResourcePath = "" } -// removeFileFromViolations is helper func to make file in violations blank -func removeFileFromViolations(v violations) { +// removeFileAndRoothFromViolations is helper func to make file in violations blank +func removeFileAndRoothFromViolations(v violations) { vs := []*results.Violation(v) for _, violation := range vs { violation.File = "" + violation.PlanRoot = "" } }