From 937bfc7b2ee8e2fe86b26c89aa06326174ad0cbd Mon Sep 17 00:00:00 2001
From: Roberto Rojas <robertojrojas@gmail.com>
Date: Fri, 26 Jul 2019 11:11:59 -0700
Subject: [PATCH] =?UTF-8?q?issue=20#344:=20Adds=20support=20for=20array=20?=
 =?UTF-8?q?comparison.=20Every=20element=20in=20the=20s=E2=80=A6=20(#367)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* issue #344: Adds support for array comparison. Every element in the source array must exist in the target array.

* issue #344: Fixed typo and found if condition based on code review

* adds unit tests for valid_elements comparison

* removes spaces from split strings
---
 cfg/1.13-json/node.yaml |   4 +-
 check/test.go           |  56 ++++++++++++++++++-
 check/test_test.go      | 115 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/cfg/1.13-json/node.yaml b/cfg/1.13-json/node.yaml
index 3f7c2b2..f938bc2 100644
--- a/cfg/1.13-json/node.yaml
+++ b/cfg/1.13-json/node.yaml
@@ -296,9 +296,9 @@ groups:
     audit: "cat $kubeletconf"
     tests:
       test_items:
-      - path: "{.tlsCipherSuites}"
+      - path: "{range .tlsCipherSuites[:]}{}{','}{end}"
         compare:
-          op: eq
+          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/check/test.go b/check/test.go
index d79c72f..10c629b 100644
--- a/check/test.go
+++ b/check/test.go
@@ -37,8 +37,9 @@ import (
 type binOp string
 
 const (
-	and binOp = "and"
-	or        = "or"
+	and                   binOp = "and"
+	or                          = "or"
+	defaultArraySeparator       = ","
 )
 
 type testItem struct {
@@ -193,6 +194,13 @@ func compareOp(tCompareOp string, flagVal string, tCompareValue string) (string,
 		expectedResultPattern = " '%s' matched by '%s'"
 		opRe := regexp.MustCompile(tCompareValue)
 		testResult = opRe.MatchString(flagVal)
+
+	case "valid_elements":
+		expectedResultPattern = "'%s' contains valid elements from '%s'"
+		s := splitAndRemoveLastSeparator(flagVal, defaultArraySeparator)
+		target := splitAndRemoveLastSeparator(tCompareValue, defaultArraySeparator)
+		testResult = allElementsValid(s, target)
+
 	}
 
 	if expectedResultPattern == "" {
@@ -231,6 +239,50 @@ func executeJSONPath(path string, jsonInterface interface{}) (string, error) {
 	return jsonpathResult, nil
 }
 
+func allElementsValid(s, t []string) bool {
+	sourceEmpty := s == nil || len(s) == 0
+	targetEmpty := t == nil || len(t) == 0
+
+	if sourceEmpty && targetEmpty {
+		return true
+	}
+
+	// XOR comparison -
+	//     if either value is empty and the other is not empty,
+	//     not all elements are valid
+	if (sourceEmpty || targetEmpty) && !(sourceEmpty && targetEmpty) {
+		return false
+	}
+
+	for _, sv := range s {
+		found := false
+		for _, tv := range t {
+			if sv == tv {
+				found = true
+				break
+			}
+		}
+		if !found {
+			return false
+		}
+	}
+	return true
+}
+
+func splitAndRemoveLastSeparator(s, sep string) []string {
+	cleanS := strings.TrimRight(strings.TrimSpace(s), sep)
+	if len(cleanS) == 0 {
+		return []string{}
+	}
+
+	ts := strings.Split(cleanS, sep)
+	for i := range ts {
+		ts[i] = strings.TrimSpace(ts[i])
+	}
+
+	return ts
+}
+
 type tests struct {
 	TestItems []*testItem `yaml:"test_items"`
 	BinOp     binOp       `yaml:"bin_op"`
diff --git a/check/test_test.go b/check/test_test.go
index ab79fe9..bd168c2 100644
--- a/check/test_test.go
+++ b/check/test_test.go
@@ -323,6 +323,108 @@ func TestExecuteJSONPath(t *testing.T) {
 	}
 }
 
+func TestAllElementsValid(t *testing.T) {
+	cases := []struct {
+		source []string
+		target []string
+		valid  bool
+	}{
+		{
+			source: []string{},
+			target: []string{},
+			valid:  true,
+		},
+		{
+			source: []string{"blah"},
+			target: []string{},
+			valid:  false,
+		},
+		{
+			source: []string{},
+			target: []string{"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"},
+			valid: false,
+		},
+		{
+			source: []string{"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"},
+			target: []string{"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"},
+			valid: true,
+		},
+		{
+			source: []string{"blah"},
+			target: []string{"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"},
+			valid: false,
+		},
+		{
+			source: []string{"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "blah"},
+			target: []string{"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"},
+			valid: false,
+		},
+	}
+	for _, c := range cases {
+		if !allElementsValid(c.source, c.target) && c.valid {
+			t.Errorf("Not All Elements in %q are found in %q \n", c.source, c.target)
+		}
+	}
+}
+
+func TestSplitAndRemoveLastSeparator(t *testing.T) {
+	cases := []struct {
+		source     string
+		valid      bool
+		elementCnt int
+	}{
+		{
+			source:     "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",
+			valid:      true,
+			elementCnt: 8,
+		},
+		{
+			source:     "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,",
+			valid:      true,
+			elementCnt: 2,
+		},
+		{
+			source:     "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,",
+			valid:      true,
+			elementCnt: 2,
+		},
+		{
+			source:     "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, ",
+			valid:      true,
+			elementCnt: 2,
+		},
+		{
+			source:     " TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,",
+			valid:      true,
+			elementCnt: 2,
+		},
+	}
+
+	for _, c := range cases {
+		as := splitAndRemoveLastSeparator(c.source, defaultArraySeparator)
+		if len(as) == 0 && c.valid {
+			t.Errorf("Split did not work with %q \n", c.source)
+		}
+
+		if c.elementCnt != len(as) {
+			t.Errorf("Split did not work with %q expected: %d got: %d\n", c.source, c.elementCnt, len(as))
+		}
+
+	}
+}
+
 func TestCompareOp(t *testing.T) {
 	cases := []struct {
 		label                 string
@@ -527,6 +629,19 @@ func TestCompareOp(t *testing.T) {
 		{label: "op=gt, flagVal=empty", op: "regex", flagVal: "",
 			compareValue: "blah", expectedResultPattern: " '' matched by 'blah'",
 			testResult: false},
+
+		// Test Op "valid_elements"
+		{label: "op=valid_elements, valid_elements both empty", op: "valid_elements", flagVal: "",
+			compareValue: "", expectedResultPattern: "'' contains valid elements from ''",
+			testResult: true},
+
+		{label: "op=valid_elements, valid_elements flagVal empty", op: "valid_elements", flagVal: "",
+			compareValue: "a,b", expectedResultPattern: "'' contains valid elements from 'a,b'",
+			testResult: false},
+
+		{label: "op=valid_elements, valid_elements expectedResultPattern empty", op: "valid_elements", flagVal: "a,b",
+			compareValue: "", expectedResultPattern: "'a,b' contains valid elements from ''",
+			testResult: false},
 	}
 
 	for _, c := range cases {
-- 
GitLab