diff --git a/docs/usage/command_line_mode.md b/docs/usage/command_line_mode.md index 8d9477a53..7f33f4aac 100644 --- a/docs/usage/command_line_mode.md +++ b/docs/usage/command_line_mode.md @@ -205,6 +205,8 @@ aws_ecr_repository: | |scan-rules|Specify rules to scan, example: --scan-rules="ruleID1,ruleID2"| | |skip-rules|Specify one or more rules to skip while scanning. Example: --skip-rules="ruleID1,ruleID2"| | |use-colours |Configure the color for output (**auto**, t, f) | +|--non-recursive |Use this for non recursive directories and modules scan | By default directory is scanned recursively, if this flag is used then only provided root directory will be scanned| +|--use-terraform-cache |Use this to refer terraform remote modules from terraform init cache rather than downloading | By default remote module will be downloaded in temporary directory. If this flag is set then modules will be refered from terraform init cache if module is not present in terraform init cache it will be downloaded. Directory will be scanned non recurively if this flag is used.(applicable only with terraform IaC provider)| | -v | verbose | Displays violations with all details | | Global flags | Description | Options | @@ -246,6 +248,7 @@ Flags: --show-passed display passed rules, along with violations --skip-rules strings one or more rules to skip while scanning (example: --skip-rules="ruleID1,ruleID2") --use-colors string color output (auto, t, f) (default "auto") + --use-terraform-cache use terraform init cache for remote modules (when used directory scan will be non recursive,flag applicable only with terraform IaC provider) -v, --verbose will show violations with details (applicable for default output) Global Flags: diff --git a/go.mod b/go.mod index b8399c6f7..2c9cef344 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ replace ( ) require ( + github.com/BurntSushi/toml v0.4.0 // indirect github.com/VerbalExpressions/GoVerbalExpressions v0.0.0-20200410162751-4d76a1099a6e github.com/awslabs/goformation/v4 v4.19.1 github.com/ghodss/yaml v1.0.0 @@ -42,7 +43,7 @@ require ( github.com/zclconf/go-cty v1.8.3 go.uber.org/zap v1.16.0 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c - golang.org/x/tools v0.1.4 // indirect + golang.org/x/tools v0.1.5 // indirect gopkg.in/src-d/go-git.v4 v4.13.1 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b helm.sh/helm/v3 v3.4.0 diff --git a/go.sum b/go.sum index aa1a9ec1b..3853c6422 100644 --- a/go.sum +++ b/go.sum @@ -124,6 +124,16 @@ github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzS github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.3.2-0.20210614224209-34d990aa228d/go.mod h1:2QZjSXA5e+XyFeCAxxtL8Z4StYUsTquL8ODGPR3C3MA= +github.com/BurntSushi/toml v0.3.2-0.20210621044154-20a94d639b8e/go.mod h1:t4zg8TkHfP16Vb3x4WKIw7zVYMit5QFtPEO8lOWxzTg= +github.com/BurntSushi/toml v0.3.2-0.20210624061728-01bfc69d1057/go.mod h1:NMj2lD5LfMqcE0w8tnqOsH6944oaqpI1974lrIwerfE= +github.com/BurntSushi/toml v0.3.2-0.20210704081116-ccff24ee4463/go.mod h1:EkRrMiQQmfxK6kIldz3QbPlhmVkrjW1RDJUnbDqGYvc= +github.com/BurntSushi/toml v0.4.0 h1:qD/r9AL67srjW6O3fcSKZDsXqzBNX6ieSRywr2hRrdE= +github.com/BurntSushi/toml v0.4.0/go.mod h1:wtejDu7Q0FhCWAo2aXkywSJyYFg01EDTKozLNCz2JBA= +github.com/BurntSushi/toml-test v0.1.1-0.20210620192437-de01089bbf76/go.mod h1:P/PrhmZ37t5llHfDuiouWXtFgqOoQ12SAh9j6EjrBR4= +github.com/BurntSushi/toml-test v0.1.1-0.20210624055653-1f6389604dc6/go.mod h1:UAIt+Eo8itMZAAgImXkPGDMYsT1SsJkVdB5TuONl86A= +github.com/BurntSushi/toml-test v0.1.1-0.20210704062846-269931e74e3f/go.mod h1:fnFWrIwqgHsEjVsW3RYCJmDo86oq9eiJ9u6bnqhtm2g= +github.com/BurntSushi/toml-test v0.1.1-0.20210723065233-facb9eccd4da/go.mod h1:ve9Q/RRu2vHi42LocPLNvagxuUJh993/95b18bw/Nws= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= github.com/ChrisTrenkamp/goxpath v0.0.0-20190607011252-c5096ec8773d/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= @@ -1805,6 +1815,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.4 h1:cVngSRcfgyZCzys3KYOpCFa+4dqX/Oub9tAq00ttGVs= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2095,3 +2107,4 @@ sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1 sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4= vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= +zgo.at/zli v0.0.0-20210619044753-e7020a328e59/go.mod h1:HLAc12TjNGT+VRXr76JnsNE3pbooQtwKWhX+RlDjQ2Y= diff --git a/pkg/cli/run.go b/pkg/cli/run.go index 25139115f..676c9f1f2 100644 --- a/pkg/cli/run.go +++ b/pkg/cli/run.go @@ -88,7 +88,7 @@ type ScanOptions struct { // severity is the level of severity of policy violations that should be reported severity string - // verbose indicates whether to display all fields in default human readlbe output + // verbose indicates whether to display all fields in default human readable output verbose bool // showPassedRules indicates whether to display passed rules or not @@ -96,6 +96,9 @@ type ScanOptions struct { // nonRecursive enables recursive scan for the terraform iac provider nonRecursive bool + + // useTerraformCache provides ability to use terraform init local cache for modules rather than downloading them. + useTerraformCache bool } // NewScanOptions returns a new pointer to ScanOptions @@ -181,7 +184,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.nonRecursive) + s.iacFilePath, s.iacDirPath, s.policyPath, s.scanRules, s.skipRules, s.categories, s.severity, s.nonRecursive, s.useTerraformCache) if err != nil { return err } diff --git a/pkg/cli/scan.go b/pkg/cli/scan.go index a367312de..38a55f006 100644 --- a/pkg/cli/scan.go +++ b/pkg/cli/scan.go @@ -72,5 +72,6 @@ func init() { 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") + scanCmd.Flags().BoolVarP(&scanOptions.useTerraformCache, "use-terraform-cache", "", false, "use terraform init cache for remote modules (when used directory scan will be non recursive, flag applicable only with terraform IaC provider)") RegisterCommand(rootCmd, scanCmd) } diff --git a/pkg/http-server/file-scan.go b/pkg/http-server/file-scan.go index 92b4218fb..d44bfc420 100644 --- a/pkg/http-server/file-scan.go +++ b/pkg/http-server/file-scan.go @@ -152,10 +152,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, false) + tempFile.Name(), "", []string{"./testdata/testpolicies"}, scanRules, skipRules, categories, severity, false, false) } else { executor, err = runtime.NewExecutor(iacType, iacVersion, cloudType, - tempFile.Name(), "", getPolicyPathFromConfig(), scanRules, skipRules, categories, severity, false) + tempFile.Name(), "", getPolicyPathFromConfig(), scanRules, skipRules, categories, severity, false, 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 67786b6c8..1909158e0 100644 --- a/pkg/http-server/remote-repo.go +++ b/pkg/http-server/remote-repo.go @@ -128,7 +128,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, s.NonRecursive) + "", iacDirPath, policyPath, s.ScanRules, s.SkipRules, s.Categories, s.Severity, s.NonRecursive, false) if err != nil { zap.S().Error(err) return output, isAdmissionDenied, err diff --git a/pkg/iac-providers/arm/v1/load-dir.go b/pkg/iac-providers/arm/v1/load-dir.go index bb4b34918..947bec047 100644 --- a/pkg/iac-providers/arm/v1/load-dir.go +++ b/pkg/iac-providers/arm/v1/load-dir.go @@ -35,7 +35,7 @@ import ( const iacFile = "IAC file" // LoadIacDir loads all ARM template files in the current directory. -func (a *ARMV1) LoadIacDir(absRootDir string, nonRecursive bool) (output.AllResourceConfigs, error) { +func (a *ARMV1) LoadIacDir(absRootDir string, options map[string]interface{}) (output.AllResourceConfigs, error) { // set the root directory being scanned a.absRootDir = absRootDir @@ -62,7 +62,7 @@ func (a *ARMV1) LoadIacDir(absRootDir string, nonRecursive bool) (output.AllReso a.tryGetParameters(*files[i], fileDir, files) var configData output.AllResourceConfigs - if configData, err = a.LoadIacFile(file); err != nil { + if configData, err = a.LoadIacFile(file, options); err != nil { errMsg := fmt.Sprintf("error while loading iac file '%s'. err: %v", file, err) zap.S().Debug("error while loading iac files", zap.String("IAC file", file), zap.Error(err)) a.errIacLoadDirs = multierror.Append(a.errIacLoadDirs, results.DirScanErr{IacType: "arm", Directory: fileDir, ErrMessage: errMsg}) diff --git a/pkg/iac-providers/arm/v1/load-dir_test.go b/pkg/iac-providers/arm/v1/load-dir_test.go index 6063dbb44..edb799533 100644 --- a/pkg/iac-providers/arm/v1/load-dir_test.go +++ b/pkg/iac-providers/arm/v1/load-dir_test.go @@ -71,6 +71,7 @@ func TestLoadIacDir(t *testing.T) { armv1 ARMV1 name string dirPath string + options map[string]interface{} }{ { name: "empty config", @@ -94,7 +95,7 @@ func TestLoadIacDir(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - aRC, gotErr := tt.armv1.LoadIacDir(tt.dirPath, false) + aRC, gotErr := tt.armv1.LoadIacDir(tt.dirPath, tt.options) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) @@ -127,6 +128,8 @@ func TestARMMapper(t *testing.T) { t.Error(err) } + options := make(map[string]interface{}) + armv1 := ARMV1{} // get output json to verify @@ -143,7 +146,7 @@ func TestARMMapper(t *testing.T) { t.Run(root, func(t *testing.T) { - allResourceConfigs, gotErr := armv1.LoadIacDir(root, false) + allResourceConfigs, gotErr := armv1.LoadIacDir(root, options) _, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) diff --git a/pkg/iac-providers/arm/v1/load-file.go b/pkg/iac-providers/arm/v1/load-file.go index d044eb8f5..83b633256 100644 --- a/pkg/iac-providers/arm/v1/load-file.go +++ b/pkg/iac-providers/arm/v1/load-file.go @@ -34,7 +34,7 @@ import ( // LoadIacFile loads the specified ARM template file. // Note that a single ARM template json file may contain multiple resource definitions. -func (a *ARMV1) LoadIacFile(absFilePath string) (allResourcesConfig output.AllResourceConfigs, err error) { +func (a *ARMV1) LoadIacFile(absFilePath string, options map[string]interface{}) (allResourcesConfig output.AllResourceConfigs, err error) { allResourcesConfig = make(output.AllResourceConfigs) if fileExt := a.getFileType(absFilePath); fileExt != JSONExtension { return allResourcesConfig, fmt.Errorf("unsupported file %s", absFilePath) diff --git a/pkg/iac-providers/arm/v1/load-file_test.go b/pkg/iac-providers/arm/v1/load-file_test.go index 6ba5dd65f..e155bac48 100644 --- a/pkg/iac-providers/arm/v1/load-file_test.go +++ b/pkg/iac-providers/arm/v1/load-file_test.go @@ -31,6 +31,7 @@ func TestLoadIacFile(t *testing.T) { table := []struct { wantErr error want output.AllResourceConfigs + options map[string]interface{} armv1 ARMV1 name string filePath string @@ -58,7 +59,7 @@ func TestLoadIacFile(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.armv1.LoadIacFile(tt.filePath) + _, gotErr := tt.armv1.LoadIacFile(tt.filePath, tt.options) if !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } else if tt.typeOnly && (reflect.TypeOf(gotErr)) != reflect.TypeOf(tt.wantErr) { @@ -72,6 +73,7 @@ func TestLinkedTemplateDownload(t *testing.T) { table := []struct { wantErr error want output.AllResourceConfigs + options map[string]interface{} armv1 ARMV1 name string filePath string @@ -113,7 +115,7 @@ func TestLinkedTemplateDownload(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - aRC, gotErr := tt.armv1.LoadIacFile(tt.filePath) + aRC, gotErr := tt.armv1.LoadIacFile(tt.filePath, tt.options) if !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } else if tt.typeOnly && (reflect.TypeOf(gotErr)) != reflect.TypeOf(tt.wantErr) { diff --git a/pkg/iac-providers/cft/v1/load-dir.go b/pkg/iac-providers/cft/v1/load-dir.go index 8cca2d2bc..6d440a65c 100644 --- a/pkg/iac-providers/cft/v1/load-dir.go +++ b/pkg/iac-providers/cft/v1/load-dir.go @@ -29,7 +29,7 @@ import ( ) // LoadIacDir loads all CFT template files in the current directory. -func (a *CFTV1) LoadIacDir(absRootDir string, nonRecursive bool) (output.AllResourceConfigs, error) { +func (a *CFTV1) LoadIacDir(absRootDir string, options map[string]interface{}) (output.AllResourceConfigs, error) { a.absRootDir = absRootDir allResourcesConfig := make(map[string][]output.ResourceConfig) @@ -45,7 +45,7 @@ func (a *CFTV1) LoadIacDir(absRootDir string, nonRecursive bool) (output.AllReso file := filepath.Join(fileDir, *files[i]) var configData output.AllResourceConfigs - if configData, err = a.LoadIacFile(file); err != nil { + if configData, err = a.LoadIacFile(file, options); err != nil { errMsg := fmt.Sprintf("error while loading iac file '%s', err: %v", file, err) zap.S().Debug("error while loading iac files", zap.String("IAC file", file), zap.Error(err)) a.errIacLoadDirs = multierror.Append(a.errIacLoadDirs, results.DirScanErr{IacType: "cft", Directory: fileDir, ErrMessage: errMsg}) diff --git a/pkg/iac-providers/cft/v1/load-dir_test.go b/pkg/iac-providers/cft/v1/load-dir_test.go index 58a7eb733..f880355ff 100644 --- a/pkg/iac-providers/cft/v1/load-dir_test.go +++ b/pkg/iac-providers/cft/v1/load-dir_test.go @@ -64,6 +64,7 @@ func TestLoadIacDir(t *testing.T) { cftv1 CFTV1 name string dirPath string + options map[string]interface{} }{ { name: "empty config", @@ -95,7 +96,7 @@ func TestLoadIacDir(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.cftv1.LoadIacDir(tt.dirPath, false) + _, gotErr := tt.cftv1.LoadIacDir(tt.dirPath, tt.options) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) @@ -117,12 +118,12 @@ func TestCFTMapper(t *testing.T) { if err != nil { log.Fatal(err) } - + options := make(map[string]interface{}) cftv1 := CFTV1{} for _, dir := range dirList { resourceDir := filepath.Join(root, dir.Name()) t.Run(resourceDir, func(t *testing.T) { - allResourceConfigs, gotErr := cftv1.LoadIacDir(resourceDir, false) + allResourceConfigs, gotErr := cftv1.LoadIacDir(resourceDir, options) // load expected output.json from test artifacts var testArc output.AllResourceConfigs diff --git a/pkg/iac-providers/cft/v1/load-file.go b/pkg/iac-providers/cft/v1/load-file.go index 322599a2f..4700968a0 100644 --- a/pkg/iac-providers/cft/v1/load-file.go +++ b/pkg/iac-providers/cft/v1/load-file.go @@ -32,7 +32,7 @@ import ( // LoadIacFile loads the specified CFT template file. // Note that a single CFT template json file may contain multiple resource definitions. -func (a *CFTV1) LoadIacFile(absFilePath string) (allResourcesConfig output.AllResourceConfigs, err error) { +func (a *CFTV1) LoadIacFile(absFilePath string, options map[string]interface{}) (allResourcesConfig output.AllResourceConfigs, err error) { fileData, err := ioutil.ReadFile(absFilePath) if err != nil { zap.S().Debug("unable to read file", zap.Error(err), zap.String("file", absFilePath)) diff --git a/pkg/iac-providers/cft/v1/load-file_test.go b/pkg/iac-providers/cft/v1/load-file_test.go index 57e6033ad..2913e996d 100644 --- a/pkg/iac-providers/cft/v1/load-file_test.go +++ b/pkg/iac-providers/cft/v1/load-file_test.go @@ -43,6 +43,7 @@ func TestLoadIacFile(t *testing.T) { name string filePath string typeOnly bool + options map[string]interface{} }{ { wantErr: fmt.Errorf(testErrString1), @@ -77,7 +78,7 @@ func TestLoadIacFile(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.cftv1.LoadIacFile(tt.filePath) + _, gotErr := tt.cftv1.LoadIacFile(tt.filePath, tt.options) if !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("unexpected error; gotErr: '%+v', wantErr: '%+v'", gotErr, tt.wantErr) } else if tt.typeOnly && (reflect.TypeOf(gotErr)) != reflect.TypeOf(tt.wantErr) { diff --git a/pkg/iac-providers/docker/v1/load-dir.go b/pkg/iac-providers/docker/v1/load-dir.go index f4b9aa073..c6d89736c 100644 --- a/pkg/iac-providers/docker/v1/load-dir.go +++ b/pkg/iac-providers/docker/v1/load-dir.go @@ -28,7 +28,7 @@ import ( ) // LoadIacDir loads the docker file specified in given folder. -func (dc *DockerV1) LoadIacDir(absRootDir string, nonRecursive bool) (output.AllResourceConfigs, error) { +func (dc *DockerV1) LoadIacDir(absRootDir string, options map[string]interface{}) (output.AllResourceConfigs, error) { // set the root directory being scanned dc.absRootDir = absRootDir @@ -49,7 +49,7 @@ func (dc *DockerV1) LoadIacDir(absRootDir string, nonRecursive bool) (output.All for i := range files { file := filepath.Join(fileDir, *files[i]) var configData output.AllResourceConfigs - if configData, err = dc.LoadIacFile(file); err != nil { + if configData, err = dc.LoadIacFile(file, options); err != nil { errMsg := fmt.Sprintf("error while parsing file %s", file) zap.S().Errorf("error while searching for iac files", zap.String("root dir", absRootDir), errMsg) dc.errIacLoadDirs = multierror.Append(dc.errIacLoadDirs, results.DirScanErr{IacType: "docker", Directory: absRootDir, ErrMessage: errMsg}) diff --git a/pkg/iac-providers/docker/v1/load-dir_test.go b/pkg/iac-providers/docker/v1/load-dir_test.go index 05679621d..437828324 100644 --- a/pkg/iac-providers/docker/v1/load-dir_test.go +++ b/pkg/iac-providers/docker/v1/load-dir_test.go @@ -41,6 +41,7 @@ func TestLoadIacDir(t *testing.T) { tests := []struct { name string dirPath string + options map[string]interface{} dockerV1 DockerV1 want output.AllResourceConfigs wantErr error @@ -94,7 +95,7 @@ func TestLoadIacDir(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, gotErr := tt.dockerV1.LoadIacDir(tt.dirPath, false) + got, gotErr := tt.dockerV1.LoadIacDir(tt.dirPath, tt.options) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) diff --git a/pkg/iac-providers/docker/v1/load-file.go b/pkg/iac-providers/docker/v1/load-file.go index 0eab0e6d9..9f6c4425f 100644 --- a/pkg/iac-providers/docker/v1/load-file.go +++ b/pkg/iac-providers/docker/v1/load-file.go @@ -41,7 +41,7 @@ const ( ) // LoadIacFile loads the docker file specified and create ResourceConfig for each dockerfile -func (dc *DockerV1) LoadIacFile(absFilePath string) (allResourcesConfig output.AllResourceConfigs, err error) { +func (dc *DockerV1) LoadIacFile(absFilePath string, options map[string]interface{}) (allResourcesConfig output.AllResourceConfigs, err error) { allResourcesConfig = make(map[string][]output.ResourceConfig) data, comments, err := dc.Parse(absFilePath) diff --git a/pkg/iac-providers/docker/v1/load-file_test.go b/pkg/iac-providers/docker/v1/load-file_test.go index 41ddf46a6..d80a59848 100644 --- a/pkg/iac-providers/docker/v1/load-file_test.go +++ b/pkg/iac-providers/docker/v1/load-file_test.go @@ -127,6 +127,7 @@ func TestLoadIacFile(t *testing.T) { tests := []struct { name string absFilePath string + options map[string]interface{} dockerV1 DockerV1 want output.AllResourceConfigs wantErr error @@ -155,7 +156,7 @@ func TestLoadIacFile(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, gotErr := tt.dockerV1.LoadIacFile(tt.absFilePath) + got, gotErr := tt.dockerV1.LoadIacFile(tt.absFilePath, tt.options) if tt.want != nil { if got == nil || !reflect.DeepEqual(got, tt.want) { t.Errorf("unexpected result; got: '%#v', want: '%v'", got, tt.want) diff --git a/pkg/iac-providers/helm/v3/load-dir.go b/pkg/iac-providers/helm/v3/load-dir.go index 1850c2bf3..598344c3b 100644 --- a/pkg/iac-providers/helm/v3/load-dir.go +++ b/pkg/iac-providers/helm/v3/load-dir.go @@ -42,7 +42,7 @@ var ( ) // LoadIacDir loads all helm charts under the specified directory -func (h *HelmV3) LoadIacDir(absRootDir string, nonRecursive bool) (output.AllResourceConfigs, error) { +func (h *HelmV3) LoadIacDir(absRootDir string, options map[string]interface{}) (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 32072e9b1..719ba2b45 100644 --- a/pkg/iac-providers/helm/v3/load-dir_test.go +++ b/pkg/iac-providers/helm/v3/load-dir_test.go @@ -45,6 +45,7 @@ func TestLoadIacDir(t *testing.T) { name string dirPath string helmv3 HelmV3 + options map[string]interface{} want output.AllResourceConfigs wantErr error resourceCount int @@ -79,7 +80,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, false) + resources, gotErr := tt.helmv3.LoadIacDir(tt.dirPath, tt.options) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) diff --git a/pkg/iac-providers/helm/v3/load-file.go b/pkg/iac-providers/helm/v3/load-file.go index 71064925c..4258a6205 100644 --- a/pkg/iac-providers/helm/v3/load-file.go +++ b/pkg/iac-providers/helm/v3/load-file.go @@ -28,7 +28,7 @@ var ( ) // LoadIacFile is not supported for helm. Only loading chart directories are supported -func (h *HelmV3) LoadIacFile(absRootPath string) (allResourcesConfig output.AllResourceConfigs, err error) { +func (h *HelmV3) LoadIacFile(absRootPath string, options map[string]interface{}) (allResourcesConfig output.AllResourceConfigs, err error) { zap.S().Errorf("load iac file is not supported for helm") return make(map[string][]output.ResourceConfig), errLoadIacFileNotSupported } diff --git a/pkg/iac-providers/helm/v3/load-file_test.go b/pkg/iac-providers/helm/v3/load-file_test.go index 95c27f10b..3bd5ca9a2 100644 --- a/pkg/iac-providers/helm/v3/load-file_test.go +++ b/pkg/iac-providers/helm/v3/load-file_test.go @@ -28,6 +28,7 @@ func TestLoadIacFile(t *testing.T) { table := []struct { name string filePath string + options map[string]interface{} helmv3 HelmV3 typeOnly bool want output.AllResourceConfigs @@ -43,7 +44,7 @@ func TestLoadIacFile(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.helmv3.LoadIacFile(tt.filePath) + _, gotErr := tt.helmv3.LoadIacFile(tt.filePath, tt.options) if !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } else if tt.typeOnly && (reflect.TypeOf(gotErr)) != reflect.TypeOf(tt.wantErr) { diff --git a/pkg/iac-providers/interface.go b/pkg/iac-providers/interface.go index a962c8ea3..3d94118ab 100644 --- a/pkg/iac-providers/interface.go +++ b/pkg/iac-providers/interface.go @@ -23,6 +23,6 @@ import ( // IacProvider defines the interface which every IaC provider needs to implement // to claim support in terrascan type IacProvider interface { - LoadIacFile(string) (output.AllResourceConfigs, error) - LoadIacDir(string, bool) (output.AllResourceConfigs, error) + LoadIacFile(string, map[string]interface{}) (output.AllResourceConfigs, error) + LoadIacDir(string, map[string]interface{}) (output.AllResourceConfigs, error) } diff --git a/pkg/iac-providers/kubernetes/v1/load-dir.go b/pkg/iac-providers/kubernetes/v1/load-dir.go index a606420bd..faeeb85a6 100644 --- a/pkg/iac-providers/kubernetes/v1/load-dir.go +++ b/pkg/iac-providers/kubernetes/v1/load-dir.go @@ -25,7 +25,7 @@ func (*K8sV1) getFileType(file string) string { } // LoadIacDir loads all k8s files in the current directory -func (k *K8sV1) LoadIacDir(absRootDir string, nonRecursive bool) (output.AllResourceConfigs, error) { +func (k *K8sV1) LoadIacDir(absRootDir string, options map[string]interface{}) (output.AllResourceConfigs, error) { // set the root directory being scanned k.absRootDir = absRootDir @@ -42,7 +42,7 @@ func (k *K8sV1) LoadIacDir(absRootDir string, nonRecursive bool) (output.AllReso file := filepath.Join(fileDir, *files[i]) var configData output.AllResourceConfigs - if configData, err = k.LoadIacFile(file); err != nil { + if configData, err = k.LoadIacFile(file, options); err != nil { errMsg := fmt.Sprintf("error while loading iac file '%s'. err: %v", file, err) zap.S().Debug("error while loading iac files", zap.String("IAC file", file), zap.Error(err)) k.errIacLoadDirs = multierror.Append(k.errIacLoadDirs, results.DirScanErr{IacType: "k8s", Directory: fileDir, ErrMessage: errMsg}) diff --git a/pkg/iac-providers/kubernetes/v1/load-dir_test.go b/pkg/iac-providers/kubernetes/v1/load-dir_test.go index beb350f81..ac84f337e 100644 --- a/pkg/iac-providers/kubernetes/v1/load-dir_test.go +++ b/pkg/iac-providers/kubernetes/v1/load-dir_test.go @@ -39,6 +39,7 @@ func TestLoadIacDir(t *testing.T) { name string dirPath string k8sV1 K8sV1 + options map[string]interface{} want output.AllResourceConfigs wantErr error }{ @@ -88,7 +89,7 @@ func TestLoadIacDir(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.k8sV1.LoadIacDir(tt.dirPath, false) + _, gotErr := tt.k8sV1.LoadIacDir(tt.dirPath, tt.options) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) diff --git a/pkg/iac-providers/kubernetes/v1/load-file.go b/pkg/iac-providers/kubernetes/v1/load-file.go index dccb13b02..a672d1c5d 100644 --- a/pkg/iac-providers/kubernetes/v1/load-file.go +++ b/pkg/iac-providers/kubernetes/v1/load-file.go @@ -12,7 +12,7 @@ import ( // LoadIacFile loads the k8s file specified // Note that a single k8s yaml file may contain multiple resource definitions -func (k *K8sV1) LoadIacFile(absFilePath string) (allResourcesConfig output.AllResourceConfigs, err error) { +func (k *K8sV1) LoadIacFile(absFilePath string, options map[string]interface{}) (allResourcesConfig output.AllResourceConfigs, err error) { allResourcesConfig = make(map[string][]output.ResourceConfig) var iacDocuments []*utils.IacDocument diff --git a/pkg/iac-providers/kubernetes/v1/load-file_test.go b/pkg/iac-providers/kubernetes/v1/load-file_test.go index aa5737901..b42d18cd0 100644 --- a/pkg/iac-providers/kubernetes/v1/load-file_test.go +++ b/pkg/iac-providers/kubernetes/v1/load-file_test.go @@ -32,6 +32,7 @@ func TestLoadIacFile(t *testing.T) { table := []struct { name string filePath string + options map[string]interface{} k8sV1 K8sV1 typeOnly bool want output.AllResourceConfigs @@ -80,7 +81,7 @@ func TestLoadIacFile(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.k8sV1.LoadIacFile(tt.filePath) + _, gotErr := tt.k8sV1.LoadIacFile(tt.filePath, tt.options) if !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } else if tt.typeOnly && (reflect.TypeOf(gotErr)) != reflect.TypeOf(tt.wantErr) { diff --git a/pkg/iac-providers/kustomize/v3/load-dir.go b/pkg/iac-providers/kustomize/v3/load-dir.go index fd7e179cd..5963b3a02 100644 --- a/pkg/iac-providers/kustomize/v3/load-dir.go +++ b/pkg/iac-providers/kustomize/v3/load-dir.go @@ -23,7 +23,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, nonRecursive bool) (output.AllResourceConfigs, error) { +func (k *KustomizeV3) LoadIacDir(absRootDir string, options map[string]interface{}) (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 c3932cc09..8c515a722 100644 --- a/pkg/iac-providers/kustomize/v3/load-dir_test.go +++ b/pkg/iac-providers/kustomize/v3/load-dir_test.go @@ -27,6 +27,7 @@ func TestLoadIacDir(t *testing.T) { want output.AllResourceConfigs wantErr error resourceCount int + options map[string]interface{} }{ { name: "invalid dirPath", @@ -90,7 +91,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, false) + resourceMap, gotErr := tt.kustomize.LoadIacDir(tt.dirPath, tt.options) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) @@ -123,6 +124,7 @@ func TestLoadKustomize(t *testing.T) { want output.AllResourceConfigs wantErr error checkPrefix bool + options map[string]interface{} }{ { name: "simple-deployment", diff --git a/pkg/iac-providers/kustomize/v3/load-file.go b/pkg/iac-providers/kustomize/v3/load-file.go index 41dc186de..02967d5c0 100644 --- a/pkg/iac-providers/kustomize/v3/load-file.go +++ b/pkg/iac-providers/kustomize/v3/load-file.go @@ -12,7 +12,7 @@ var ( ) // LoadIacFile is not supported for kustomize. Only loading directories that have kustomization.y(a)ml file are supported -func (k *KustomizeV3) LoadIacFile(absRootPath string) (allResourcesConfig output.AllResourceConfigs, err error) { +func (k *KustomizeV3) LoadIacFile(absRootPath string, options map[string]interface{}) (allResourcesConfig output.AllResourceConfigs, err error) { zap.S().Error(errLoadIacFileNotSupported) return make(map[string][]output.ResourceConfig), errLoadIacFileNotSupported } diff --git a/pkg/iac-providers/kustomize/v3/load-file_test.go b/pkg/iac-providers/kustomize/v3/load-file_test.go index 8d52917a8..b46553898 100644 --- a/pkg/iac-providers/kustomize/v3/load-file_test.go +++ b/pkg/iac-providers/kustomize/v3/load-file_test.go @@ -12,6 +12,7 @@ func TestLoadIacFile(t *testing.T) { table := []struct { name string filePath string + options map[string]interface{} kustomize KustomizeV3 typeOnly bool want output.AllResourceConfigs @@ -27,7 +28,7 @@ func TestLoadIacFile(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.kustomize.LoadIacFile(tt.filePath) + _, gotErr := tt.kustomize.LoadIacFile(tt.filePath, tt.options) if !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } else if tt.typeOnly && (reflect.TypeOf(gotErr)) != reflect.TypeOf(tt.wantErr) { diff --git a/pkg/iac-providers/terraform/commons/load-dir.go b/pkg/iac-providers/terraform/commons/load-dir.go index a513ca8e1..624a25810 100644 --- a/pkg/iac-providers/terraform/commons/load-dir.go +++ b/pkg/iac-providers/terraform/commons/load-dir.go @@ -17,10 +17,13 @@ package commons import ( + "encoding/json" "errors" "fmt" + "io/ioutil" "os" "path/filepath" + "strings" "github.com/accurics/terrascan/pkg/downloader" "github.com/accurics/terrascan/pkg/iac-providers/output" @@ -40,6 +43,24 @@ var ( ErrBuildTFConfigDir = fmt.Errorf("failed to build terraform allResourcesConfig") ) +const ( + terraformModuleInstallDir = ".terraform/modules" + terraformInstalledModulelMetaFileName = "modules.json" +) + +// TerraformInstalledModuleMetaData metadata about the module downloaded and present in terraform cache. +type TerraformInstalledModuleMetaData struct { + Key string `json:"Key"` + SourceAddr string `json:"Source"` + VersionStr string `json:"Version,omitempty"` + Dir string `json:"Dir"` +} + +//TerraformModuleManifest holds details of all modules downloaded by terraform +type TerraformModuleManifest struct { + Modules []TerraformInstalledModuleMetaData `json:"Modules"` +} + // ModuleConfig contains the *hclConfigs.Config for every module in the // unified config tree along with *hclConfig.ModuleCall made by the parent // module. The ParentModuleCall helps in resolving references for variables @@ -52,21 +73,33 @@ type ModuleConfig struct { // TerraformDirectoryLoader implements terraform directory loading type TerraformDirectoryLoader struct { - absRootDir string - nonRecursive bool - remoteDownloader downloader.ModuleDownloader - parser *hclConfigs.Parser - errIacLoadDirs *multierror.Error + absRootDir string + nonRecursive bool + useTerraformCache bool + remoteDownloader downloader.ModuleDownloader + parser *hclConfigs.Parser + errIacLoadDirs *multierror.Error + terraformInitModuleCache map[string]TerraformModuleManifest } // 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()), +func NewTerraformDirectoryLoader(rootDirectory string, options map[string]interface{}) TerraformDirectoryLoader { + terraformDirectoryLoader := TerraformDirectoryLoader{ + absRootDir: rootDirectory, + remoteDownloader: downloader.NewRemoteDownloader(), + parser: hclConfigs.NewParser(afero.NewOsFs()), + terraformInitModuleCache: make(map[string]TerraformModuleManifest), + } + for key, val := range options { + // keeping switch case in case more flags are added + switch key { + case "useTerraformCache": + terraformDirectoryLoader.useTerraformCache = val.(bool) + case "nonRecursive": + terraformDirectoryLoader.nonRecursive = val.(bool) + } } + return terraformDirectoryLoader } // LoadIacDir starts traversing from the given rootDir and traverses through @@ -76,7 +109,7 @@ func (t TerraformDirectoryLoader) LoadIacDir() (allResourcesConfig output.AllRes defer t.remoteDownloader.CleanUp() - if t.nonRecursive { + if t.nonRecursive || t.useTerraformCache { return t.loadDirNonRecursive() } @@ -335,22 +368,30 @@ func (t TerraformDirectoryLoader) buildUnifiedConfig(rootMod *hclConfigs.Module, 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 if t.useTerraformCache { + // check if module is present in terraform cache + if _, dest := t.GetRemoteModuleIfPresentInTerraformSrc(req); dest != "" { + pathToModule = dest } - } else { - // temp dir to download the remote repo - tempDir := utils.GenerateTempDir() + } + if pathToModule == "" { + 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) + // 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) + } } } @@ -459,3 +500,63 @@ func GetConfigSource(remoteURLMapping map[string]string, resourceConfig output.R } return source, nil } + +// GetRemoteModuleIfPresentInTerraformSrc - Gets the remote module if present in terraform init cache +func (t *TerraformDirectoryLoader) GetRemoteModuleIfPresentInTerraformSrc(req *hclConfigs.ModuleRequest) (src string, destpath string) { + terraformInitRegs := filepath.Join(t.absRootDir, terraformModuleInstallDir) + modules := TerraformModuleManifest{} + var ok bool + if modules, ok = t.terraformInitModuleCache[terraformInitRegs]; !ok { + if utils.IsDirExists(terraformInitRegs) { + _, err := os.Stat(filepath.Join(terraformInitRegs, terraformInstalledModulelMetaFileName)) + if err != nil { + if os.IsNotExist(err) { + zap.S().Debug("found no terraform module metadata file in dir %s", terraformInitRegs) + return + } + zap.S().Error("error reading terraform module metadata file", err) + return + } + data, err := ioutil.ReadFile(filepath.Join(terraformInitRegs, terraformInstalledModulelMetaFileName)) + if err == nil { + err := json.Unmarshal(data, &modules) + if err != nil { + zap.S().Error("error unmarshalling terraform module metadata", err) + return + } + } + } + // if the module metadata file was read first time add that to cache against the found working directory + t.terraformInitModuleCache[terraformInitRegs] = modules + } + for _, m := range modules.Modules { + if strings.EqualFold(m.SourceAddr, req.SourceAddr) { + // if the module source is not registry then version check is not required + if !downloader.IsRegistrySourceAddr(req.SourceAddr) { + return req.SourceAddr, filepath.Join(t.absRootDir, m.Dir) + } else if versionSatisfied(m.VersionStr, req.VersionConstraint) { + return req.SourceAddr, filepath.Join(t.absRootDir, m.Dir) + } + } + } + zap.S().Debug("found no version matching for module: %s in terraform module cache %s", req.Name, filepath.Join(terraformInitRegs, "modules.json")) + return +} + +//versionSatisfied - check version in terraform init cache satisfies the required version constraints +func versionSatisfied(foundversion string, requiredVersion hclConfigs.VersionConstraint) bool { + currentVersion, err := version.NewVersion(foundversion) + if err != nil { + return false + } + + if requiredVersion.Required == nil && foundversion != "" { + return true + } + + if requiredVersion.Required.Check(currentVersion) { + return true + } + + return false +} diff --git a/pkg/iac-providers/terraform/commons/load-dir_test.go b/pkg/iac-providers/terraform/commons/load-dir_test.go index 98ab7b2d1..8bd29f2a4 100644 --- a/pkg/iac-providers/terraform/commons/load-dir_test.go +++ b/pkg/iac-providers/terraform/commons/load-dir_test.go @@ -26,6 +26,7 @@ import ( "github.com/accurics/terrascan/pkg/utils" "github.com/hashicorp/hcl/v2" hclConfigs "github.com/hashicorp/terraform/configs" + "go.uber.org/zap" ) // test data @@ -47,9 +48,10 @@ func TestProcessLocalSource(t *testing.T) { req *hclConfigs.ModuleRequest } tests := []struct { - name string - args args - want string + name string + args args + want string + options map[string]interface{} }{ { name: "no remote module", @@ -61,7 +63,7 @@ func TestProcessLocalSource(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - dl := NewTerraformDirectoryLoader("", false) + dl := NewTerraformDirectoryLoader("", tt.options) if got := dl.processLocalSource(tt.args.req); got != tt.want { t.Errorf("processLocalSource() got = %v, want = %v", got, tt.want) } @@ -83,6 +85,7 @@ func TestProcessTerraformRegistrySource(t *testing.T) { args args want string wantErr bool + options map[string]interface{} }{ { name: "invalid registry host", @@ -112,7 +115,7 @@ func TestProcessTerraformRegistrySource(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { defer os.RemoveAll(tt.args.tempDir) - dl := NewTerraformDirectoryLoader("", false) + dl := NewTerraformDirectoryLoader("", tt.options) 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) @@ -256,3 +259,68 @@ func TestGetConfigSource(t *testing.T) { }) } } + +func TestGetRemoteModuleIfPresentInTerraformSrc(t *testing.T) { + absRootDir, err := filepath.Abs(filepath.Dir(filepath.Join("testdata", "terraform_cache_use_in_scan", "remote-module.tf"))) + if err != nil { + zap.S().Error("error finding working directory", err) + } + terraformInitRegs := filepath.Join(absRootDir, terraformModuleInstallDir, "network") + type fields struct { + Cache map[string]TerraformModuleManifest + } + type args struct { + req *hclConfigs.ModuleRequest + } + tests := []struct { + name string + fields fields + args args + wantSrc string + wantDestpath string + }{ + { + name: "module present in terraform cache", + fields: fields{ + Cache: make(map[string]TerraformModuleManifest), + }, + args: args{ + req: &hclConfigs.ModuleRequest{ + SourceAddr: "Azure/network/azurerm", + SourceAddrRange: hcl.Range{Filename: filepath.Join("testdata", "terraform_cache_use_in_scan", "remote-module.tf")}, + }, + }, + wantSrc: "Azure/network/azurerm", + wantDestpath: terraformInitRegs, + }, + { + name: "module not present in terraform cache", + fields: fields{ + Cache: make(map[string]TerraformModuleManifest), + }, + args: args{ + req: &hclConfigs.ModuleRequest{ + SourceAddr: "Azure/network/azurermtest", + SourceAddrRange: hcl.Range{Filename: filepath.Join("testdata", "terraform_cache_use_in_scan", "remote-module.tf")}, + }, + }, + wantSrc: "", + wantDestpath: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := &TerraformDirectoryLoader{ + absRootDir: absRootDir, + terraformInitModuleCache: tt.fields.Cache, + } + gotSrc, gotDestpath := tr.GetRemoteModuleIfPresentInTerraformSrc(tt.args.req) + if gotSrc != tt.wantSrc { + t.Errorf("TerraformModuleManifestCache.GetRemoteModuleIfPresentInTerraformSrc() gotSrc = %v, want %v", gotSrc, tt.wantSrc) + } + if gotDestpath != tt.wantDestpath { + t.Errorf("TerraformModuleManifestCache.GetRemoteModuleIfPresentInTerraformSrc() gotDestpath = %v, want %v", gotDestpath, tt.wantDestpath) + } + }) + } +} diff --git a/pkg/iac-providers/terraform/commons/testdata/terraform_cache_use_in_scan/.terraform/modules/modules.json b/pkg/iac-providers/terraform/commons/testdata/terraform_cache_use_in_scan/.terraform/modules/modules.json new file mode 100644 index 000000000..3f3723ed3 --- /dev/null +++ b/pkg/iac-providers/terraform/commons/testdata/terraform_cache_use_in_scan/.terraform/modules/modules.json @@ -0,0 +1,15 @@ +{ + "Modules": [ + { + "Key": "", + "Source": "", + "Dir": "." + }, + { + "Key": "network", + "Source": "Azure/network/azurerm", + "Version": "3.2.1", + "Dir": ".terraform/modules/network" + } + ] +} \ No newline at end of file diff --git a/pkg/iac-providers/terraform/commons/testdata/terraform_cache_use_in_scan/.terraform/modules/network/main.tf b/pkg/iac-providers/terraform/commons/testdata/terraform_cache_use_in_scan/.terraform/modules/network/main.tf new file mode 100644 index 000000000..780f2118c --- /dev/null +++ b/pkg/iac-providers/terraform/commons/testdata/terraform_cache_use_in_scan/.terraform/modules/network/main.tf @@ -0,0 +1,21 @@ +#Azure Generic vNet Module +data "azurerm_resource_group" "network" { + name = var.resource_group_name +} + +resource "azurerm_virtual_network" "vnet" { + name = var.vnet_name + resource_group_name = data.azurerm_resource_group.network.name + location = data.azurerm_resource_group.network.location + address_space = [var.address_space] + dns_servers = var.dns_servers + tags = var.tags +} + +resource "azurerm_subnet" "subnet" { + count = length(var.subnet_names) + name = var.subnet_names[count.index] + resource_group_name = data.azurerm_resource_group.network.name + address_prefixes = [var.subnet_prefixes[count.index]] + virtual_network_name = azurerm_virtual_network.vnet.name +} diff --git a/pkg/iac-providers/terraform/commons/testdata/terraform_cache_use_in_scan/.terraform/modules/network/outputs.tf b/pkg/iac-providers/terraform/commons/testdata/terraform_cache_use_in_scan/.terraform/modules/network/outputs.tf new file mode 100644 index 000000000..4534e0215 --- /dev/null +++ b/pkg/iac-providers/terraform/commons/testdata/terraform_cache_use_in_scan/.terraform/modules/network/outputs.tf @@ -0,0 +1,24 @@ +output "vnet_id" { + description = "The id of the newly created vNet" + value = azurerm_virtual_network.vnet.id +} + +output "vnet_name" { + description = "The Name of the newly created vNet" + value = azurerm_virtual_network.vnet.name +} + +output "vnet_location" { + description = "The location of the newly created vNet" + value = azurerm_virtual_network.vnet.location +} + +output "vnet_address_space" { + description = "The address space of the newly created vNet" + value = azurerm_virtual_network.vnet.address_space +} + +output "vnet_subnets" { + description = "The ids of subnets created inside the newl vNet" + value = azurerm_subnet.subnet.*.id +} diff --git a/pkg/iac-providers/terraform/commons/testdata/terraform_cache_use_in_scan/.terraform/modules/network/variables.tf b/pkg/iac-providers/terraform/commons/testdata/terraform_cache_use_in_scan/.terraform/modules/network/variables.tf new file mode 100644 index 000000000..3a964abf9 --- /dev/null +++ b/pkg/iac-providers/terraform/commons/testdata/terraform_cache_use_in_scan/.terraform/modules/network/variables.tf @@ -0,0 +1,44 @@ +variable "vnet_name" { + description = "Name of the vnet to create." + type = string + default = "acctvnet" +} + +variable "resource_group_name" { + description = "The name of an existing resource group to be imported." + type = string +} + +variable "address_space" { + description = "The address space that is used by the virtual network." + type = string + default = "10.0.0.0/16" +} + +# If no values specified, this defaults to Azure DNS +variable "dns_servers" { + description = "The DNS servers to be used with vNet." + type = list(string) + default = [] +} + +variable "subnet_prefixes" { + description = "The address prefix to use for the subnet." + type = list(string) + default = ["10.0.1.0/24"] +} + +variable "subnet_names" { + description = "A list of public subnets inside the vNet." + type = list(string) + default = ["subnet1"] +} + +variable "tags" { + description = "The tags to associate with your network and subnets." + type = map(string) + + default = { + environment = "dev" + } +} diff --git a/pkg/iac-providers/terraform/commons/testdata/terraform_cache_use_in_scan/remote-module.tf b/pkg/iac-providers/terraform/commons/testdata/terraform_cache_use_in_scan/remote-module.tf new file mode 100644 index 000000000..fb88de2fd --- /dev/null +++ b/pkg/iac-providers/terraform/commons/testdata/terraform_cache_use_in_scan/remote-module.tf @@ -0,0 +1,9 @@ +#file added for fix for #418 + +# source https://registry.terraform.io/ + +module "network" { + source = "Azure/network/azurerm" + version = "3.2.1" +} + diff --git a/pkg/iac-providers/terraform/v12/load-dir.go b/pkg/iac-providers/terraform/v12/load-dir.go index 471a08465..6675e82bf 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, nonRecursive bool) (allResourcesConfig output.AllResourceConfigs, err error) { +func (*TfV12) LoadIacDir(absRootDir string, options map[string]interface{}) (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.NewTerraformDirectoryLoader(absRootDir, nonRecursive).LoadIacDir() + return commons.NewTerraformDirectoryLoader(absRootDir, options).LoadIacDir() } diff --git a/pkg/iac-providers/terraform/v12/load-dir_test.go b/pkg/iac-providers/terraform/v12/load-dir_test.go index 262fca7cd..86de9ea30 100644 --- a/pkg/iac-providers/terraform/v12/load-dir_test.go +++ b/pkg/iac-providers/terraform/v12/load-dir_test.go @@ -82,19 +82,21 @@ References to other resources during the destroy phase can cause dependency cycl } table := []struct { - name string - dirPath string - tfv12 TfV12 - want output.AllResourceConfigs - nonRecursive bool - wantErr error + name string + dirPath string + tfv12 TfV12 + want output.AllResourceConfigs + wantErr error + options map[string]interface{} }{ { - name: "invalid dirPath", - dirPath: testDirPath1, - tfv12: TfV12{}, - nonRecursive: true, - wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath1)), + name: "invalid dirPath", + dirPath: testDirPath1, + tfv12: TfV12{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath1)), }, { name: "invalid dirPath recursive", @@ -103,11 +105,13 @@ References to other resources during the destroy phase can cause dependency cycl wantErr: multierror.Append(pathErr), }, { - name: "empty config", - dirPath: testDirPath2, - tfv12: TfV12{}, - nonRecursive: true, - wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath2)), + name: "empty config", + dirPath: testDirPath2, + tfv12: TfV12{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath2)), }, { name: "empty config recursive", @@ -116,11 +120,13 @@ References to other resources during the destroy phase can cause dependency cycl wantErr: nilMultiErr, }, { - 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", + dirPath: filepath.Join(testDataDir, "invalid-moduleconfigs"), + tfv12: TfV12{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: multierror.Append(fmt.Errorf("failed to build terraform allResourcesConfig")), }, { name: "incorrect module structure recursive", @@ -130,11 +136,13 @@ References to other resources during the destroy phase can cause dependency cycl 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", + dirPath: testDataDir, + tfv12: TfV12{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: multierror.Append(fmt.Errorf(testErrorString1)), }, { name: "load invalid config dir recursive", @@ -154,11 +162,13 @@ References to other resources during the destroy phase can cause dependency cycl ), }, { - name: "load multiple provider config dir", - dirPath: multipleProvidersDir, - tfv12: TfV12{}, - nonRecursive: true, - wantErr: multierror.Append(fmt.Errorf(testErrorString2)), + name: "load multiple provider config dir", + dirPath: multipleProvidersDir, + tfv12: TfV12{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: multierror.Append(fmt.Errorf(testErrorString2)), }, { name: "load multiple provider config dir recursive", @@ -176,7 +186,7 @@ References to other resources during the destroy phase can cause dependency cycl for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.tfv12.LoadIacDir(tt.dirPath, tt.nonRecursive) + _, gotErr := tt.tfv12.LoadIacDir(tt.dirPath, tt.options) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) @@ -195,20 +205,22 @@ References to other resources during the destroy phase can cause dependency cycl nestedModuleErr2 := fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "deep-modules", "modules", "m4", "modules")) table2 := []struct { - name string - tfConfigDir string - tfJSONFile string - tfv12 TfV12 - nonRecursive bool - wantErr error + name string + tfConfigDir string + tfJSONFile string + tfv12 TfV12 + options map[string]interface{} + wantErr error }{ { - name: "config1", - tfConfigDir: filepath.Join(testDataDir, "tfconfigs"), - tfJSONFile: filepath.Join(tfJSONDir, "fullconfig.json"), - tfv12: TfV12{}, - nonRecursive: true, - wantErr: nilMultiErr, + name: "config1", + tfConfigDir: filepath.Join(testDataDir, "tfconfigs"), + tfJSONFile: filepath.Join(tfJSONDir, "fullconfig.json"), + tfv12: TfV12{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: nilMultiErr, }, { name: "config1 recursive", @@ -219,12 +231,14 @@ References to other resources during the destroy phase can cause dependency cycl wantErr: nilMultiErr, }, { - name: "module directory", - tfConfigDir: filepath.Join(testDataDir, "moduleconfigs"), - tfJSONFile: filepath.Join(tfJSONDir, "moduleconfigs.json"), - tfv12: TfV12{}, - nonRecursive: true, - wantErr: nilMultiErr, + name: "module directory", + tfConfigDir: filepath.Join(testDataDir, "moduleconfigs"), + tfJSONFile: filepath.Join(tfJSONDir, "moduleconfigs.json"), + tfv12: TfV12{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: nilMultiErr, }, { name: "module directory recursive", @@ -235,12 +249,14 @@ References to other resources during the destroy phase can cause dependency cycl 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: "nested module directory", + tfConfigDir: filepath.Join(testDataDir, "deep-modules"), + tfJSONFile: filepath.Join(tfJSONDir, "deep-modules.json"), + tfv12: TfV12{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: nilMultiErr, }, { name: "variables of list type", @@ -260,7 +276,7 @@ References to other resources during the destroy phase can cause dependency cycl for _, tt := range table2 { t.Run(tt.name, func(t *testing.T) { - got, gotErr := tt.tfv12.LoadIacDir(tt.tfConfigDir, tt.nonRecursive) + got, gotErr := tt.tfv12.LoadIacDir(tt.tfConfigDir, tt.options) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) diff --git a/pkg/iac-providers/terraform/v12/load-file.go b/pkg/iac-providers/terraform/v12/load-file.go index c55f4d0db..a94e16de2 100644 --- a/pkg/iac-providers/terraform/v12/load-file.go +++ b/pkg/iac-providers/terraform/v12/load-file.go @@ -23,7 +23,7 @@ import ( ) // LoadIacFile parses the given terraform file from the given file path -func (*TfV12) LoadIacFile(absFilePath string) (allResourcesConfig output.AllResourceConfigs, err error) { +func (*TfV12) LoadIacFile(absFilePath string, options map[string]interface{}) (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.LoadIacFile(absFilePath) } diff --git a/pkg/iac-providers/terraform/v12/load-file_test.go b/pkg/iac-providers/terraform/v12/load-file_test.go index 8468087aa..d75cfc492 100644 --- a/pkg/iac-providers/terraform/v12/load-file_test.go +++ b/pkg/iac-providers/terraform/v12/load-file_test.go @@ -59,6 +59,7 @@ References to other resources during the destroy phase can cause dependency cycl table := []struct { name string filePath string + options map[string]interface{} tfv12 TfV12 want output.AllResourceConfigs wantErr error @@ -90,7 +91,7 @@ References to other resources during the destroy phase can cause dependency cycl for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.tfv12.LoadIacFile(tt.filePath) + _, gotErr := tt.tfv12.LoadIacFile(tt.filePath, tt.options) if !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } @@ -101,6 +102,7 @@ References to other resources during the destroy phase can cause dependency cycl name string tfConfigFile string tfJSONFile string + options map[string]interface{} tfv12 TfV12 wantErr error }{ @@ -122,7 +124,7 @@ References to other resources during the destroy phase can cause dependency cycl for _, tt := range table2 { t.Run(tt.name, func(t *testing.T) { - got, gotErr := tt.tfv12.LoadIacFile(tt.tfConfigFile) + got, gotErr := tt.tfv12.LoadIacFile(tt.tfConfigFile, tt.options) if !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } diff --git a/pkg/iac-providers/terraform/v14/load-dir.go b/pkg/iac-providers/terraform/v14/load-dir.go index 9dac6d8ef..3790d0294 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, nonRecursive bool) (allResourcesConfig output.AllResourceConfigs, err error) { - return commons.NewTerraformDirectoryLoader(absRootDir, nonRecursive).LoadIacDir() +func (tfv14 *TfV14) LoadIacDir(absRootDir string, options map[string]interface{}) (allResourcesConfig output.AllResourceConfigs, err error) { + return commons.NewTerraformDirectoryLoader(absRootDir, options).LoadIacDir() } diff --git a/pkg/iac-providers/terraform/v14/load-dir_test.go b/pkg/iac-providers/terraform/v14/load-dir_test.go index aa9bb7c70..3384385af 100644 --- a/pkg/iac-providers/terraform/v14/load-dir_test.go +++ b/pkg/iac-providers/terraform/v14/load-dir_test.go @@ -63,19 +63,21 @@ func TestLoadIacDir(t *testing.T) { } table := []struct { - name string - dirPath string - tfv14 TfV14 - want output.AllResourceConfigs - nonRecursive bool - wantErr error + name string + dirPath string + tfv14 TfV14 + want output.AllResourceConfigs + options map[string]interface{} + wantErr error }{ { - name: "invalid dirPath", - dirPath: testDirPath1, - tfv14: TfV14{}, - nonRecursive: true, - wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath1)), + name: "invalid dirPath", + dirPath: testDirPath1, + tfv14: TfV14{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath1)), }, { name: "invalid dirPath recursive", @@ -84,11 +86,13 @@ func TestLoadIacDir(t *testing.T) { wantErr: multierror.Append(pathErr), }, { - name: "empty config", - dirPath: testDirPath2, - tfv14: TfV14{}, - nonRecursive: true, - wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath2)), + name: "empty config", + dirPath: testDirPath2, + tfv14: TfV14{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath2)), }, { name: "empty config recursive", @@ -97,11 +101,13 @@ func TestLoadIacDir(t *testing.T) { 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", + dirPath: filepath.Join(testDataDir, "invalid-moduleconfigs"), + tfv14: TfV14{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: multierror.Append(fmt.Errorf("failed to build terraform allResourcesConfig")), }, { name: "incorrect module structure recursive", @@ -111,11 +117,13 @@ func TestLoadIacDir(t *testing.T) { wantErr: multierror.Append(fmt.Errorf(errStringInvalidModuleConfigs), fmt.Errorf(errStringInvalidModuleConfigs)), }, { - name: "load invalid config dir", - dirPath: testDataDir, - tfv14: TfV14{}, - nonRecursive: true, - wantErr: multierror.Append(fmt.Errorf(testErrorMessage)), + name: "load invalid config dir", + dirPath: testDataDir, + tfv14: TfV14{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: multierror.Append(fmt.Errorf(testErrorMessage)), }, { name: "load invalid config dir recursive", @@ -143,7 +151,7 @@ func TestLoadIacDir(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.tfv14.LoadIacDir(tt.dirPath, tt.nonRecursive) + _, gotErr := tt.tfv14.LoadIacDir(tt.dirPath, tt.options) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) @@ -163,20 +171,22 @@ func TestLoadIacDir(t *testing.T) { nestedModuleErr2 := fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "deep-modules", "modules", "m4", "modules")) table2 := []struct { - name string - tfConfigDir string - tfJSONFile string - tfv14 TfV14 - nonRecursive bool - wantErr error + name string + tfConfigDir string + tfJSONFile string + tfv14 TfV14 + options map[string]interface{} + wantErr error }{ { - name: "config1", - tfConfigDir: filepath.Join(testDataDir, "tfconfigs"), - tfJSONFile: filepath.Join(tfJSONDir, "fullconfig.json"), - tfv14: TfV14{}, - nonRecursive: true, - wantErr: nilMultiErr, + name: "config1", + tfConfigDir: filepath.Join(testDataDir, "tfconfigs"), + tfJSONFile: filepath.Join(tfJSONDir, "fullconfig.json"), + tfv14: TfV14{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: nilMultiErr, }, { name: "config1 recursive", @@ -187,12 +197,14 @@ func TestLoadIacDir(t *testing.T) { wantErr: nilMultiErr, }, { - name: "module directory", - tfConfigDir: filepath.Join(testDataDir, "moduleconfigs"), - tfJSONFile: filepath.Join(tfJSONDir, "moduleconfigs.json"), - tfv14: TfV14{}, - nonRecursive: true, - wantErr: nilMultiErr, + name: "module directory", + tfConfigDir: filepath.Join(testDataDir, "moduleconfigs"), + tfJSONFile: filepath.Join(tfJSONDir, "moduleconfigs.json"), + tfv14: TfV14{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: nilMultiErr, }, { name: "module directory recursive", @@ -203,12 +215,14 @@ func TestLoadIacDir(t *testing.T) { wantErr: nilMultiErr, }, { - 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", + tfConfigDir: filepath.Join(testDataDir, "deep-modules"), + tfJSONFile: filepath.Join(tfJSONDir, "deep-modules.json"), + tfv14: TfV14{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: nilMultiErr, }, { name: "nested module directory recursive", @@ -218,42 +232,50 @@ func TestLoadIacDir(t *testing.T) { wantErr: multierror.Append(nestedModuleErr1, nestedModuleErr2), }, { - name: "complex variables", - tfConfigDir: filepath.Join(testDataDir, "complex-variables"), - tfJSONFile: filepath.Join(tfJSONDir, "complex-variables.json"), - tfv14: TfV14{}, - nonRecursive: true, - wantErr: nilMultiErr, + name: "complex variables", + tfConfigDir: filepath.Join(testDataDir, "complex-variables"), + tfJSONFile: filepath.Join(tfJSONDir, "complex-variables.json"), + tfv14: TfV14{}, + options: map[string]interface{}{ + "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{}, - 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{}, + options: map[string]interface{}{ + "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{}, - 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{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: nilMultiErr, }, { - name: "recursive loop while resolving locals with same name in parent and child module", - tfConfigDir: filepath.Join(testDataDir, "recursive-loop-duplicate-locals"), - tfJSONFile: filepath.Join(tfJSONDir, "recursive-loop-duplicate-locals.json"), - tfv14: TfV14{}, - nonRecursive: true, - wantErr: nilMultiErr, + name: "recursive loop while resolving locals with same name in parent and child module", + tfConfigDir: filepath.Join(testDataDir, "recursive-loop-duplicate-locals"), + tfJSONFile: filepath.Join(tfJSONDir, "recursive-loop-duplicate-locals.json"), + tfv14: TfV14{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: nilMultiErr, }, } for _, tt := range table2 { t.Run(tt.name, func(t *testing.T) { - got, gotErr := tt.tfv14.LoadIacDir(tt.tfConfigDir, tt.nonRecursive) + got, gotErr := tt.tfv14.LoadIacDir(tt.tfConfigDir, tt.options) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) diff --git a/pkg/iac-providers/terraform/v14/load-file.go b/pkg/iac-providers/terraform/v14/load-file.go index 43d61043a..a24293960 100644 --- a/pkg/iac-providers/terraform/v14/load-file.go +++ b/pkg/iac-providers/terraform/v14/load-file.go @@ -22,6 +22,6 @@ import ( ) // LoadIacFile parses the given terraform file from the given file path -func (*TfV14) LoadIacFile(absFilePath string) (allResourcesConfig output.AllResourceConfigs, err error) { +func (*TfV14) LoadIacFile(absFilePath string, options map[string]interface{}) (allResourcesConfig output.AllResourceConfigs, err error) { return commons.LoadIacFile(absFilePath) } diff --git a/pkg/iac-providers/terraform/v14/load-file_test.go b/pkg/iac-providers/terraform/v14/load-file_test.go index e872f223c..df34ef24e 100644 --- a/pkg/iac-providers/terraform/v14/load-file_test.go +++ b/pkg/iac-providers/terraform/v14/load-file_test.go @@ -47,6 +47,7 @@ func TestLoadIacFile(t *testing.T) { name string filePath string tfv14 TfV14 + options map[string]interface{} want output.AllResourceConfigs wantErr error }{ @@ -91,7 +92,7 @@ func TestLoadIacFile(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.tfv14.LoadIacFile(tt.filePath) + _, gotErr := tt.tfv14.LoadIacFile(tt.filePath, tt.options) if !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } @@ -102,6 +103,7 @@ func TestLoadIacFile(t *testing.T) { name string tfConfigFile string tfJSONFile string + options map[string]interface{} tfv14 TfV14 wantErr error }{ @@ -123,7 +125,7 @@ func TestLoadIacFile(t *testing.T) { for _, tt := range table2 { t.Run(tt.name, func(t *testing.T) { - got, gotErr := tt.tfv14.LoadIacFile(tt.tfConfigFile) + got, gotErr := tt.tfv14.LoadIacFile(tt.tfConfigFile, tt.options) if !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } diff --git a/pkg/iac-providers/terraform/v15/load-dir.go b/pkg/iac-providers/terraform/v15/load-dir.go index 3049f4b73..759e144ca 100644 --- a/pkg/iac-providers/terraform/v15/load-dir.go +++ b/pkg/iac-providers/terraform/v15/load-dir.go @@ -25,6 +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 (*TfV15) LoadIacDir(absRootDir string, nonRecursive bool) (allResourcesConfig output.AllResourceConfigs, err error) { - return commons.NewTerraformDirectoryLoader(absRootDir, nonRecursive).LoadIacDir() +func (*TfV15) LoadIacDir(absRootDir string, options map[string]interface{}) (allResourcesConfig output.AllResourceConfigs, err error) { + + return commons.NewTerraformDirectoryLoader(absRootDir, options).LoadIacDir() } diff --git a/pkg/iac-providers/terraform/v15/load-dir_test.go b/pkg/iac-providers/terraform/v15/load-dir_test.go index ba47c0bbc..e8c4bad2b 100644 --- a/pkg/iac-providers/terraform/v15/load-dir_test.go +++ b/pkg/iac-providers/terraform/v15/load-dir_test.go @@ -63,19 +63,21 @@ func TestLoadIacDir(t *testing.T) { } table := []struct { - name string - dirPath string - tfv15 TfV15 - want output.AllResourceConfigs - nonRecursive bool - wantErr error + name string + dirPath string + tfv15 TfV15 + want output.AllResourceConfigs + wantErr error + options map[string]interface{} }{ { - name: "invalid dirPath", - dirPath: testDirPath1, - tfv15: TfV15{}, - nonRecursive: true, - wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath1)), + name: "invalid dirPath", + dirPath: testDirPath1, + tfv15: TfV15{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath1)), }, { name: "invalid dirPath recursive", @@ -84,11 +86,13 @@ func TestLoadIacDir(t *testing.T) { wantErr: multierror.Append(pathErr), }, { - name: "empty config", - dirPath: testDirPath2, - tfv15: TfV15{}, - nonRecursive: true, - wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath2)), + name: "empty config", + dirPath: testDirPath2, + tfv15: TfV15{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath2)), }, { name: "empty config recursive", @@ -97,11 +101,13 @@ func TestLoadIacDir(t *testing.T) { wantErr: nilMultiErr, }, { - name: "incorrect module structure", - dirPath: filepath.Join(testDataDir, "invalid-moduleconfigs"), - tfv15: TfV15{}, - nonRecursive: true, - wantErr: multierror.Append(fmt.Errorf("failed to build terraform allResourcesConfig")), + name: "incorrect module structure", + dirPath: filepath.Join(testDataDir, "invalid-moduleconfigs"), + tfv15: TfV15{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: multierror.Append(fmt.Errorf("failed to build terraform allResourcesConfig")), }, { name: "incorrect module structure recursive", @@ -111,11 +117,13 @@ func TestLoadIacDir(t *testing.T) { wantErr: multierror.Append(fmt.Errorf(errStringInvalidModuleConfigs), fmt.Errorf(errStringInvalidModuleConfigs)), }, { - name: "load invalid config dir", - dirPath: testDataDir, - tfv15: TfV15{}, - nonRecursive: true, - wantErr: multierror.Append(fmt.Errorf(testErrorMessage)), + name: "load invalid config dir", + dirPath: testDataDir, + tfv15: TfV15{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: multierror.Append(fmt.Errorf(testErrorMessage)), }, { name: "load invalid config dir recursive", @@ -155,7 +163,7 @@ func TestLoadIacDir(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.tfv15.LoadIacDir(tt.dirPath, tt.nonRecursive) + _, gotErr := tt.tfv15.LoadIacDir(tt.dirPath, tt.options) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) @@ -175,20 +183,22 @@ func TestLoadIacDir(t *testing.T) { nestedModuleErr2 := fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "deep-modules", "modules", "m4", "modules")) table2 := []struct { - name string - tfConfigDir string - tfJSONFile string - tfv15 TfV15 - nonRecursive bool - wantErr error + name string + tfConfigDir string + tfJSONFile string + tfv15 TfV15 + wantErr error + options map[string]interface{} }{ { - name: "config1", - tfConfigDir: filepath.Join(testDataDir, "tfconfigs"), - tfJSONFile: filepath.Join(tfJSONDir, "fullconfig.json"), - tfv15: TfV15{}, - nonRecursive: true, - wantErr: nilMultiErr, + name: "config1", + tfConfigDir: filepath.Join(testDataDir, "tfconfigs"), + tfJSONFile: filepath.Join(tfJSONDir, "fullconfig.json"), + tfv15: TfV15{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: nilMultiErr, }, { name: "config1 recursive", @@ -199,12 +209,14 @@ func TestLoadIacDir(t *testing.T) { wantErr: nilMultiErr, }, { - name: "module directory", - tfConfigDir: filepath.Join(testDataDir, "moduleconfigs"), - tfJSONFile: filepath.Join(tfJSONDir, "moduleconfigs.json"), - tfv15: TfV15{}, - nonRecursive: true, - wantErr: nilMultiErr, + name: "module directory", + tfConfigDir: filepath.Join(testDataDir, "moduleconfigs"), + tfJSONFile: filepath.Join(tfJSONDir, "moduleconfigs.json"), + tfv15: TfV15{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: nilMultiErr, }, { name: "module directory recursive", @@ -215,12 +227,14 @@ func TestLoadIacDir(t *testing.T) { wantErr: nilMultiErr, }, { - name: "nested module directory", - tfConfigDir: filepath.Join(testDataDir, "deep-modules"), - tfJSONFile: filepath.Join(tfJSONDir, "deep-modules.json"), - tfv15: TfV15{}, - nonRecursive: true, - wantErr: nilMultiErr, + name: "nested module directory", + tfConfigDir: filepath.Join(testDataDir, "deep-modules"), + tfJSONFile: filepath.Join(tfJSONDir, "deep-modules.json"), + tfv15: TfV15{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: nilMultiErr, }, { name: "nested module directory recursive", @@ -230,34 +244,40 @@ func TestLoadIacDir(t *testing.T) { wantErr: multierror.Append(nestedModuleErr1, nestedModuleErr2), }, { - name: "complex variables", - tfConfigDir: filepath.Join(testDataDir, "complex-variables"), - tfJSONFile: filepath.Join(tfJSONDir, "complex-variables.json"), - tfv15: TfV15{}, - nonRecursive: true, - wantErr: nilMultiErr, + name: "complex variables", + tfConfigDir: filepath.Join(testDataDir, "complex-variables"), + tfJSONFile: filepath.Join(tfJSONDir, "complex-variables.json"), + tfv15: TfV15{}, + options: map[string]interface{}{ + "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"), - tfv15: TfV15{}, - 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"), + tfv15: TfV15{}, + options: map[string]interface{}{ + "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"), - tfv15: TfV15{}, - 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"), + tfv15: TfV15{}, + options: map[string]interface{}{ + "nonRecursive": true, + }, + wantErr: nilMultiErr, }, } for _, tt := range table2 { t.Run(tt.name, func(t *testing.T) { - got, gotErr := tt.tfv15.LoadIacDir(tt.tfConfigDir, tt.nonRecursive) + got, gotErr := tt.tfv15.LoadIacDir(tt.tfConfigDir, tt.options) me, ok := gotErr.(*multierror.Error) if !ok { t.Errorf("expected multierror.Error, got %T", gotErr) diff --git a/pkg/iac-providers/terraform/v15/load-file.go b/pkg/iac-providers/terraform/v15/load-file.go index f2ef8588a..345faebc8 100644 --- a/pkg/iac-providers/terraform/v15/load-file.go +++ b/pkg/iac-providers/terraform/v15/load-file.go @@ -22,6 +22,6 @@ import ( ) // LoadIacFile parses the given terraform file from the given file path -func (*TfV15) LoadIacFile(absFilePath string) (allResourcesConfig output.AllResourceConfigs, err error) { +func (*TfV15) LoadIacFile(absFilePath string, options map[string]interface{}) (allResourcesConfig output.AllResourceConfigs, err error) { return commons.LoadIacFile(absFilePath) } diff --git a/pkg/iac-providers/terraform/v15/load-file_test.go b/pkg/iac-providers/terraform/v15/load-file_test.go index d0cc358b8..126008435 100644 --- a/pkg/iac-providers/terraform/v15/load-file_test.go +++ b/pkg/iac-providers/terraform/v15/load-file_test.go @@ -46,6 +46,7 @@ func TestLoadIacFile(t *testing.T) { table := []struct { name string filePath string + options map[string]interface{} tfv15 TfV15 want output.AllResourceConfigs wantErr error @@ -101,7 +102,7 @@ func TestLoadIacFile(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.tfv15.LoadIacFile(tt.filePath) + _, gotErr := tt.tfv15.LoadIacFile(tt.filePath, tt.options) if !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } @@ -112,6 +113,7 @@ func TestLoadIacFile(t *testing.T) { name string tfConfigFile string tfJSONFile string + options map[string]interface{} tfv15 TfV15 wantErr error }{ @@ -133,7 +135,7 @@ func TestLoadIacFile(t *testing.T) { for _, tt := range table2 { t.Run(tt.name, func(t *testing.T) { - got, gotErr := tt.tfv15.LoadIacFile(tt.tfConfigFile) + got, gotErr := tt.tfv15.LoadIacFile(tt.tfConfigFile, tt.options) if !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } diff --git a/pkg/iac-providers/tfplan/v1/load-dir.go b/pkg/iac-providers/tfplan/v1/load-dir.go index 2b7e070f2..aaf2cf3e5 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, nonRecursive bool) (output.AllResourceConfigs, error) { +func (k *TFPlan) LoadIacDir(absRootDir string, options map[string]interface{}) (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 147a24e29..abd347a99 100644 --- a/pkg/iac-providers/tfplan/v1/load-dir_test.go +++ b/pkg/iac-providers/tfplan/v1/load-dir_test.go @@ -28,8 +28,9 @@ func TestLoadIacDir(t *testing.T) { dirPath = "some-path" tfplan = TFPlan{} wantErr = errIacDirNotSupport + options = make(map[string]interface{}) ) - _, err := tfplan.LoadIacDir(dirPath, false) + _, err := tfplan.LoadIacDir(dirPath, options) if !reflect.DeepEqual(wantErr, err) { t.Errorf("error want: '%v', got: '%v'", wantErr, err) } diff --git a/pkg/iac-providers/tfplan/v1/load-file.go b/pkg/iac-providers/tfplan/v1/load-file.go index 9ceceabcf..004172633 100644 --- a/pkg/iac-providers/tfplan/v1/load-file.go +++ b/pkg/iac-providers/tfplan/v1/load-file.go @@ -37,7 +37,7 @@ var ( ) // LoadIacFile parses the given tfplan file from the given file path -func (t *TFPlan) LoadIacFile(absFilePath string) (allResourcesConfig output.AllResourceConfigs, err error) { +func (t *TFPlan) LoadIacFile(absFilePath string, options map[string]interface{}) (allResourcesConfig output.AllResourceConfigs, err error) { zap.S().Debug("processing tfplan file") diff --git a/pkg/iac-providers/tfplan/v1/load-file_test.go b/pkg/iac-providers/tfplan/v1/load-file_test.go index ffb720626..43d109293 100644 --- a/pkg/iac-providers/tfplan/v1/load-file_test.go +++ b/pkg/iac-providers/tfplan/v1/load-file_test.go @@ -38,6 +38,7 @@ func TestLoadIacFile(t *testing.T) { table := []struct { name string filePath string + options map[string]interface{} tfplan TFPlan want output.AllResourceConfigs wantErr error @@ -70,13 +71,13 @@ func TestLoadIacFile(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - _, gotErr := tt.tfplan.LoadIacFile(tt.filePath) + _, gotErr := tt.tfplan.LoadIacFile(tt.filePath, tt.options) if !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) } }) } - + options := make(map[string]interface{}) t.Run("validate tfplan iac output", func(t *testing.T) { var ( tfplan = TFPlan{} @@ -85,7 +86,7 @@ func TestLoadIacFile(t *testing.T) { wantErr error = nil ) - got, err := tfplan.LoadIacFile(tfplanFile) + got, err := tfplan.LoadIacFile(tfplanFile, options) if !reflect.DeepEqual(err, wantErr) { 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 c2aac25ee..eb69b03d7 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -191,10 +191,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{}, "", false) + filePath, "", []string{testPoliciesPath}, []string{}, []string{}, []string{}, "", false, false) } else { executor, err = runtime.NewExecutor("k8s", "v1", []string{"k8s"}, - filePath, "", []string{}, []string{}, []string{}, []string{}, "", false) + filePath, "", []string{}, []string{}, []string{}, []string{}, "", false, false) } if err != nil { zap.S().Errorf("failed to create runtime executer: '%v'", err) diff --git a/pkg/policy/docker.go b/pkg/policy/docker.go new file mode 100644 index 000000000..1ec3eb207 --- /dev/null +++ b/pkg/policy/docker.go @@ -0,0 +1,28 @@ +/* + 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 policy + +const ( + docker supportedCloudType = "docker" + defaultDockerIacType supportedIacType = "docker" + defaultDockerIacVersion supportedIacVersion = version1 +) + +func init() { + // Register docker as a cloud provider with terrascan + RegisterCloudProvider(docker, defaultDockerIacType, defaultDockerIacVersion) +} diff --git a/pkg/runtime/executor.go b/pkg/runtime/executor.go index a730316bf..e33a95381 100644 --- a/pkg/runtime/executor.go +++ b/pkg/runtime/executor.go @@ -17,9 +17,10 @@ package runtime import ( - "github.com/accurics/terrascan/pkg/policy/opa" "sort" + "github.com/accurics/terrascan/pkg/policy/opa" + "go.uber.org/zap" "github.com/accurics/terrascan/pkg/filters" @@ -30,35 +31,42 @@ import ( "github.com/hashicorp/go-multierror" ) +const ( + useTerraformCache = "useTerraformCache" + nonRecursive = "nonRecursive" +) + // Executor object type Executor struct { - filePath string - dirPath string - policyPath []string - iacType string - iacVersion string - scanRules []string - skipRules []string - iacProviders []iacProvider.IacProvider - policyEngines []policy.Engine - notifiers []notifications.Notifier - categories []string - policyTypes []string - severity string - nonRecursive bool + filePath string + dirPath string + policyPath []string + iacType string + iacVersion string + scanRules []string + skipRules []string + iacProviders []iacProvider.IacProvider + policyEngines []policy.Engine + notifiers []notifications.Notifier + categories []string + policyTypes []string + severity string + nonRecursive bool + useTerraformCache bool } // NewExecutor creates a runtime object -func NewExecutor(iacType, iacVersion string, policyTypes []string, filePath, dirPath string, policyPath, scanRules, skipRules, categories []string, severity string, nonRecursive bool) (e *Executor, err error) { +func NewExecutor(iacType, iacVersion string, policyTypes []string, filePath, dirPath string, policyPath, scanRules, skipRules, categories []string, severity string, nonRecursive, useTerraformCache bool) (e *Executor, err error) { e = &Executor{ - filePath: filePath, - dirPath: dirPath, - policyPath: policyPath, - policyTypes: policyTypes, - iacType: iacType, - iacVersion: iacVersion, - iacProviders: make([]iacProvider.IacProvider, 0), - nonRecursive: nonRecursive, + filePath: filePath, + dirPath: dirPath, + policyPath: policyPath, + policyTypes: policyTypes, + iacType: iacType, + iacVersion: iacVersion, + iacProviders: make([]iacProvider.IacProvider, 0), + nonRecursive: nonRecursive, + useTerraformCache: useTerraformCache, } // read config file and update scan and skip rules @@ -159,6 +167,7 @@ func (e *Executor) initPolicyEngines() (err error) { // initialize the engine if err := engine.Init(policyPath, preloadFilter); err != nil { zap.S().Errorf("failed to initialize policy engine for path %s, error: %s", policyPath, err) + zap.S().Error("perform 'terrascan init' command and then try running the scan command again") return err } e.policyEngines = append(e.policyEngines, engine) @@ -178,9 +187,10 @@ func (e *Executor) Execute(configOnly bool) (results Output, err error) { // get all resource configs in the directory resourceConfig, merr = e.getResourceConfigs() } else { + options := e.buildOptions() // create results output from Iac provider // iac providers will contain one element - resourceConfig, err = e.iacProviders[0].LoadIacFile(e.filePath) + resourceConfig, err = e.iacProviders[0].LoadIacFile(e.filePath, options) if err != nil { return results, err } @@ -242,10 +252,11 @@ func (e *Executor) getResourceConfigs() (output.AllResourceConfigs, *multierror. // channel for directory scan response scanRespChan := make(chan dirScanResp) + options := e.buildOptions() // create results output from Iac provider[s] for _, iacP := range e.iacProviders { go func(ip iacProvider.IacProvider) { - rc, err := ip.LoadIacDir(e.dirPath, e.nonRecursive) + rc, err := ip.LoadIacDir(e.dirPath, options) scanRespChan <- dirScanResp{err, rc} }(iacP) } @@ -313,3 +324,11 @@ func implementsSubFolderScan(iacType string, nonRecursive bool) bool { } return false } + +// buildOptions builds map of scan options from executors config +func (e *Executor) buildOptions() map[string]interface{} { + options := make(map[string]interface{}) + options[useTerraformCache] = e.useTerraformCache + options[nonRecursive] = e.nonRecursive + return options +} diff --git a/pkg/runtime/executor_test.go b/pkg/runtime/executor_test.go index 4cc9cd6dd..dc1ba320e 100644 --- a/pkg/runtime/executor_test.go +++ b/pkg/runtime/executor_test.go @@ -59,11 +59,11 @@ type MockIacProvider struct { err error } -func (m MockIacProvider) LoadIacDir(dir string, nonRecursive bool) (output.AllResourceConfigs, error) { +func (m MockIacProvider) LoadIacDir(dir string, options map[string]interface{}) (output.AllResourceConfigs, error) { return m.output, m.err } -func (m MockIacProvider) LoadIacFile(file string) (output.AllResourceConfigs, error) { +func (m MockIacProvider) LoadIacFile(file string, options map[string]interface{}) (output.AllResourceConfigs, error) { return m.output, m.err } @@ -557,7 +557,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.policyTypes, tt.flags.filePath, tt.flags.dirPath, tt.flags.policyPath, tt.flags.scanRules, tt.flags.skipRules, tt.flags.categories, tt.flags.severity, false) + gotExecutor, gotErr := NewExecutor(tt.flags.iacType, tt.flags.iacVersion, tt.flags.policyTypes, tt.flags.filePath, tt.flags.dirPath, tt.flags.policyPath, tt.flags.scanRules, tt.flags.skipRules, tt.flags.categories, tt.flags.severity, false, false) if !reflect.DeepEqual(tt.wantErr, gotErr) { t.Errorf("Mismatch in error => got: '%v', want: '%v'", gotErr, tt.wantErr) diff --git a/test/e2e/help/golden/help_scan.txt b/test/e2e/help/golden/help_scan.txt index 7c2d00d55..8b444d480 100644 --- a/test/e2e/help/golden/help_scan.txt +++ b/test/e2e/help/golden/help_scan.txt @@ -15,7 +15,7 @@ Flags: --iac-version string iac version (arm: v1, cft: v1, docker: v1, helm: v3, k8s: v1, kustomize: v3, terraform: v12, v13, v14, v15, 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]) + -t, --policy-type strings policy type (all, aws, azure, docker, gcp, github, k8s) (default [all]) -r, --remote-type string type of remote backend (git, s3, gcs, http, terraform-registry) -u, --remote-url string url pointing to remote IaC repository --scan-rules strings one or more rules to scan (example: --scan-rules="ruleID1,ruleID2") @@ -23,6 +23,7 @@ Flags: --show-passed display passed rules, along with violations --skip-rules strings one or more rules to skip while scanning (example: --skip-rules="ruleID1,ruleID2") --use-colors string color output (auto, t, f) (default "auto") + --use-terraform-cache use terraform init cache for remote modules (when used directory scan will be non recursive, flag applicable only with terraform IaC provider) -v, --verbose will show violations with details (applicable for default output) Global Flags: diff --git a/test/e2e/scan/golden/terraform_scans/terraform_cache_use_in_scan/terraform_cache_use_in_scan_result.txt b/test/e2e/scan/golden/terraform_scans/terraform_cache_use_in_scan/terraform_cache_use_in_scan_result.txt new file mode 100644 index 000000000..660333c01 --- /dev/null +++ b/test/e2e/scan/golden/terraform_scans/terraform_cache_use_in_scan/terraform_cache_use_in_scan_result.txt @@ -0,0 +1,16 @@ +{ + "results": { + "violations": null, + "skipped_violations": null, + "scan_summary": { + "file/folder": "/Users/suvarna/go/src/github.com/rchanger/terrascan/test/e2e/test_data/iac/terraform_cache_use_in_scan", + "iac_type": "terraform", + "scanned_at": "2021-07-22 16:06:39.189999 +0000 UTC", + "policies_validated": 8, + "violated_policies": 0, + "low": 0, + "medium": 0, + "high": 0 + } + } +} \ No newline at end of file diff --git a/test/e2e/scan/scan_tf_files_test.go b/test/e2e/scan/scan_tf_files_test.go index 1eba8579e..8589670cd 100644 --- a/test/e2e/scan/scan_tf_files_test.go +++ b/test/e2e/scan/scan_tf_files_test.go @@ -17,9 +17,10 @@ package scan_test import ( - "github.com/accurics/terrascan/pkg/version" "path/filepath" + "github.com/accurics/terrascan/pkg/version" + scanUtils "github.com/accurics/terrascan/test/e2e/scan" "github.com/accurics/terrascan/test/helper" . "github.com/onsi/ginkgo" @@ -224,5 +225,14 @@ var _ = Describe("Scan is run for terraform files", func() { }) }) }) + Context("when --use-terraform-cache flag is used, all remote modules are refered from terraform cache", func() { + When("when --use-terraform-cache is set with output format json", func() { + It("should not display any violations", func() { + iacDir := filepath.Join(iacRootRelPath, "terraform_cache_use_in_scan") + scanArgs := []string{"-i", "terraform", "-p", policyDir, "-d", iacDir, "-o", "json", "--use-terraform-cache"} + scanUtils.RunScanAndAssertGoldenOutputRegex(terrascanBinaryPath, filepath.Join(tfGoldenRelPath, "terraform_cache_use_in_scan", "terraform_cache_use_in_scan_result.txt"), helper.ExitCodeZero, false, true, outWriter, errWriter, scanArgs...) + }) + }) + }) }) }) diff --git a/test/e2e/test_data/iac/terraform_cache_use_in_scan/.terraform/modules/modules.json b/test/e2e/test_data/iac/terraform_cache_use_in_scan/.terraform/modules/modules.json new file mode 100644 index 000000000..3f3723ed3 --- /dev/null +++ b/test/e2e/test_data/iac/terraform_cache_use_in_scan/.terraform/modules/modules.json @@ -0,0 +1,15 @@ +{ + "Modules": [ + { + "Key": "", + "Source": "", + "Dir": "." + }, + { + "Key": "network", + "Source": "Azure/network/azurerm", + "Version": "3.2.1", + "Dir": ".terraform/modules/network" + } + ] +} \ No newline at end of file diff --git a/test/e2e/test_data/iac/terraform_cache_use_in_scan/.terraform/modules/network/main.tf b/test/e2e/test_data/iac/terraform_cache_use_in_scan/.terraform/modules/network/main.tf new file mode 100644 index 000000000..780f2118c --- /dev/null +++ b/test/e2e/test_data/iac/terraform_cache_use_in_scan/.terraform/modules/network/main.tf @@ -0,0 +1,21 @@ +#Azure Generic vNet Module +data "azurerm_resource_group" "network" { + name = var.resource_group_name +} + +resource "azurerm_virtual_network" "vnet" { + name = var.vnet_name + resource_group_name = data.azurerm_resource_group.network.name + location = data.azurerm_resource_group.network.location + address_space = [var.address_space] + dns_servers = var.dns_servers + tags = var.tags +} + +resource "azurerm_subnet" "subnet" { + count = length(var.subnet_names) + name = var.subnet_names[count.index] + resource_group_name = data.azurerm_resource_group.network.name + address_prefixes = [var.subnet_prefixes[count.index]] + virtual_network_name = azurerm_virtual_network.vnet.name +} diff --git a/test/e2e/test_data/iac/terraform_cache_use_in_scan/.terraform/modules/network/outputs.tf b/test/e2e/test_data/iac/terraform_cache_use_in_scan/.terraform/modules/network/outputs.tf new file mode 100644 index 000000000..4534e0215 --- /dev/null +++ b/test/e2e/test_data/iac/terraform_cache_use_in_scan/.terraform/modules/network/outputs.tf @@ -0,0 +1,24 @@ +output "vnet_id" { + description = "The id of the newly created vNet" + value = azurerm_virtual_network.vnet.id +} + +output "vnet_name" { + description = "The Name of the newly created vNet" + value = azurerm_virtual_network.vnet.name +} + +output "vnet_location" { + description = "The location of the newly created vNet" + value = azurerm_virtual_network.vnet.location +} + +output "vnet_address_space" { + description = "The address space of the newly created vNet" + value = azurerm_virtual_network.vnet.address_space +} + +output "vnet_subnets" { + description = "The ids of subnets created inside the newl vNet" + value = azurerm_subnet.subnet.*.id +} diff --git a/test/e2e/test_data/iac/terraform_cache_use_in_scan/.terraform/modules/network/variables.tf b/test/e2e/test_data/iac/terraform_cache_use_in_scan/.terraform/modules/network/variables.tf new file mode 100644 index 000000000..3a964abf9 --- /dev/null +++ b/test/e2e/test_data/iac/terraform_cache_use_in_scan/.terraform/modules/network/variables.tf @@ -0,0 +1,44 @@ +variable "vnet_name" { + description = "Name of the vnet to create." + type = string + default = "acctvnet" +} + +variable "resource_group_name" { + description = "The name of an existing resource group to be imported." + type = string +} + +variable "address_space" { + description = "The address space that is used by the virtual network." + type = string + default = "10.0.0.0/16" +} + +# If no values specified, this defaults to Azure DNS +variable "dns_servers" { + description = "The DNS servers to be used with vNet." + type = list(string) + default = [] +} + +variable "subnet_prefixes" { + description = "The address prefix to use for the subnet." + type = list(string) + default = ["10.0.1.0/24"] +} + +variable "subnet_names" { + description = "A list of public subnets inside the vNet." + type = list(string) + default = ["subnet1"] +} + +variable "tags" { + description = "The tags to associate with your network and subnets." + type = map(string) + + default = { + environment = "dev" + } +} diff --git a/test/e2e/test_data/iac/terraform_cache_use_in_scan/remote-module.tf b/test/e2e/test_data/iac/terraform_cache_use_in_scan/remote-module.tf new file mode 100644 index 000000000..fb88de2fd --- /dev/null +++ b/test/e2e/test_data/iac/terraform_cache_use_in_scan/remote-module.tf @@ -0,0 +1,9 @@ +#file added for fix for #418 + +# source https://registry.terraform.io/ + +module "network" { + source = "Azure/network/azurerm" + version = "3.2.1" +} +