From aa2a6f08f3ff3cae311d32b743fd7a4e3b665293 Mon Sep 17 00:00:00 2001
From: Wicked <jason_attwood@hotmail.co.uk>
Date: Thu, 29 Oct 2020 10:12:45 +0000
Subject: [PATCH] Add exit-code parameter for when checks have failed (#734)

* Add int command to specify exit code wih a default of 0

* Re-structured to add tests

* Refactor exit code selection
---
 cmd/common.go                              | 10 +++
 cmd/common_test.go                         | 18 +++++
 cmd/root.go                                |  4 ++
 cmd/testdata/passedControlsCollection.json | 77 ++++++++++++++++++++++
 4 files changed, 109 insertions(+)
 create mode 100644 cmd/testdata/passedControlsCollection.json

diff --git a/cmd/common.go b/cmd/common.go
index 9e33098..55b5c35 100644
--- a/cmd/common.go
+++ b/cmd/common.go
@@ -345,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/common_test.go b/cmd/common_test.go
index 774c4aa..4f04c28 100644
--- a/cmd/common_test.go
+++ b/cmd/common_test.go
@@ -535,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/root.go b/cmd/root.go
index 41334a7..327f2e7 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -48,6 +48,7 @@ var (
 	controlplaneFile    = "controlplane.yaml"
 	policiesFile        = "policies.yaml"
 	managedservicesFile = "managedservices.yaml"
+	exitCode            int
 	noResults           bool
 	noSummary           bool
 	noRemediations      bool
@@ -124,6 +125,8 @@ var RootCmd = &cobra.Command{
 		}
 
 		writeOutput(controlsCollection)
+		exitCode := exitCodeSelection(controlsCollection)
+		os.Exit(exitCode)
 	},
 }
 
@@ -146,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")
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
+  }
+]
-- 
GitLab