Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

terrascan should exit with non zero exit code when scan error are present #994

Merged
merged 2 commits into from
Aug 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,16 @@ To scan your code for security issues you can run the following (defaults to sca
```sh
$ terrascan scan
```
**Note**: Terrascan will exit with an error code 3 if any issues are found during a scan.

**Note**: Terrascan will exit with an error code if any errors or violations are found during a scan.

#### List of possible Exit Codes
| Scenario | Exit Code |
| ----------- | ----------- |
| scan summary has errors and violations | 5 |
| scan summary has errors but no violations | 4 |
| scan summary has violations but no errors | 3 |
| scan summary has no violations or errors | 0 |
| scan command errors out due to invalid inputs | 1 |
### Step 3: Integrate with CI\CD

Terrascan can be integrated into CI/CD pipelines to enforce security best practices.
Expand Down
11 changes: 10 additions & 1 deletion docs/usage/command_line_mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,16 @@ When scanning Terraform code, Terrascan checks for the availability of the file

By default, Terrascan output is displayed in a human friendly format. Use the `-o` flag to change this to **YAML**, **JSON**, **XML**, **JUNIT-XML** and **SARIF** formats.

> **Note**: Terrascan exits with error code 3 if any issues are found during a scan.
> **Note**: Terrascan will exit with an error code if any errors or violations are found during a scan.

> #### List of possible Exit Codes
> | Scenario | Exit Code |
> | ----------- | ----------- |
> | scan summary has errors and violations | 5 |
> | scan summary has errors but no violations | 4 |
> | scan summary has violations but no errors | 3 |
> | scan summary has no violations or errors | 0 |
> | scan command errors out due to invalid inputs | 1 |


Terrascan's output is a list of security violations present in the scanned IaC files. The example below is terrascan's output in YAML.
Expand Down
20 changes: 18 additions & 2 deletions pkg/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,12 @@ func (s *ScanOptions) Run() error {
return err
}

if !s.configOnly && results.Violations.ViolationStore.Summary.ViolatedPolicies != 0 && flag.Lookup("test.v") == nil {
if !s.configOnly && flag.Lookup("test.v") == nil {
os.RemoveAll(tempDir)
os.Exit(3)
exitCode := getExitCode(results)
if exitCode != 0 {
os.Exit(exitCode)
}
}
return nil
}
Expand Down Expand Up @@ -247,3 +250,16 @@ func (s ScanOptions) writeResults(results runtime.Output) error {

return writer.Write(s.outputType, results.Violations, outputWriter)
}

// getExitCode returns appropriate exit code for terrascan based on scan output
func getExitCode(o runtime.Output) int {
if len(o.Violations.ViolationStore.DirScanErrors) > 0 {
if o.Violations.ViolationStore.Summary.ViolatedPolicies > 0 {
return 5
}
return 4
} else if o.Violations.ViolationStore.Summary.ViolatedPolicies > 0 {
return 3
}
return 0
}
88 changes: 88 additions & 0 deletions pkg/cli/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -567,3 +567,91 @@ func TestScanOptionsScan(t *testing.T) {
})
}
}

