diff --git a/pkg/runtime/executor.go b/pkg/runtime/executor.go index a2edaf75e..cd8f74645 100644 --- a/pkg/runtime/executor.go +++ b/pkg/runtime/executor.go @@ -1,131 +1,91 @@ package runtime import ( - "fmt" "os" "github.com/accurics/terrascan/pkg/utils" "go.uber.org/zap" - CloudProvider "github.com/accurics/terrascan/pkg/cloud-providers" - IacProvider "github.com/accurics/terrascan/pkg/iac-providers" + cloudProvider "github.com/accurics/terrascan/pkg/cloud-providers" + iacProvider "github.com/accurics/terrascan/pkg/iac-providers" "github.com/accurics/terrascan/pkg/iac-providers/output" ) // Executor object type Executor struct { - filePath string - dirPath string - cloudType string - iacType string - iacVersion string + filePath string + dirPath string + cloudType string + iacType string + iacVersion string + iacProvider iacProvider.IacProvider + cloudProvider cloudProvider.CloudProvider } // NewExecutor creates a runtime object -func NewExecutor(iacType, iacVersion, cloudType, filePath, dirPath string) *Executor { - return &Executor{ +func NewExecutor(iacType, iacVersion, cloudType, filePath, dirPath string) (e *Executor, err error) { + e = &Executor{ filePath: filePath, dirPath: dirPath, cloudType: cloudType, iacType: iacType, iacVersion: iacVersion, } -} - -// ValidateInputs validates the inputs to the executor object -func (r *Executor) ValidateInputs() error { - - // error message - errMsg := "input validation failed" - - // terrascan can accept either a file or a directory, both inputs cannot - // be processed together - if r.filePath != "" && r.dirPath != "" { - zap.S().Errorf("cannot accept both '-f %s' and '-d %s' options together", r.filePath, r.dirPath) - return fmt.Errorf(errMsg) - } - - if r.dirPath != "" { - // if directory, check if directory exists - absDirPath, err := utils.GetAbsPath(r.dirPath) - if err != nil { - return err - } - - if _, err := os.Stat(absDirPath); err != nil { - zap.S().Errorf("directory '%s' does not exist", absDirPath) - return fmt.Errorf(errMsg) - } - zap.S().Debugf("directory '%s' exists", absDirPath) - } else { - // if file path, check if file exists - absFilePath, err := utils.GetAbsPath(r.filePath) - if err != nil { - return fmt.Errorf(errMsg) - } - - if _, err := os.Stat(absFilePath); err != nil { - zap.S().Errorf("file '%s' does not exist", absFilePath) - return fmt.Errorf(errMsg) - } - zap.S().Debugf("file '%s' exists", absFilePath) + // initialized executor + if err = e.Init(); err != nil { + return e, err } - // check if Iac type is supported - if !IacProvider.IsIacSupported(r.iacType, r.iacVersion) { - zap.S().Errorf("iac type '%s', version '%s' not supported", r.iacType, r.iacVersion) - return fmt.Errorf(errMsg) - } - zap.S().Debugf("iac type '%s', version '%s' is supported", r.iacType, r.iacVersion) - - // check if cloud type is supported - if !CloudProvider.IsCloudSupported(r.cloudType) { - zap.S().Errorf("cloud type '%s' not supported", r.cloudType) - return fmt.Errorf(errMsg) - } - zap.S().Debugf("cloud type '%s' supported", r.cloudType) - - // check if policy type is supported - - // successful - zap.S().Debug("input validation successful") - return nil + return e, nil } -// Execute validates the inputs, processes the IaC, creates json output -func (r *Executor) Execute() error { +// Init validates input and initializes iac and cloud providers +func (e *Executor) Init() error { // validate inputs - if err := r.ValidateInputs(); err != nil { + err := e.ValidateInputs() + if err != nil { return err } // create new IacProvider - iacProvider, err := IacProvider.NewIacProvider(r.iacType, r.iacVersion) + e.iacProvider, err = iacProvider.NewIacProvider(e.iacType, e.iacVersion) if err != nil { - zap.S().Errorf("failed to create a new IacProvider for iacType '%s'. error: '%s'", r.iacType, err) + zap.S().Errorf("failed to create a new IacProvider for iacType '%s'. error: '%s'", e.iacType, err) return err } - var iacOut output.AllResourceConfigs - if r.dirPath != "" { - iacOut, err = iacProvider.LoadIacDir(r.dirPath) - } else { - // create config from IaC - iacOut, err = iacProvider.LoadIacFile(r.filePath) - } + // create new CloudProvider + e.cloudProvider, err = cloudProvider.NewCloudProvider(e.cloudType) if err != nil { + zap.S().Errorf("failed to create a new CloudProvider for cloudType '%s'. error: '%s'", e.cloudType, err) return err } - // create new CloudProvider - cloudProvider, err := CloudProvider.NewCloudProvider(r.cloudType) + return nil +} + +// Execute validates the inputs, processes the IaC, creates json output +func (e *Executor) Execute() error { + + // load iac config + var ( + iacOut output.AllResourceConfigs + err error + ) + if e.dirPath != "" { + iacOut, err = e.iacProvider.LoadIacDir(e.dirPath) + } else { + // create config from IaC + iacOut, err = e.iacProvider.LoadIacFile(e.filePath) + } if err != nil { - zap.S().Errorf("failed to create a new CloudProvider for cloudType '%s'. error: '%s'", r.cloudType, err) return err } - normalized, err := cloudProvider.CreateNormalizedJSON(iacOut) + + // create normalized json + normalized, err := e.cloudProvider.CreateNormalizedJSON(iacOut) if err != nil { return err } diff --git a/pkg/runtime/executor_test.go b/pkg/runtime/executor_test.go new file mode 100644 index 000000000..e72f2f585 --- /dev/null +++ b/pkg/runtime/executor_test.go @@ -0,0 +1,133 @@ +package runtime + +import ( + "fmt" + "reflect" + "testing" + + cloudProvider "github.com/accurics/terrascan/pkg/cloud-providers" + awsProvider "github.com/accurics/terrascan/pkg/cloud-providers/aws" + iacProvider "github.com/accurics/terrascan/pkg/iac-providers" + "github.com/accurics/terrascan/pkg/iac-providers/output" + tfv12 "github.com/accurics/terrascan/pkg/iac-providers/terraform/v12" +) + +var ( + errMockLoadIacDir = fmt.Errorf("mock LoadIacDir") + errMockLoadIacFile = fmt.Errorf("mock LoadIacFile") + errMockCreateNormalizedJSON = fmt.Errorf("mock CreateNormalizedJSON") +) + +// MockIacProvider mocks IacProvider interface +type MockIacProvider struct { + output output.AllResourceConfigs + err error +} + +func (m MockIacProvider) LoadIacDir(dir string) (output.AllResourceConfigs, error) { + return m.output, m.err +} + +func (m MockIacProvider) LoadIacFile(file string) (output.AllResourceConfigs, error) { + return m.output, m.err +} + +// MockCloudProvider mocks CloudProvider interface +type MockCloudProvider struct { + err error +} + +func (m MockCloudProvider) CreateNormalizedJSON(data output.AllResourceConfigs) (mockInterface interface{}, err error) { + return data, m.err +} + +func TestExecute(t *testing.T) { + + table := []struct { + name string + executor Executor + wantErr error + }{ + { + name: "test LoadIacDir", + executor: Executor{ + dirPath: "./testdata/testdir", + iacProvider: MockIacProvider{err: errMockLoadIacDir}, + }, + wantErr: errMockLoadIacDir, + }, + { + name: "test LoadIacFile", + executor: Executor{ + filePath: "./testdata/testfile", + iacProvider: MockIacProvider{err: errMockLoadIacFile}, + }, + wantErr: errMockLoadIacFile, + }, + { + name: "test CreateNormalizedJSON error", + executor: Executor{ + filePath: "./testdata/testfile", + iacProvider: MockIacProvider{err: nil}, + cloudProvider: MockCloudProvider{err: errMockCreateNormalizedJSON}, + }, + wantErr: errMockCreateNormalizedJSON, + }, + { + name: "test CreateNormalizedJSON", + executor: Executor{ + filePath: "./testdata/testfile", + iacProvider: MockIacProvider{err: nil}, + cloudProvider: MockCloudProvider{err: nil}, + }, + wantErr: nil, + }, + } + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + gotErr := tt.executor.Execute() + if !reflect.DeepEqual(gotErr, tt.wantErr) { + t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) + } + }) + } +} + +func TestInit(t *testing.T) { + + table := []struct { + name string + executor Executor + wantErr error + wantIacProvider iacProvider.IacProvider + wantCloudProvider cloudProvider.CloudProvider + }{ + { + name: "valid filePath", + executor: Executor{ + filePath: "./testdata/testfile", + dirPath: "", + cloudType: "aws", + iacType: "terraform", + iacVersion: "v12", + }, + wantErr: nil, + wantIacProvider: &tfv12.TfV12{}, + wantCloudProvider: &awsProvider.AWSProvider{}, + }, + } + + for _, tt := range table { + gotErr := tt.executor.Init() + if !reflect.DeepEqual(gotErr, tt.wantErr) { + t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) + } + if !reflect.DeepEqual(tt.executor.iacProvider, tt.wantIacProvider) { + t.Errorf("got: '%v', want: '%v'", tt.executor.iacProvider, tt.wantIacProvider) + } + if !reflect.DeepEqual(tt.executor.cloudProvider, tt.wantCloudProvider) { + t.Errorf("got: '%v', want: '%v'", tt.executor.cloudProvider, tt.wantCloudProvider) + } + } +} diff --git a/pkg/runtime/testdata/testfile b/pkg/runtime/testdata/testfile new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/runtime/validate.go b/pkg/runtime/validate.go new file mode 100644 index 000000000..49a185b78 --- /dev/null +++ b/pkg/runtime/validate.go @@ -0,0 +1,82 @@ +package runtime + +import ( + "fmt" + "os" + + "github.com/accurics/terrascan/pkg/utils" + "go.uber.org/zap" + + CloudProvider "github.com/accurics/terrascan/pkg/cloud-providers" + IacProvider "github.com/accurics/terrascan/pkg/iac-providers" +) + +var ( + errEmptyIacPath = fmt.Errorf("empty iac path, either use '-f' or '-d' option") + errIncorrectIacPath = fmt.Errorf("cannot accept both '-f' and '-d' options together") + errDirNotExists = fmt.Errorf("directory does not exist") + errFileNotExists = fmt.Errorf("file does not exist") + errIacNotSupported = fmt.Errorf("iac type or version not supported") + errCloudNotSupported = fmt.Errorf("cloud type not supported") +) + +// ValidateInputs validates the inputs to the executor object +func (e *Executor) ValidateInputs() error { + + // terrascan can accept either a file or a directory + if e.filePath == "" && e.dirPath == "" { + zap.S().Errorf("no IaC path specified; use '-f' for file or '-d' for directory") + return errEmptyIacPath + } + if e.filePath != "" && e.dirPath != "" { + zap.S().Errorf("cannot accept both '-f %s' and '-d %s' options together", e.filePath, e.dirPath) + return errIncorrectIacPath + } + + if e.dirPath != "" { + // if directory, check if directory exists + absDirPath, err := utils.GetAbsPath(e.dirPath) + if err != nil { + return err + } + + if _, err := os.Stat(absDirPath); err != nil { + zap.S().Errorf("directory '%s' does not exist", absDirPath) + return errDirNotExists + } + zap.S().Debugf("directory '%s' exists", absDirPath) + } else { + + // if file path, check if file exists + absFilePath, err := utils.GetAbsPath(e.filePath) + if err != nil { + return err + } + + if _, err := os.Stat(absFilePath); err != nil { + zap.S().Errorf("file '%s' does not exist", absFilePath) + return errFileNotExists + } + zap.S().Debugf("file '%s' exists", absFilePath) + } + + // check if Iac type is supported + if !IacProvider.IsIacSupported(e.iacType, e.iacVersion) { + zap.S().Errorf("iac type '%s', version '%s' not supported", e.iacType, e.iacVersion) + return errIacNotSupported + } + zap.S().Debugf("iac type '%s', version '%s' is supported", e.iacType, e.iacVersion) + + // check if cloud type is supported + if !CloudProvider.IsCloudSupported(e.cloudType) { + zap.S().Errorf("cloud type '%s' not supported", e.cloudType) + return errCloudNotSupported + } + zap.S().Debugf("cloud type '%s' supported", e.cloudType) + + // check if policy type is supported + + // successful + zap.S().Debug("input validation successful") + return nil +} diff --git a/pkg/runtime/validate_test.go b/pkg/runtime/validate_test.go new file mode 100644 index 000000000..02091c651 --- /dev/null +++ b/pkg/runtime/validate_test.go @@ -0,0 +1,110 @@ +package runtime + +import ( + "reflect" + "testing" +) + +func TestValidateInputs(t *testing.T) { + + table := []struct { + name string + executor Executor + wantErr error + }{ + { + name: "valid filePath", + executor: Executor{ + filePath: "./testdata/testfile", + dirPath: "", + cloudType: "aws", + iacType: "terraform", + iacVersion: "v12", + }, + wantErr: nil, + }, + { + name: "valid dirPath", + executor: Executor{ + filePath: "", + dirPath: "./testdata/testdir", + cloudType: "aws", + iacType: "terraform", + iacVersion: "v12", + }, + wantErr: nil, + }, + { + name: "empty iac path", + executor: Executor{ + filePath: "", + dirPath: "", + }, + wantErr: errEmptyIacPath, + }, + { + name: "incorrect iac path", + executor: Executor{ + filePath: "./testdata/testfile", + dirPath: "./testdata/testdir", + }, + wantErr: errIncorrectIacPath, + }, + { + name: "filepath does not exist", + executor: Executor{ + filePath: "./testdata/notthere", + }, + wantErr: errFileNotExists, + }, + { + name: "directory does not exist", + executor: Executor{ + dirPath: "./testdata/notthere", + }, + wantErr: errDirNotExists, + }, + { + name: "invalid cloud", + executor: Executor{ + filePath: "", + dirPath: "./testdata/testdir", + cloudType: "nothere", + iacType: "terraform", + iacVersion: "v12", + }, + wantErr: errCloudNotSupported, + }, + { + name: "invalid iac type", + executor: Executor{ + filePath: "", + dirPath: "./testdata/testdir", + cloudType: "aws", + iacType: "notthere", + iacVersion: "v12", + }, + wantErr: errIacNotSupported, + }, + { + name: "invalid iac version", + executor: Executor{ + filePath: "", + dirPath: "./testdata/testdir", + cloudType: "aws", + iacType: "terraform", + iacVersion: "notthere", + }, + wantErr: errIacNotSupported, + }, + } + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + gotErr := tt.executor.ValidateInputs() + if !reflect.DeepEqual(gotErr, tt.wantErr) { + t.Errorf("unexpected error, gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr) + } + }) + } +}