Repository: tenable/terrascan (language: python)

Issue:
terrascan init should not be triggered if the user only wants to generate normalised json.
### Description

I have observed that even if a user just wants to generate normalized json using the `--config-only` flag (NO POLICIES REQUIRED), we force a `terrascan init` if the policy repo is not cached locally. It should not be the case.
terrascan init should not be triggered if the user only wants to generate normalised json.
### Description

I have observed that even if a user just wants to generate normalized json using the `--config-only` flag (NO POLICIES REQUIRED), we force a `terrascan init` if the policy repo is not cached locally. It should not be the case.


Discussion / hints:

Fix patch:
diff --git a/pkg/cli/run.go b/pkg/cli/run.go
index bc04b03a0..3773b5ed5 100644
--- a/pkg/cli/run.go
+++ b/pkg/cli/run.go
@@ -185,7 +185,7 @@ func (s *ScanOptions) Run() error {
 	}
 
 	// executor output
-	results, err := executor.Execute()
+	results, err := executor.Execute(s.configOnly)
 	if err != nil {
 		return err
 	}
@@ -202,7 +202,7 @@ func (s *ScanOptions) Run() error {
 		return err
 	}
 
-	if results.Violations.ViolationStore.Summary.ViolatedPolicies != 0 && flag.Lookup("test.v") == nil {
+	if !s.configOnly && results.Violations.ViolationStore.Summary.ViolatedPolicies != 0 && flag.Lookup("test.v") == nil {
 		os.RemoveAll(tempDir)
 		os.Exit(3)
 	}
@@ -226,6 +226,13 @@ func (s *ScanOptions) downloadRemoteRepository(tempDir string) error {
 }
 
 func (s ScanOptions) writeResults(results runtime.Output) error {
+
+	outputWriter := NewOutputWriter(s.UseColors)
+
+	if s.configOnly {
+		return writer.Write(s.outputType, results.ResourceConfig, outputWriter)
+	}
+
 	// add verbose flag to the scan summary
 	results.Violations.ViolationStore.Summary.ShowViolationDetails = s.verbose
 
@@ -233,10 +240,5 @@ func (s ScanOptions) writeResults(results runtime.Output) error {
 		results.Violations.ViolationStore.PassedRules = nil
 	}
 
-	outputWriter := NewOutputWriter(s.UseColors)
-
-	if s.configOnly {
-		return writer.Write(s.outputType, results.ResourceConfig, outputWriter)
-	}
 	return writer.Write(s.outputType, results.Violations, outputWriter)
 }
diff --git a/pkg/cli/scan.go b/pkg/cli/scan.go
index 70fb5edd5..a367312de 100644
--- a/pkg/cli/scan.go
+++ b/pkg/cli/scan.go
@@ -36,6 +36,9 @@ var scanCmd = &cobra.Command{
 Detect compliance and security violations across Infrastructure as Code to mitigate risk before provisioning cloud native infrastructure.
 `,
 	PreRunE: func(cmd *cobra.Command, args []string) error {
+		if scanOptions.configOnly {
+			return nil
+		}
 		return initial(cmd, args, true)
 	},
 	RunE:          scan,
diff --git a/pkg/http-server/file-scan.go b/pkg/http-server/file-scan.go
index 3e041e04a..92b4218fb 100644
--- a/pkg/http-server/file-scan.go
+++ b/pkg/http-server/file-scan.go
@@ -162,7 +162,7 @@ func (g *APIHandler) scanFile(w http.ResponseWriter, r *http.Request) {
 		apiErrorResponse(w, err.Error(), http.StatusBadRequest)
 		return
 	}
-	normalized, err := executor.Execute()
+	normalized, err := executor.Execute(configOnly)
 	if err != nil {
 		errMsg := fmt.Sprintf("failed to scan uploaded file. error: '%v'", err)
 		zap.S().Error(errMsg)
@@ -172,14 +172,13 @@ func (g *APIHandler) scanFile(w http.ResponseWriter, r *http.Request) {
 
 	var output interface{}
 
-	if !showPassed {
-		normalized.Violations.ViolationStore.PassedRules = nil
-	}
-
 	// if config only, return resource config else return violations
 	if configOnly {
 		output = normalized.ResourceConfig
 	} else {
+		if !showPassed {
+			normalized.Violations.ViolationStore.PassedRules = nil
+		}
 		output = normalized.Violations
 	}
 
diff --git a/pkg/http-server/remote-repo.go b/pkg/http-server/remote-repo.go
index 7157616ca..67786b6c8 100644
--- a/pkg/http-server/remote-repo.go
+++ b/pkg/http-server/remote-repo.go
@@ -135,25 +135,26 @@ func (s *scanRemoteRepoReq) ScanRemoteRepo(iacType, iacVersion string, cloudType
 	}
 
 	// evaluate policies IaC for violations
-	results, err := executor.Execute()
+	results, err := executor.Execute(s.ConfigOnly)
 	if err != nil {
 		errMsg := fmt.Sprintf("failed to scan uploaded file. error: '%v'", err)
 		zap.S().Error(errMsg)
 		return output, isAdmissionDenied, err
 	}
-	// set remote url in case remote repo is scanned
-	if s.RemoteURL != "" {
-		results.Violations.Summary.ResourcePath = s.RemoteURL
-	}
-
-	if !s.ShowPassed {
-		results.Violations.ViolationStore.PassedRules = nil
-	}
 
 	// if config only, return only config else return only violations
 	if s.ConfigOnly {
 		output = results.ResourceConfig
 	} else {
+		// set remote url in case remote repo is scanned
+		if s.RemoteURL != "" {
+			results.Violations.Summary.ResourcePath = s.RemoteURL
+		}
+
+		if !s.ShowPassed {
+			results.Violations.ViolationStore.PassedRules = nil
+		}
+
 		isAdmissionDenied = hasK8sAdmissionDeniedViolations(results)
 		output = results.Violations
 	}
diff --git a/pkg/initialize/run.go b/pkg/initialize/run.go
index 7302cadc1..3f9d197d4 100644
--- a/pkg/initialize/run.go
+++ b/pkg/initialize/run.go
@@ -36,8 +36,6 @@ const terrascanReadmeURL string = "https://raw.githubusercontent.com/accurics/te
 
 // Run initializes terrascan if not done already
 func Run(isNonInitCmd bool) error {
-	zap.S().Debug("initializing terrascan")
-
 	// check if policy paths exist
 	if path, err := os.Stat(config.GetPolicyRepoPath()); err == nil && path.IsDir() {
 		if isNonInitCmd {
@@ -45,6 +43,8 @@ func Run(isNonInitCmd bool) error {
 		}
 	}
 
+	zap.S().Debug("initializing terrascan")
+
 	if !connected(terrascanReadmeURL) {
 		return errNoConnection
 	}
diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go
index 4572fbc43..c2aac25ee 100644
--- a/pkg/k8s/admission-webhook/validating-webhook.go
+++ b/pkg/k8s/admission-webhook/validating-webhook.go
@@ -201,7 +201,7 @@ func (w ValidatingWebhook) scanK8sFile(filePath string) (runtime.Output, error)
 		return result, err
 	}
 
-	result, err = executor.Execute()
+	result, err = executor.Execute(false)
 	if err != nil {
 		zap.S().Error("failed to scan resource object. error: '%v'", err)
 		return result, err
diff --git a/pkg/runtime/executor.go b/pkg/runtime/executor.go
index 0f3c88257..bcb985426 100644
--- a/pkg/runtime/executor.go
+++ b/pkg/runtime/executor.go
@@ -17,6 +17,7 @@
 package runtime
 
 import (
+	"github.com/accurics/terrascan/pkg/policy/opa"
 	"sort"
 
 	"go.uber.org/zap"
@@ -26,7 +27,6 @@ import (
 	"github.com/accurics/terrascan/pkg/iac-providers/output"
 	"github.com/accurics/terrascan/pkg/notifications"
 	"github.com/accurics/terrascan/pkg/policy"
-	opa "github.com/accurics/terrascan/pkg/policy/opa"
 	"github.com/hashicorp/go-multierror"
 )
 
@@ -139,6 +139,11 @@ func (e *Executor) Init() error {
 		}
 	}
 
+	zap.S().Debug("initialized executor")
+	return nil
+}
+
+func (e *Executor) initPolicyEngines() (err error) {
 	// create a new policy engine based on IaC type
 	zap.S().Debugf("using policy path %v", e.policyPath)
 	for _, policyPath := range e.policyPath {
@@ -153,18 +158,16 @@ func (e *Executor) Init() error {
 
 		// initialize the engine
 		if err := engine.Init(policyPath, preloadFilter); err != nil {
-			zap.S().Errorf("%s", err)
+			zap.S().Errorf("failed to initialize policy engine for path %s, error: %s", policyPath, err)
 			return err
 		}
 		e.policyEngines = append(e.policyEngines, engine)
 	}
-
-	zap.S().Debug("initialized executor")
 	return nil
 }
 
 // Execute validates the inputs, processes the IaC, creates json output
-func (e *Executor) Execute() (results Output, err error) {
+func (e *Executor) Execute(configOnly bool) (results Output, err error) {
 
 	var merr *multierror.Error
 	var resourceConfig output.AllResourceConfigs
@@ -194,6 +197,14 @@ func (e *Executor) Execute() (results Output, err error) {
 	// update results with resource config
 	results.ResourceConfig = resourceConfig
 
+	if configOnly {
+		return results, nil
+	}
+
+	if err := e.initPolicyEngines(); err != nil {
+		return results, err
+	}
+
 	if err := e.findViolations(&results); err != nil {
 		return results, err
 	}


Test patch:
diff --git a/pkg/runtime/executor_test.go b/pkg/runtime/executor_test.go
index 41bbe5790..71018c1f9 100644
--- a/pkg/runtime/executor_test.go
+++ b/pkg/runtime/executor_test.go
@@ -178,7 +178,49 @@ func TestExecute(t *testing.T) {
 
 	for _, tt := range table {
 		t.Run(tt.name, func(t *testing.T) {
-			_, gotErr := tt.executor.Execute()
+			_, gotErr := tt.executor.Execute(false)
+			if !reflect.DeepEqual(gotErr, tt.wantErr) {
+				t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestInitPolicyEngine(t *testing.T) {
+	table := []struct {
+		name     string
+		executor Executor
+		wantErr  error
+	}{
+		{
+			name: "invalid policy path",
+			executor: Executor{
+				filePath:    filepath.Join(testDataDir, "testfile"),
+				dirPath:     "",
+				policyTypes: []string{"aws"},
+				iacType:     "terraform",
+				iacVersion:  "v14",
+				policyPath:  []string{filepath.Join(testDataDir, "notthere")},
+			},
+			wantErr: fmt.Errorf("failed to initialize OPA policy engine"),
+		},
+		{
+			name: "invalid policy path",
+			executor: Executor{
+				filePath:    filepath.Join(testDataDir, "testfile"),
+				dirPath:     "",
+				policyTypes: []string{"aws"},
+				iacType:     "terraform",
+				iacVersion:  "v12",
+				policyPath:  []string{filepath.Join(testDataDir, "notthere")},
+			},
+			wantErr: fmt.Errorf("failed to initialize OPA policy engine"),
+		},
+	}
+
+	for _, tt := range table {
+		t.Run(tt.name, func(t *testing.T) {
+			gotErr := tt.executor.initPolicyEngines()
 			if !reflect.DeepEqual(gotErr, tt.wantErr) {
 				t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr)
 			}
@@ -273,21 +315,6 @@ func TestInit(t *testing.T) {
 			wantErr:         config.ErrNotPresent,
 			wantIacProvider: []iacProvider.IacProvider{&tfv14.TfV14{}},
 		},
-		{
-			name: "invalid policy path",
-			executor: Executor{
-				filePath:    filepath.Join(testDataDir, "testfile"),
-				dirPath:     "",
-				policyTypes: []string{"aws"},
-				iacType:     "terraform",
-				iacVersion:  "v14",
-				policyPath:  []string{filepath.Join(testDataDir, "notthere")},
-			},
-			configFile:      filepath.Join(testDataDir, "webhook.toml"),
-			wantErr:         fmt.Errorf("failed to initialize OPA policy engine"),
-			wantIacProvider: []iacProvider.IacProvider{&tfv14.TfV14{}},
-			wantNotifiers:   []notifications.Notifier{&webhook.Webhook{}},
-		},
 		{
 			name: "config file with invalid category",
 			executor: Executor{
@@ -358,21 +385,6 @@ func TestInit(t *testing.T) {
 			wantErr:         config.ErrNotPresent,
 			wantIacProvider: []iacProvider.IacProvider{&tfv12.TfV12{}},
 		},
-		{
-			name: "invalid policy path",
-			executor: Executor{
-				filePath:    filepath.Join(testDataDir, "testfile"),
-				dirPath:     "",
-				policyTypes: []string{"aws"},
-				iacType:     "terraform",
-				iacVersion:  "v12",
-				policyPath:  []string{filepath.Join(testDataDir, "notthere")},
-			},
-			configFile:      filepath.Join(testDataDir, "webhook.toml"),
-			wantErr:         fmt.Errorf("failed to initialize OPA policy engine"),
-			wantIacProvider: []iacProvider.IacProvider{&tfv12.TfV12{}},
-			wantNotifiers:   []notifications.Notifier{&webhook.Webhook{}},
-		},
 	}
 
 	for _, tt := range table {
diff --git a/test/e2e/scan/scan_config_only_test.go b/test/e2e/scan/scan_config_only_test.go
index a9df65672..4831401c9 100644
--- a/test/e2e/scan/scan_config_only_test.go
+++ b/test/e2e/scan/scan_config_only_test.go
@@ -85,20 +85,20 @@ var _ = Describe("Scan With Config Only Flag", func() {
 		Context("for terraform files", func() {
 			When("output type is json", func() {
 				Context("it supports --config-only flag", func() {
-					It("should display config json and exit with status code 3", func() {
+					It("should display config json and exit with status code 0", func() {
 						scanArgs := []string{scanUtils.ScanCommand, "-d", iacDir, "--config-only", "-o", "json"}
 						session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
-						Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeThree))
+						Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeZero))
 					})
 				})
 			})
 
 			When("output type is yaml", func() {
 				Context("it supports --config-only flag", func() {
-					It("should display config json and exit with status code 3", func() {
+					It("should display config json and exit with status code 0", func() {
 						scanArgs := []string{scanUtils.ScanCommand, "-d", iacDir, "--config-only", "-o", "yaml"}
 						session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
-						Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeThree))
+						Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeZero))
 					})
 				})
 			})
@@ -110,20 +110,20 @@ var _ = Describe("Scan With Config Only Flag", func() {
 			})
 			When("output type is json", func() {
 				Context("it supports --config-only flag", func() {
-					It("should display config json and exit with status code 3", func() {
+					It("should display config json and exit with status code 0", func() {
 						scanArgs := []string{scanUtils.ScanCommand, "-d", iacDir, "--config-only", "-o", "json", "-i", "k8s"}
 						session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
-						Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeThree))
+						Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeZero))
 					})
 				})
 			})
 
 			When("output type is yaml", func() {
 				Context("it supports --config-only flag", func() {
-					It("should display config json and exit with status code 3", func() {
+					It("should display config json and exit with status code 0", func() {
 						scanArgs := []string{scanUtils.ScanCommand, "-d", iacDir, "--config-only", "-o", "yaml", "-i", "k8s"}
 						session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
-						Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeThree))
+						Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeZero))
 					})
 				})
 			})