func Test_getExitCode(t *testing.T) {
testDirScanErrors := []results.DirScanErr{
{
IacType: "all",
Directory: "test",
ErrMessage: "error occurred",
},
}

testScanSummary := results.ScanSummary{
ViolatedPolicies: 1,
}

scanOutputWithDirErrorsOnly := runtime.Output{
Violations: policy.EngineOutput{
ViolationStore: &results.ViolationStore{
DirScanErrors: testDirScanErrors,
},
},
}

scanOutputWithDirErrorsAndViolatedPolicies := runtime.Output{
Violations: policy.EngineOutput{
ViolationStore: &results.ViolationStore{
DirScanErrors: testDirScanErrors,
Summary: testScanSummary,
},
},
}

scanOutputWithViolatedPoliciesOnly := runtime.Output{
Violations: policy.EngineOutput{
ViolationStore: &results.ViolationStore{
Summary: testScanSummary,
},
},
}

type args struct {
o runtime.Output
}
tests := []struct {
name string
args args
want int
}{
{
name: "has directory scan errors without violated policies",
args: args{
o: scanOutputWithDirErrorsOnly,
},
want: 4,
},
{
name: "has directory scan errors with violated policies",
args: args{
o: scanOutputWithDirErrorsAndViolatedPolicies,
},
want: 5,
},
{
name: "has violated policies but no directory scan errors",
args: args{
o: scanOutputWithViolatedPoliciesOnly,
},
want: 3,
},
{
name: "neither violations nor directory scan errors",
args: args{
o: runtime.Output{
Violations: policy.EngineOutput{
ViolationStore: &results.ViolationStore{},
},
},
},
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := getExitCode(tt.args.o); got != tt.want {
t.Errorf("getExitCode() = %v, want %v", got, tt.want)
}
})
}
}
5 changes: 3 additions & 2 deletions test/e2e/scan/scan_docker_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ var _ = Describe("Scan is run for dockerfile directories and files", func() {
It("should scan all iac and display violations", func() {
scanArgs := []string{scanUtils.ScanCommand, "-d", iacDir}
session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
// exit code is 3 because iac files in directory has violations
helper.ValidateExitCode(session, scanUtils.ScanTimeout, helper.ExitCodeThree)
// exit code is 5 because iac files in directory has violations
// and directory scan errors
helper.ValidateExitCode(session, scanUtils.ScanTimeout, helper.ExitCodeFive)
})
})
})
Expand Down
5 changes: 3 additions & 2 deletions test/e2e/scan/scan_k8s_files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ var _ = Describe("Scan is run for k8s directories and files", func() {
It("should scan will all iac and display violations", func() {
scanArgs := []string{scanUtils.ScanCommand, "-d", iacDir}
session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
// exit code is 3 because iac files in directory has violations
helper.ValidateExitCode(session, scanUtils.ScanTimeout, helper.ExitCodeThree)
// exit code is 5 because iac files in directory has violations
// and directory scan errors
helper.ValidateExitCode(session, scanUtils.ScanTimeout, helper.ExitCodeFive)
})
})
})
Expand Down
18 changes: 12 additions & 6 deletions test/e2e/scan/scan_remote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,18 @@ var _ = Describe("Scan Command using remote types", func() {
It("should download the resource and generate scan results", func() {
scanArgs := []string{scanUtils.ScanCommand, "-r", "git", "--remote-url", remoteURL}
session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
Eventually(session, scanUtils.RemoteScanTimeout).Should(gexec.Exit(helper.ExitCodeThree))
// exit code is 5 because iac files in directory has violations
// and directory scan errors
Eventually(session, scanUtils.RemoteScanTimeout).Should(gexec.Exit(helper.ExitCodeFive))
})

It("should download the resource and generate scan results", func() {
remoteURL := "https://github.com/accurics/KaiMonkey.git//terraform/aws"
scanArgs := []string{scanUtils.ScanCommand, "-r", "git", "--remote-url", remoteURL}
session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
Eventually(session, scanUtils.RemoteScanTimeout).Should(gexec.Exit(helper.ExitCodeThree))
// exit code is 5 because iac files in directory has violations
// and directory scan errors
Eventually(session, scanUtils.RemoteScanTimeout).Should(gexec.Exit(helper.ExitCodeFive))
})
})

Expand Down Expand Up @@ -175,7 +179,8 @@ var _ = Describe("Scan Command using remote types", func() {
scanArgs := []string{scanUtils.ScanCommand, "-r", "terraform-registry", "--remote-url", remoteURL}
session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
// has a OR condition because we don't know if there would be violations or not
Eventually(session, scanUtils.RemoteScanTimeout).Should(Or(gexec.Exit(helper.ExitCodeThree), gexec.Exit(helper.ExitCodeZero)))
// there would be directory scan errors due to all iac type
Eventually(session, scanUtils.RemoteScanTimeout).Should(Or(gexec.Exit(helper.ExitCodeFive), gexec.Exit(helper.ExitCodeFour)))
})
})

Expand All @@ -185,15 +190,16 @@ var _ = Describe("Scan Command using remote types", func() {
scanArgs := []string{scanUtils.ScanCommand, "-r", "terraform-registry", "--remote-url", remoteURL}
session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
// has a OR condition because we don't know if there would be violations or not
Eventually(session, scanUtils.RemoteScanTimeout).Should(Or(gexec.Exit(helper.ExitCodeThree), gexec.Exit(helper.ExitCodeZero)))
// there would be directory scan errors due to all iac type
Eventually(session, scanUtils.RemoteScanTimeout).Should(Or(gexec.Exit(helper.ExitCodeFive), gexec.Exit(helper.ExitCodeFour)))
})
})

