Repository: aquasecurity/kube-bench
Pull request #734 (2020-10-10 17:01:05)

Add exit-code parameter for when checks have failed
This is a change that addresses the issue #723

**Behaviour:**
- If exit-code parameter is unset it defaults to 0, meaning regardless of pass or failure it always returns exit code 0. (Assuming kube-bench made it to the end of execution without error)
- If exit-code is specified and is non-zero then if any control collection has more than 0 failures it will exit with the supplied exit code.

Please let me know if any additional changes are needed or if there is any change in functionality needed. I've tested locally and it all seems to work correctly.

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/kube-bench/pull/734?src=pr&el=h1) Report
> Merging [#734](https://codecov.io/gh/aquasecurity/kube-bench/pull/734?src=pr&el=desc) into [master](https://codecov.io/gh/aquasecurity/kube-bench/commit/58bea9c89b9be43ebdf76de256d916a608df43c4?el=desc) will **decrease** coverage by `0.32%`.
> The diff coverage is `14.28%`.

[![Impacted file tree graph](https://codecov.io/gh/aquasecurity/kube-bench/pull/734/graphs/tree.svg?width=650&height=150&src=pr&token=MVaDEmdgi9)](https://codecov.io/gh/aquasecurity/kube-bench/pull/734?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #734      +/-   ##
==========================================
- Coverage   63.31%   62.98%   -0.33%     
==========================================
  Files          12       12              
  Lines        1044     1051       +7     
==========================================
+ Hits          661      662       +1     
- Misses        344      350       +6     
  Partials       39       39              
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/kube-bench/pull/734?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [cmd/common.go](https://codecov.io/gh/aquasecurity/kube-bench/pull/734/diff?src=pr&el=tree#diff-Y21kL2NvbW1vbi5nbw==) | `39.28% <0.00%> (-0.90%)` | :arrow_down: |
| [cmd/root.go](https://codecov.io/gh/aquasecurity/kube-bench/pull/734/diff?src=pr&el=tree#diff-Y21kL3Jvb3QuZ28=) | `40.42% <50.00%> (+0.20%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aquasecurity/kube-bench/pull/734?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/aquasecurity/kube-bench/pull/734?src=pr&el=footer). Last update [58bea9c...932eb2d](https://codecov.io/gh/aquasecurity/kube-bench/pull/734?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/README.md b/README.md
index 8d2a782..926ee11 100644
--- a/README.md
+++ b/README.md
@@ -298,7 +298,7 @@ Alternatively, you can manually download and extract the kube-bench binary:
 ```
 curl -L https://github.com/aquasecurity/kube-bench/releases/download/v0.3.1/kube-bench_0.3.1_linux_amd64.tar.gz -o kube-bench_0.3.1_linux_amd64.tar.gz
 
-tar -xvf kube-bench_0.3.0_linux_amd64.tar.gz
+tar -xvf kube-bench_0.3.1_linux_amd64.tar.gz
 ```
 
 You can then run kube-bench directly:
diff --git a/cfg/cis-1.5/master.yaml b/cfg/cis-1.5/master.yaml
index 3881fae..8f9c1ad 100644
--- a/cfg/cis-1.5/master.yaml
+++ b/cfg/cis-1.5/master.yaml
@@ -223,7 +223,7 @@ groups:
 
       - id: 1.1.15
         text: "Ensure that the scheduler.conf file permissions are set to 644 or more restrictive (Scored)"
-        audit: "/bin/sh -c 'if test -e /etc/kubernetes/scheduler.conf; then stat -c permissions=%a /etc/kubernetes/scheduler.conf; fi'"
+        audit: "/bin/sh -c 'if test -e $schedulerkubeconfig; then stat -c permissions=%a $schedulerkubeconfig; fi'"
         tests:
           test_items:
             - flag: "permissions"
@@ -234,12 +234,12 @@ groups:
         remediation: |
           Run the below command (based on the file location on your system) on the master node.
           For example,
-          chmod 644 /etc/kubernetes/scheduler.conf
+          chmod 644 $schedulerkubeconfig
         scored: true
 
       - id: 1.1.16
         text: "Ensure that the scheduler.conf file ownership is set to root:root (Scored)"
-        audit: "/bin/sh -c 'if test -e /etc/kubernetes/scheduler.conf; then stat -c %U:%G /etc/kubernetes/scheduler.conf; fi'"
+        audit: "/bin/sh -c 'if test -e $schedulerkubeconfig; then stat -c %U:%G $schedulerkubeconfig; fi'"
         tests:
           test_items:
             - flag: "root:root"
@@ -250,12 +250,12 @@ groups:
         remediation: |
           Run the below command (based on the file location on your system) on the master node.
           For example,
-          chown root:root /etc/kubernetes/scheduler.conf
+          chown root:root $schedulerkubeconfig
         scored: true
 
       - id: 1.1.17
         text: "Ensure that the controller-manager.conf file permissions are set to 644 or more restrictive (Scored)"
-        audit: "/bin/sh -c 'if test -e /etc/kubernetes/controller-manager.conf; then stat -c permissions=%a /etc/kubernetes/controller-manager.conf; fi'"
+        audit: "/bin/sh -c 'if test -e $controllermanagerkubeconfig; then stat -c permissions=%a $controllermanagerkubeconfig; fi'"
         tests:
           test_items:
             - flag: "permissions"
@@ -266,12 +266,12 @@ groups:
         remediation: |
           Run the below command (based on the file location on your system) on the master node.
           For example,
-          chmod 644 /etc/kubernetes/controller-manager.conf
+          chmod 644 $controllermanagerkubeconfig
         scored: true
 
       - id: 1.1.18
         text: "Ensure that the controller-manager.conf file ownership is set to root:root (Scored)"
-        audit: "/bin/sh -c 'if test -e /etc/kubernetes/controller-manager.conf; then stat -c %U:%G /etc/kubernetes/controller-manager.conf; fi'"
+        audit: "/bin/sh -c 'if test -e $controllermanagerkubeconfig; then stat -c %U:%G $controllermanagerkubeconfig; fi'"
         tests:
           test_items:
             - flag: "root:root"
@@ -282,7 +282,7 @@ groups:
         remediation: |
           Run the below command (based on the file location on your system) on the master node.
           For example,
-          chown root:root /etc/kubernetes/controller-manager.conf
+          chown root:root $controllermanagerkubeconfig
         scored: true
 
       - id: 1.1.19
@@ -908,7 +908,7 @@ groups:
           test_items:
             - flag: "--tls-cipher-suites"
               compare:
-                op: has
+                op: valid_elements
                 value: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_GCM_SHA256"
               set: true
         remediation: |
diff --git a/cfg/cis-1.6/master.yaml b/cfg/cis-1.6/master.yaml
index 8f07ced..fa23948 100644
--- a/cfg/cis-1.6/master.yaml
+++ b/cfg/cis-1.6/master.yaml
@@ -196,7 +196,7 @@ groups:
 
       - id: 1.1.15
         text: "Ensure that the scheduler.conf file permissions are set to 644 or more restrictive (Automated)"
-        audit: "/bin/sh -c 'if test -e /etc/kubernetes/scheduler.conf; then stat -c permissions=%a /etc/kubernetes/scheduler.conf; fi'"
+        audit: "/bin/sh -c 'if test -e $schedulerkubeconfig; then stat -c permissions=%a $schedulerkubeconfig; fi'"
         tests:
           test_items:
             - flag: "permissions"
@@ -206,24 +206,24 @@ groups:
         remediation: |
           Run the below command (based on the file location on your system) on the master node.
           For example,
-          chmod 644 /etc/kubernetes/scheduler.conf
+          chmod 644 $schedulerkubeconfig
         scored: true
 
       - id: 1.1.16
         text: "Ensure that the scheduler.conf file ownership is set to root:root (Automated)"
-        audit: "/bin/sh -c 'if test -e /etc/kubernetes/scheduler.conf; then stat -c %U:%G /etc/kubernetes/scheduler.conf; fi'"
+        audit: "/bin/sh -c 'if test -e $schedulerkubeconfig; then stat -c %U:%G $schedulerkubeconfig; fi'"
         tests:
           test_items:
             - flag: "root:root"
         remediation: |
           Run the below command (based on the file location on your system) on the master node.
           For example,
-          chown root:root /etc/kubernetes/scheduler.conf
+          chown root:root $schedulerkubeconfig
         scored: true
 
       - id: 1.1.17
         text: "Ensure that the controller-manager.conf file permissions are set to 644 or more restrictive (Automated)"
-        audit: "/bin/sh -c 'if test -e /etc/kubernetes/controller-manager.conf; then stat -c permissions=%a /etc/kubernetes/controller-manager.conf; fi'"
+        audit: "/bin/sh -c 'if test -e $controllermanagerkubeconfig; then stat -c permissions=%a $controllermanagerkubeconfig; fi'"
         tests:
           test_items:
             - flag: "permissions"
@@ -233,19 +233,19 @@ groups:
         remediation: |
           Run the below command (based on the file location on your system) on the master node.
           For example,
-          chmod 644 /etc/kubernetes/controller-manager.conf
+          chmod 644 $controllermanagerkubeconfig
         scored: true
 
       - id: 1.1.18
         text: "Ensure that the controller-manager.conf file ownership is set to root:root (Automated)"
-        audit: "/bin/sh -c 'if test -e /etc/kubernetes/controller-manager.conf; then stat -c %U:%G /etc/kubernetes/controller-manager.conf; fi'"
+        audit: "/bin/sh -c 'if test -e $controllermanagerkubeconfig; then stat -c %U:%G $controllermanagerkubeconfig; fi'"
         tests:
           test_items:
             - flag: "root:root"
         remediation: |
           Run the below command (based on the file location on your system) on the master node.
           For example,
-          chown root:root /etc/kubernetes/controller-manager.conf
+          chown root:root $controllermanagerkubeconfig
         scored: true
 
       - id: 1.1.19
@@ -832,7 +832,7 @@ groups:
           test_items:
             - flag: "--tls-cipher-suites"
               compare:
-                op: has
+                op: valid_elements
                 value: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_GCM_SHA256"
         remediation: |
           Edit the API server pod specification file /etc/kubernetes/manifests/kube-apiserver.yaml
diff --git a/cfg/config.yaml b/cfg/config.yaml
index 7170877..52360fb 100644
--- a/cfg/config.yaml
+++ b/cfg/config.yaml
@@ -46,6 +46,9 @@ master:
       - /var/snap/kube-scheduler/current/args
       - /var/snap/microk8s/current/args/kube-scheduler
     defaultconf: /etc/kubernetes/manifests/kube-scheduler.yaml
+    kubeconfig:
+      - /etc/kubernetes/scheduler.conf
+    defaultkubeconfig: /etc/kubernetes/scheduler.conf
 
   controllermanager:
     bins:
@@ -61,6 +64,9 @@ master:
       - /var/snap/kube-controller-manager/current/args
       - /var/snap/microk8s/current/args/kube-controller-manager
     defaultconf: /etc/kubernetes/manifests/kube-controller-manager.yaml
+    kubeconfig:
+      - /etc/kubernetes/controller-manager.conf
+    defaultkubeconfig: /etc/kubernetes/controller-manager.conf
 
   etcd:
     optional: true
diff --git a/cfg/eks-1.0/policies.yaml b/cfg/eks-1.0/policies.yaml
index 98dbe28..51f25a0 100644
--- a/cfg/eks-1.0/policies.yaml
+++ b/cfg/eks-1.0/policies.yaml
@@ -131,7 +131,7 @@ groups:
         text: "Minimize the admission of containers with capabilities assigned (Not Scored)"
         type: "manual"
         remediation: |
-          Review the use of capabilites in applications runnning on your cluster. Where a namespace
+          Review the use of capabilities in applications running on your cluster. Where a namespace
           contains applications which do not require any Linux capabities to operate consider adding
           a PSP which forbids the admission of containers which do not drop all capabilities.
         scored: false
diff --git a/check/check.go b/check/check.go
index aa44a00..463afa8 100644
--- a/check/check.go
+++ b/check/check.go
@@ -39,6 +39,9 @@ const (
 	// INFO informational message
 	INFO State = "INFO"
 
+	// SKIP for when a check should be skipped.
+	SKIP = "skip"
+
 	// MASTER a master node
 	MASTER NodeType = "master"
 	// NODE a node
@@ -111,7 +114,7 @@ func (c *Check) run() State {
 	}
 
 	// If check type is skip, force result to INFO
-	if c.Type == "skip" {
+	if c.Type == SKIP {
 		c.Reason = "Test marked as skip"
 		c.State = INFO
 		return c.State
diff --git a/check/controls.go b/check/controls.go
index 1b36dee..d6f6721 100644
--- a/check/controls.go
+++ b/check/controls.go
@@ -38,6 +38,7 @@ type Controls struct {
 // Group is a collection of similar checks.
 type Group struct {
 	ID     string   `yaml:"id" json:"section"`
+	Skip   bool     `yaml:"skip" json:"skip"`
 	Pass   int      `json:"pass"`
 	Fail   int      `json:"fail"`
 	Warn   int      `json:"warn"`
@@ -74,7 +75,7 @@ func NewControls(t NodeType, in []byte) (*Controls, error) {
 }
 
 // RunChecks runs the checks with the given Runner. Only checks for which the filter Predicate returns `true` will run.
-func (controls *Controls) RunChecks(runner Runner, filter Predicate) Summary {
+func (controls *Controls) RunChecks(runner Runner, filter Predicate, skipIdMap map[string]bool) Summary {
 	var g []*Group
 	m := make(map[string]*Group)
 	controls.Summary.Pass, controls.Summary.Fail, controls.Summary.Warn, controls.Info = 0, 0, 0, 0
@@ -86,7 +87,15 @@ func (controls *Controls) RunChecks(runner Runner, filter Predicate) Summary {
 				continue
 			}
 
+			_, groupSkippedViaCmd := skipIdMap[group.ID]
+			_, checkSkippedViaCmd := skipIdMap[check.ID]
+
+			if group.Skip || groupSkippedViaCmd || checkSkippedViaCmd  {
+				check.Type = SKIP
+			}
+
 			state := runner.Run(check)
+
 			check.TestInfo = append(check.TestInfo, check.Remediation)
 
 			// Check if we have already added this checks group.
@@ -95,6 +104,7 @@ func (controls *Controls) RunChecks(runner Runner, filter Predicate) Summary {
 				w := &Group{
 					ID:     group.ID,
 					Text:   group.Text,
+					Skip:   group.Skip,
 					Checks: []*Check{},
 				}
 
diff --git a/cmd/common.go b/cmd/common.go
index 49a4e2d..55b5c35 100644
--- a/cmd/common.go
+++ b/cmd/common.go
@@ -32,7 +32,6 @@ import (
 
 // NewRunFilter constructs a Predicate based on FilterOpts which determines whether tested Checks should be run or not.
 func NewRunFilter(opts FilterOpts) (check.Predicate, error) {
-
 	if opts.CheckList != "" && opts.GroupList != "" {
 		return nil, fmt.Errorf("group option and check option can't be used together")
 	}
@@ -118,10 +117,20 @@ func runChecks(nodetype check.NodeType, testYamlFile string) {
 		exitWithError(fmt.Errorf("error setting up run filter: %v", err))
 	}
 
-	controls.RunChecks(runner, filter)
+	controls.RunChecks(runner, filter, parseSkipIds(skipIds))
 	controlsCollection = append(controlsCollection, controls)
 }
 
+func parseSkipIds(skipIds string) map[string]bool {
+	var skipIdMap =  make(map[string]bool, 0)
+	if skipIds != "" {
+		for _, id := range strings.Split(skipIds, ",") {
+			skipIdMap[strings.Trim(id, " ")] = true
+		}
+	}
+	return skipIdMap
+}
+
 // colorPrint outputs the state in a specific colour, along with a message string
 func colorPrint(state check.State, s string) {
 	colors[state].Printf("[%s] ", state)
@@ -336,6 +345,16 @@ func isThisNodeRunning(nodeType check.NodeType) bool {
 	return true
 }
 
+func exitCodeSelection(controlsCollection []*check.Controls) int {
+	for _, control := range controlsCollection {
+		if control.Fail > 0  {
+			return exitCode
+		}
+	}
+
+	return 0
+}
+
 func writeOutput(controlsCollection []*check.Controls) {
 	sort.Slice(controlsCollection, func(i, j int) bool {
 		iid, _ := strconv.Atoi(controlsCollection[i].ID)
diff --git a/cmd/root.go b/cmd/root.go
index 432d326..327f2e7 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -48,9 +48,11 @@ var (
 	controlplaneFile    = "controlplane.yaml"
 	policiesFile        = "policies.yaml"
 	managedservicesFile = "managedservices.yaml"
+	exitCode            int
 	noResults           bool
 	noSummary           bool
 	noRemediations      bool
+	skipIds             string
 	filterOpts          FilterOpts
 	includeTestOutput   bool
 	outputFile          string
@@ -123,6 +125,8 @@ var RootCmd = &cobra.Command{
 		}
 
 		writeOutput(controlsCollection)
+		exitCode := exitCodeSelection(controlsCollection)
+		os.Exit(exitCode)
 	},
 }
 
@@ -145,6 +149,7 @@ func init() {
 	cobra.OnInitialize(initConfig)
 
 	// Output control
+	RootCmd.PersistentFlags().IntVar(&exitCode, "exit-code", 0, "Specify the exit code for when checks fail")
 	RootCmd.PersistentFlags().BoolVar(&noResults, "noresults", false, "Disable printing of results section")
 	RootCmd.PersistentFlags().BoolVar(&noSummary, "nosummary", false, "Disable printing of summary section")
 	RootCmd.PersistentFlags().BoolVar(&noRemediations, "noremediations", false, "Disable printing of remediations section")
@@ -153,6 +158,7 @@ func init() {
 	RootCmd.PersistentFlags().BoolVar(&pgSQL, "pgsql", false, "Save the results to PostgreSQL")
 	RootCmd.PersistentFlags().BoolVar(&filterOpts.Scored, "scored", true, "Run the scored CIS checks")
 	RootCmd.PersistentFlags().BoolVar(&filterOpts.Unscored, "unscored", true, "Run the unscored CIS checks")
+	RootCmd.PersistentFlags().StringVar(&skipIds, "skip", "", "List of comma separated values of checks to be skipped")
 	RootCmd.PersistentFlags().BoolVar(&includeTestOutput, "include-test-output", false, "Prints the actual result when test fails")
 	RootCmd.PersistentFlags().StringVar(&outputFile, "outputfile", "", "Writes the JSON results to output file")
 


Test patch:
diff --git a/check/controls_test.go b/check/controls_test.go
index fd49686..34a76b8 100644
--- a/check/controls_test.go
+++ b/check/controls_test.go
@@ -69,7 +69,6 @@ func TestYamlFiles(t *testing.T) {
 }
 
 func TestNewControls(t *testing.T) {
-
 	t.Run("Should return error when node type is not specified", func(t *testing.T) {
 		// given
 		in := []byte(`
@@ -95,8 +94,75 @@ groups:
 
 }
 
-func TestControls_RunChecks(t *testing.T) {
+func TestControls_RunChecks_SkippedCmd(t *testing.T) {
+	t.Run("Should skip checks and groups specified by skipMap", func(t *testing.T) {
+		// given
+		normalRunner := &defaultRunner{}
+		// and
+		in := []byte(`
+---
+type: "master"
+groups:
+- id: G1
+  checks:
+  - id: G1/C1
+  - id: G1/C2
+  - id: G1/C3
+- id: G2
+  checks:
+  - id: G2/C1
+  - id: G2/C2
+`)
+		controls, err := NewControls(MASTER, in)
+		assert.NoError(t, err)
+
+		var allChecks Predicate = func(group *Group, c *Check) bool {
+			return true
+		}
+
+		skipMap := make(map[string]bool, 0)
+		skipMap["G1"] = true
+		skipMap["G2/C1"] = true
+		skipMap["G2/C2"] = true
+		controls.RunChecks(normalRunner, allChecks, skipMap)
+
+		G1 := controls.Groups[0]
+		assertEqualGroupSummary(t, 0, 0, 3, 0, G1)
+
+		G2 := controls.Groups[1]
+		assertEqualGroupSummary(t, 0, 0, 2, 0, G2)
+	})
+}
+
+func TestControls_RunChecks_Skipped(t *testing.T) {
+	t.Run("Should skip checks where the parent group is marked as skip", func(t *testing.T) {
+		// given
+		normalRunner := &defaultRunner{}
+		// and
+		in := []byte(`
+---
+type: "master"
+groups:
+- id: G1
+  skip: true
+  checks:
+  - id: G1/C1
+`)
+		controls, err := NewControls(MASTER, in)
+		assert.NoError(t, err)
+
+		var allChecks Predicate = func(group *Group, c *Check) bool {
+			return true
+		}
+		emptySkipList := make(map[string]bool, 0)
+		controls.RunChecks(normalRunner, allChecks, emptySkipList)
+
+		G1 := controls.Groups[0]
+		assertEqualGroupSummary(t, 0, 0, 1, 0, G1)
+	})
+}
 
+func TestControls_RunChecks(t *testing.T) {
 	t.Run("Should run checks matching the filter and update summaries", func(t *testing.T) {
 		// given
 		runner := new(mockRunner)
@@ -134,8 +200,9 @@ groups:
 		var runAll Predicate = func(group *Group, c *Check) bool {
 			return true
 		}
+		var emptySkipList = make(map[string]bool, 0)
 		// when
-		controls.RunChecks(runner, runAll)
+		controls.RunChecks(runner, runAll, emptySkipList)
 		// then
 		assert.Equal(t, 2, len(controls.Groups))
 		// and
diff --git a/cmd/common_test.go b/cmd/common_test.go
index 15f4164..4f04c28 100644
--- a/cmd/common_test.go
+++ b/cmd/common_test.go
@@ -30,6 +30,18 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+func TestParseSkipIds(t *testing.T) {
+	skipMap := parseSkipIds("4.12,4.13,5")
+	_, fourTwelveExists := skipMap["4.12"]
+	_, fourThirteenExists := skipMap["4.13"]
+	_, fiveExists := skipMap["5"]
+	_, other := skipMap["G1"]
+	assert.True(t, fourThirteenExists)
+	assert.True(t, fourTwelveExists)
+	assert.True(t, fiveExists)
+	assert.False(t, other)
+}
+
 func TestNewRunFilter(t *testing.T) {
 
 	type TestCase struct {
@@ -523,6 +535,24 @@ func TestWriteResultToJsonFile(t *testing.T) {
 	assert.Equal(t, expect, result)
 }
 
+func TestExitCodeSelection(t *testing.T){
+	exitCode = 10
+	controlsCollectionAllPassed, errPassed := parseControlsJsonFile("./testdata/passedControlsCollection.json")
+	if errPassed != nil {
+		t.Error(errPassed)
+	}
+	controlsCollectionWithFailures, errFailure := parseControlsJsonFile("./testdata/controlsCollection.json")
+	if errFailure != nil {
+		t.Error(errFailure)
+	}
+
+	exitCodePassed := exitCodeSelection(controlsCollectionAllPassed)
+	assert.Equal(t, 0, exitCodePassed)
+
+	exitCodeFailure := exitCodeSelection(controlsCollectionWithFailures)
+	assert.Equal(t, 10, exitCodeFailure)
+}
+
 func parseControlsJsonFile(filepath string) ([]*check.Controls, error) {
 	var result []*check.Controls
 
diff --git a/cmd/testdata/passedControlsCollection.json b/cmd/testdata/passedControlsCollection.json
new file mode 100644
index 0000000..5235dbb
--- /dev/null
+++ b/cmd/testdata/passedControlsCollection.json
@@ -0,0 +1,77 @@
+[
+  {
+    "id": "2",
+    "version": "1.15",
+    "text": "Etcd Node Configuration",
+    "node_type": "etcd",
+    "tests": [
+      {
+        "section": "2",
+        "pass": 7,
+        "fail": 0,
+        "warn": 0,
+        "info": 0,
+        "desc": "Etcd Node Configuration Files",
+        "results": [
+          {
+            "test_number": "2.1",
+            "test_desc": "Ensure that the --cert-file and --key-file arguments are set as appropriate (Scored)",
+            "audit": "/bin/ps -ef | /bin/grep etcd | /bin/grep -v grep",
+            "AuditConfig": "",
+            "type": "",
+            "remediation": "Follow the etcd service documentation and configure TLS encryption.\nThen, edit the etcd pod specification file /etc/kubernetes/manifests/etcd.yaml\non the master node and set the below parameters.\n--cert-file=</path/to/ca-file>\n--key-file=</path/to/key-file>\n",
+            "test_info": [
+              "Follow the etcd service documentation and configure TLS encryption.\nThen, edit the etcd pod specification file /etc/kubernetes/manifests/etcd.yaml\non the master node and set the below parameters.\n--cert-file=</path/to/ca-file>\n--key-file=</path/to/key-file>\n"
+            ],
+            "status": "PASS",
+            "actual_value": "root      3277  3218  3 Apr19 ?        03:57:52 etcd --advertise-client-urls=https://192.168.64.4:2379 --cert-file=/var/lib/minikube/certs/etcd/server.crt --client-cert-auth=true --data-dir=/var/lib/minikube/etcd --initial-advertise-peer-urls=https://192.168.64.4:2380 --initial-cluster=minikube=https://192.168.64.4:2380 --key-file=/var/lib/minikube/certs/etcd/server.key --listen-client-urls=https://127.0.0.1:2379,https://192.168.64.4:2379 --listen-metrics-urls=http://127.0.0.1:2381 --listen-peer-urls=https://192.168.64.4:2380 --name=minikube --peer-cert-file=/var/lib/minikube/certs/etcd/peer.crt --peer-client-cert-auth=true --peer-key-file=/var/lib/minikube/certs/etcd/peer.key --peer-trusted-ca-file=/var/lib/minikube/certs/etcd/ca.crt --snapshot-count=10000 --trusted-ca-file=/var/lib/minikube/certs/etcd/ca.crt\nroot      4624  4605  8 Apr21 ?        04:55:10 kube-apiserver --advertise-address=192.168.64.4 --allow-privileged=true --authorization-mode=Node,RBAC --client-ca-file=/var/lib/minikube/certs/ca.crt --enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota,PodSecurityPolicy --enable-bootstrap-token-auth=true --etcd-cafile=/var/lib/minikube/certs/etcd/ca.crt --etcd-certfile=/var/lib/minikube/certs/apiserver-etcd-client.crt --etcd-keyfile=/var/lib/minikube/certs/apiserver-etcd-client.key --etcd-servers=https://127.0.0.1:2379 --insecure-port=0 --kubelet-client-certificate=/var/lib/minikube/certs/apiserver-kubelet-client.crt --kubelet-client-key=/var/lib/minikube/certs/apiserver-kubelet-client.key --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname --proxy-client-cert-file=/var/lib/minikube/certs/front-proxy-client.crt --proxy-client-key-file=/var/lib/minikube/certs/front-proxy-client.key --requestheader-allowed-names=front-proxy-client --requestheader-client-ca-file=/var/lib/minikube/certs/front-proxy-ca.crt --requestheader-extra-headers-prefix=X-Remote-Extra- --requestheader-group-headers=X-Remote-Group --requestheader-username-headers=X-Remote-User --secure-port=8443 --service-account-key-file=/var/lib/minikube/certs/sa.pub --service-cluster-ip-range=10.96.0.0/12 --tls-cert-file=/var/lib/minikube/certs/apiserver.crt --tls-private-key-file=/var/lib/minikube/certs/apiserver.key\n",
+            "scored": true,
+            "expected_result": "'--cert-file' is present AND '--key-file' is present"
+          }
+        ]
+      }
+    ],
+    "total_pass": 7,
+    "total_fail": 0,
+    "total_warn": 0,
+    "total_info": 0
+  },
+  {
+    "id": "3",
+    "version": "1.5",
+    "text": "Control Plane Configuration",
+    "node_type": "controlplane",
+    "tests": [
+      {
+        "section": "3.1",
+        "pass": 0,
+        "fail": 0,
+        "warn": 1,
+        "info": 0,
+        "desc": "Authentication and Authorization",
+        "results": [
+          {
+            "test_number": "3.1.1",
+            "test_desc": "Client certificate authentication should not be used for users (Not Scored)",
+            "audit": "",
+            "AuditConfig": "",
+            "type": "manual",
+            "remediation": "Alternative mechanisms provided by Kubernetes such as the use of OIDC should be\nimplemented in place of client certificates.\n",
+            "test_info": [
+              "Alternative mechanisms provided by Kubernetes such as the use of OIDC should be\nimplemented in place of client certificates.\n"
+            ],
+            "status": "WARN",
+            "actual_value": "",
+            "scored": false,
+            "expected_result": "",
+            "reason": "Test marked as a manual test"
+          }
+        ]
+      }
+    ],
+    "total_pass": 0,
+    "total_fail": 0,
+    "total_warn": 3,
+    "total_info": 0
+  }
+]