Context("remote modules has reference to its local modules", func() {
When("remote type is terraform registry and remote url has a subdirectory", func() {
remoteURL := "terraform-aws-modules/security-group/aws//modules/http-80"
It("should download the remote registry and generate scan results", func() {
scanArgs := []string{scanUtils.ScanCommand, "-r", "terraform-registry", "--remote-url", remoteURL}
scanArgs := []string{scanUtils.ScanCommand, "-r", "terraform-registry", "--remote-url", remoteURL, "-i", "terraform", "--non-recursive"}
session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
// has a OR condition because we don't know if there would be violations or not
Eventually(session, scanUtils.RemoteScanTimeout).Should(Or(gexec.Exit(helper.ExitCodeThree), gexec.Exit(helper.ExitCodeZero)))
Expand All @@ -203,7 +209,7 @@ var _ = Describe("Scan Command using remote types", func() {
When("remote type is git and remote url has a subdirectory", func() {
remoteURL := "github.com/terraform-aws-modules/terraform-aws-security-group//modules/http-80"
It("should download the remote registry and generate scan results", func() {
scanArgs := []string{scanUtils.ScanCommand, "-r", "git", "--remote-url", remoteURL}
scanArgs := []string{scanUtils.ScanCommand, "-r", "git", "--remote-url", remoteURL, "-i", "terraform", "--non-recursive"}
session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
// has a OR condition because we don't know if there would be violations or not
Eventually(session, scanUtils.RemoteScanTimeout).Should(Or(gexec.Exit(helper.ExitCodeThree), gexec.Exit(helper.ExitCodeZero)))
Expand Down
12 changes: 7 additions & 5 deletions test/e2e/scan/scan_rules_filtering_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,18 +129,19 @@ var _ = Describe("Scan command with rule filtering options", func() {
Context("severity leve specified is 'low'", func() {
Context("iac file has only medium severity violations", func() {
It("should report the violations and exit with status code 3", func() {
scanArgs := []string{scanUtils.ScanCommand, "-p", policyDir, "-d", iacDir, "-o", "json", "--severity", "low"}
scanArgs := []string{scanUtils.ScanCommand, "-p", policyDir, "-d", iacDir, "-o", "json", "--severity", "low", "-i", "terraform"}
session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeThree))
})
})
})
Context("severity leve specified is 'high'", func() {
Context("severity level specified is 'high'", func() {
Context("iac files has only medium severity violations", func() {
It("should not report any violation and exit with status code 0", func() {
// there would not no violations but directory scan errors would be present due to all iac scan
It("should not report any violation and exit with status code 4", func() {
scanArgs := []string{scanUtils.ScanCommand, "-p", policyDir, "-d", iacDir, "-o", "json", "--severity", "high"}
session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeZero))
Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeFour))
})
})
})
Expand Down Expand Up @@ -171,7 +172,8 @@ var _ = Describe("Scan command with rule filtering options", func() {
It("should not report any violation and exit with status code 0", func() {
scanArgs := []string{scanUtils.ScanCommand, "-p", policyDir, "-d", iacDir, "-o", "json", "--categories", "COMPLIANCE VALIDATION"}
session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeZero))
// summary would contain directory scan errors due to all iac scan
Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeFour))
})
})
})
Expand Down
22 changes: 11 additions & 11 deletions test/e2e/scan/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ var _ = Describe("Scan", func() {
Describe("scan command is run", func() {
Context("when no iac type is provided, terrascan scans with all iac providers", func() {
Context("no tf files are present in the working directory", func() {
It("scan the directory and display results", func() {
It("scans the directory with all iac and display results", func() {
scanArgs := []string{scanUtils.ScanCommand}
session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
helper.ValidateExitCode(session, scanUtils.ScanTimeout, helper.ExitCodeZero)
helper.ValidateExitCode(session, scanUtils.ScanTimeout, helper.ExitCodeFour)
})
Context("iac loading errors would be displayed in the output, output type is json", func() {
It("scan the directory and display results", func() {
Expand All @@ -106,17 +106,17 @@ var _ = Describe("Scan", func() {
})
})
Context("tf files are present in the working directory", func() {
It("should scan the directory, return results and exit with status code 3", func() {
It("should scan the directory, return results and exit with status code 3 as there would no directory scan errors", func() {
workDir, err := filepath.Abs(filepath.Join(awsIacRelPath, "aws_ami_violation"))
Expect(err).NotTo(HaveOccurred())

scanArgs := []string{scanUtils.ScanCommand}
scanArgs := []string{scanUtils.ScanCommand, "-i", "terraform", "--non-recursive"}
session = helper.RunCommandDir(terrascanBinaryPath, workDir, outWriter, errWriter, scanArgs...)
Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeThree))
})

When("tf file present in the dir has no violations", func() {
Context("when there are no violations, terrascan exits with status code 0", func() {
Context("when there are no violations, but has dir scan erros, terrascan exits with status code 4", func() {
It("should scan the directory and exit with status code 0", func() {
workDir, err := filepath.Abs(filepath.Join(awsIacRelPath, "aws_db_instance_violation"))
Expect(err).NotTo(HaveOccurred())
Expand All @@ -127,7 +127,7 @@ var _ = Describe("Scan", func() {

scanArgs := []string{scanUtils.ScanCommand, "-p", policyDir}
session = helper.RunCommandDir(terrascanBinaryPath, workDir, outWriter, errWriter, scanArgs...)
Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeZero))
Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeFour))
})
})
})
Expand Down Expand Up @@ -194,10 +194,10 @@ var _ = Describe("Scan", func() {

When("--iac-version flag is supplied invalid version", func() {
Context("default iac type is all and --iac-version would be ignored", func() {
It("should error out and exit with status code 1", func() {
It("should error out and exit with status code 4", func() {
scanArgs := []string{scanUtils.ScanCommand, "--iac-version", "test"}
session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, scanArgs...)
helper.ValidateExitCode(session, scanUtils.ScanTimeout, helper.ExitCodeZero)
helper.ValidateExitCode(session, scanUtils.ScanTimeout, helper.ExitCodeFour)
})
})
})
Expand Down Expand Up @@ -276,9 +276,9 @@ var _ = Describe("Scan", func() {
It("should scan with the policies and exit with status code 0", func() {
scanArgs := []string{scanUtils.ScanCommand, "-p", validPolicyPath1, "-p", validPolicyPath2}
session = helper.RunCommandDir(terrascanBinaryPath, workDirPath, outWriter, errWriter, scanArgs...)
// exits with status code 0, because all iac scan should display results
// and the directory doesn't have iac files for violations
Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeZero))
// exits with status code 4, because there are no iac files for violations but
// would contain directory scan errors
Eventually(session, scanUtils.ScanTimeout).Should(gexec.Exit(helper.ExitCodeFour))
})
})

Expand Down
4 changes: 2 additions & 2 deletions test/e2e/scan/scan_tf_files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ var _ = Describe("Scan is run for terraform files", func() {
Context("when iac type is not specified and a directory is specified, it will be scanned will all iac providers", func() {
It("should display violations in json format, and should have iac type as 'all'", func() {
scanArgs := []string{"-p", policyDir, "-d", iacDir, "-o", "json"}
scanUtils.RunScanAndAssertGoldenOutputRegex(terrascanBinaryPath, filepath.Join(tfAwsAmiGoldenRelPath, "aws_ami_violation_json_all.txt"), helper.ExitCodeThree, false, true, outWriter, errWriter, scanArgs...)
scanUtils.RunScanAndAssertGoldenOutputRegex(terrascanBinaryPath, filepath.Join(tfAwsAmiGoldenRelPath, "aws_ami_violation_json_all.txt"), helper.ExitCodeFive, false, true, outWriter, errWriter, scanArgs...)
})
})
})
Expand Down Expand Up @@ -217,7 +217,7 @@ var _ = Describe("Scan is run for terraform files", func() {
It("should display violations in json format", func() {
iacDir := filepath.Join(iacRootRelPath, "terraform_recursive")
scanArgs := []string{"-i", "terraform", "-p", policyDir, "-d", iacDir, "-o", "json"}
scanUtils.RunScanAndAssertGoldenOutputRegex(terrascanBinaryPath, filepath.Join(tfAwsAmiGoldenRelPath, "aws_ami_violation_json_recursive.txt"), helper.ExitCodeThree, false, true, outWriter, errWriter, scanArgs...)
scanUtils.RunScanAndAssertGoldenOutputRegex(terrascanBinaryPath, filepath.Join(tfAwsAmiGoldenRelPath, "aws_ami_violation_json_recursive.txt"), helper.ExitCodeFive, false, true, outWriter, errWriter, scanArgs...)
})
})
})
Expand Down
14 changes: 9 additions & 5 deletions test/helper/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,15 @@ import (

const (
// ExitCodeZero represents command exit code 0
ExitCodeZero = 0
// ExitCodeOne represents command exit code 0
ExitCodeOne = 1
// ExitCodeThree represents command exit code 0
ExitCodeThree = 3
ExitCodeZero = iota
// ExitCodeOne represents command exit code 1
ExitCodeOne
// ExitCodeThree represents command exit code 3
ExitCodeThree = iota + 1
// ExitCodeFour represents command exit code 4
ExitCodeFour
// ExitCodeFive represents command exit code 5
ExitCodeFive
)

var (
Expand Down