diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index f29d385..6375d97 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,3 +1,4 @@ +--- blank_issues_enabled: false contact_links: - name: Feature request diff --git a/cfg/cis-1.5/node.yaml b/cfg/cis-1.5/node.yaml index e70daf5..167fec8 100644 --- a/cfg/cis-1.5/node.yaml +++ b/cfg/cis-1.5/node.yaml @@ -236,6 +236,7 @@ groups: audit: "/bin/ps -fC $kubeletbin" audit_config: "/bin/cat $kubeletconf" tests: + bin_op: or test_items: - flag: "--read-only-port" path: '{.readOnlyPort}' @@ -243,6 +244,9 @@ groups: compare: op: eq value: 0 + - flag: "--read-only-port" + path: '{.readOnlyPort}' + set: false remediation: | If using a Kubelet config file, edit the file to set readOnlyPort to 0. If using command line arguments, edit the kubelet service file diff --git a/check/check.go b/check/check.go index 369ef2f..a5f3793 100644 --- a/check/check.go +++ b/check/check.go @@ -62,21 +62,23 @@ const ( // Check contains information about a recommendation in the // CIS Kubernetes document. type Check struct { - ID string `yaml:"id" json:"test_number"` - Text string `json:"test_desc"` - Audit string `json:"audit"` - AuditConfig string `yaml:"audit_config"` - Type string `json:"type"` - Tests *tests `json:"omit"` - Set bool `json:"omit"` - Remediation string `json:"remediation"` - TestInfo []string `json:"test_info"` - State `json:"status"` - ActualValue string `json:"actual_value"` - Scored bool `json:"scored"` - IsMultiple bool `yaml:"use_multiple_values"` - ExpectedResult string `json:"expected_result"` - Reason string `json:"reason,omitempty"` + ID string `yaml:"id" json:"test_number"` + Text string `json:"test_desc"` + Audit string `json:"audit"` + AuditConfig string `yaml:"audit_config"` + Type string `json:"type"` + Tests *tests `json:"omit"` + Set bool `json:"omit"` + Remediation string `json:"remediation"` + TestInfo []string `json:"test_info"` + State `json:"status"` + ActualValue string `json:"actual_value"` + Scored bool `json:"scored"` + IsMultiple bool `yaml:"use_multiple_values"` + ExpectedResult string `json:"expected_result"` + Reason string `json:"reason,omitempty"` + AuditOutput string `json:"omit"` + AuditConfigOutput string `json:"omit"` } // Runner wraps the basic Run method. @@ -123,63 +125,46 @@ func (c *Check) run() State { return c.State } - lastCommand := c.Audit - hasAuditConfig := c.AuditConfig != "" - - state, finalOutput, retErrmsgs := performTest(c.Audit, c.Tests, c.IsMultiple) - if len(state) > 0 { - c.Reason = retErrmsgs - c.State = state + // If there aren't any tests defined this is a FAIL or WARN + if c.Tests == nil || len(c.Tests.TestItems) == 0 { + c.Reason = "No tests defined" + if c.Scored { + c.State = FAIL + } else { + c.State = WARN + } return c.State } - errmsgs := retErrmsgs - - // If something went wrong with the 'Audit' command - // and an 'AuditConfig' command was provided, use it to - // execute tests - if (finalOutput == nil || !finalOutput.testResult) && hasAuditConfig { - lastCommand = c.AuditConfig - - nItems := len(c.Tests.TestItems) - // The reason we're creating a copy of the "tests" - // is so that tests can executed - // with the AuditConfig command - // against the Path only - currentTests := &tests{ - BinOp: c.Tests.BinOp, - TestItems: make([]*testItem, nItems), - } - for i := 0; i < nItems; i++ { - ti := c.Tests.TestItems[i] - nti := &testItem{ - // Path is used to test Command Param values - // AuditConfig ==> Path - Path: ti.Path, - Set: ti.Set, - Compare: ti.Compare, - } - currentTests.TestItems[i] = nti - } + // Command line parameters override the setting in the config file, so if we get a good result from the Audit command that's all we need to run + var finalOutput *testOutput + var lastCommand string - state, finalOutput, retErrmsgs = performTest(c.AuditConfig, currentTests, c.IsMultiple) - if len(state) > 0 { - c.Reason = retErrmsgs - c.State = state - return c.State - } - errmsgs += retErrmsgs + lastCommand, err := c.runAuditCommands() + if err == nil { + finalOutput, err = c.execute() } - if finalOutput != nil && finalOutput.testResult { - c.State = PASS + if finalOutput != nil { + if finalOutput.testResult { + c.State = PASS + } else { + if c.Scored { + c.State = FAIL + } else { + c.State = WARN + } + } + c.ActualValue = finalOutput.actualResult c.ExpectedResult = finalOutput.ExpectedResult - } else { + } + + if err != nil { + c.Reason = err.Error() if c.Scored { c.State = FAIL } else { - c.Reason = errmsgs c.State = WARN } } @@ -190,39 +175,97 @@ func (c *Check) run() State { glog.V(3).Infof("Check.ID: %s Command: %q TestResult: <> \n", c.ID, lastCommand) } - if errmsgs != "" { - glog.V(2).Info(errmsgs) + if c.Reason != "" { + glog.V(2).Info(c.Reason) } return c.State } -func performTest(audit string, tests *tests, isMultipleOutput bool) (State, *testOutput, string) { - if len(strings.TrimSpace(audit)) == 0 { - return "", failTestItem("missing command"), "missing audit command" +func (c *Check) runAuditCommands() (lastCommand string, err error) { + // Run the audit command and auditConfig commands, if present + c.AuditOutput, err = runAudit(c.Audit) + if err != nil { + return c.Audit, err } - var out bytes.Buffer - errmsgs := runAudit(audit, &out) + c.AuditConfigOutput, err = runAudit(c.AuditConfig) + return c.AuditConfig, err +} + +func (c *Check) execute() (finalOutput *testOutput, err error) { + finalOutput = &testOutput{} + + ts := c.Tests + res := make([]testOutput, len(ts.TestItems)) + expectedResultArr := make([]string, len(res)) + + glog.V(3).Infof("%d tests", len(ts.TestItems)) + for i, t := range ts.TestItems { + + t.isMultipleOutput = c.IsMultiple - finalOutput := tests.execute(out.String(), isMultipleOutput) - if finalOutput == nil { - errmsgs += fmt.Sprintf("Final output is <>. Failed to run: %s\n", audit) + // Try with the auditOutput first, and if that's not found, try the auditConfigOutput + t.isConfigSetting = false + result := *(t.execute(c.AuditOutput)) + if !result.flagFound { + t.isConfigSetting = true + result = *(t.execute(c.AuditConfigOutput)) + } + res[i] = result + expectedResultArr[i] = res[i].ExpectedResult } - return "", finalOutput, errmsgs + var result bool + // If no binary operation is specified, default to AND + switch ts.BinOp { + default: + glog.V(2).Info(fmt.Sprintf("unknown binary operator for tests %s\n", ts.BinOp)) + finalOutput.actualResult = fmt.Sprintf("unknown binary operator for tests %s\n", ts.BinOp) + return finalOutput, fmt.Errorf("unknown binary operator for tests %s", ts.BinOp) + case and, "": + result = true + for i := range res { + result = result && res[i].testResult + } + // Generate an AND expected result + finalOutput.ExpectedResult = strings.Join(expectedResultArr, " AND ") + + case or: + result = false + for i := range res { + result = result || res[i].testResult + } + // Generate an OR expected result + finalOutput.ExpectedResult = strings.Join(expectedResultArr, " OR ") + } + + finalOutput.testResult = result + finalOutput.actualResult = res[0].actualResult + + glog.V(3).Infof("Returning from execute on tests: finalOutput %#v", finalOutput) + return finalOutput, nil } -func runAudit(audit string, out *bytes.Buffer) string { - errmsgs := "" +func runAudit(audit string) (output string, err error) { + var out bytes.Buffer + + audit = strings.TrimSpace(audit) + if len(audit) == 0 { + return output, err + } cmd := exec.Command("/bin/sh") cmd.Stdin = strings.NewReader(audit) - cmd.Stdout = out - cmd.Stderr = out - if err := cmd.Run(); err != nil { - errmsgs += fmt.Sprintf("failed to run: %q, output: %q, error: %s\n", audit, out.String(), err) - } + cmd.Stdout = &out + cmd.Stderr = &out + err = cmd.Run() + output = out.String() + + if err != nil { + err = fmt.Errorf("failed to run: %q, output: %q, error: %s", audit, output, err) + } else { + glog.V(3).Infof("Command %q\n - Output:\n %q", audit, output) - glog.V(3).Infof("Command %q - Output:\n\n %q\n - Error Messages:%q \n", audit, out.String(), errmsgs) - return errmsgs + } + return output, err } diff --git a/check/check_test.go b/check/check_test.go index c6b93f8..e624408 100644 --- a/check/check_test.go +++ b/check/check_test.go @@ -15,38 +15,57 @@ package check import ( - "bytes" "strings" "testing" ) func TestCheck_Run(t *testing.T) { type TestCase struct { + name string check Check Expected State } testCases := []TestCase{ - {check: Check{Type: MANUAL}, Expected: WARN}, - {check: Check{Type: "skip"}, Expected: INFO}, - - {check: Check{Scored: false}, Expected: WARN}, // Not scored checks with no type, or not scored failing tests are marked warn + {name: "Manual check should WARN", check: Check{Type: MANUAL}, Expected: WARN}, + {name: "Skip check should INFO", check: Check{Type: "skip"}, Expected: INFO}, + {name: "Unscored check (with no type) should WARN on failure", check: Check{Scored: false}, Expected: WARN}, { - check: Check{ // Not scored checks with passing tests are marked pass + name: "Unscored check that pass should PASS", + check: Check{ Scored: false, - Audit: ":", - Tests: &tests{TestItems: []*testItem{&testItem{}}}, + Audit: "echo hello", + Tests: &tests{TestItems: []*testItem{{ + Flag: "hello", + Set: true, + }}}, }, Expected: PASS, }, - {check: Check{Scored: true}, Expected: WARN}, // If there are no tests in the check, warn - {check: Check{Scored: true, Tests: &tests{}}, Expected: FAIL}, // If there are tests that are not passing, fail + {name: "Check with no tests should WARN", check: Check{Scored: true}, Expected: WARN}, + {name: "Scored check with empty tests should FAIL", check: Check{Scored: true, Tests: &tests{}}, Expected: FAIL}, { - check: Check{ // Scored checks with passing tests are marked pass + name: "Scored check that doesn't pass should FAIL", + check: Check{ Scored: true, - Audit: ":", - Tests: &tests{TestItems: []*testItem{&testItem{}}}, + Audit: "echo hello", + Tests: &tests{TestItems: []*testItem{{ + Flag: "hello", + Set: false, + }}, + }}, + Expected: FAIL, + }, + { + name: "Scored checks that pass should PASS", + check: Check{ + Scored: true, + Audit: "echo hello", + Tests: &tests{TestItems: []*testItem{{ + Flag: "hello", + Set: true, + }}}, }, Expected: PASS, }, @@ -56,7 +75,7 @@ func TestCheck_Run(t *testing.T) { testCase.check.run() if testCase.check.State != testCase.Expected { - t.Errorf("test failed, expected %s, actual %s\n", testCase.Expected, testCase.check.State) + t.Errorf("%s: expected %s, actual %s\n", testCase.name, testCase.Expected, testCase.check.State) } } } @@ -115,6 +134,26 @@ func TestCheckAuditConfig(t *testing.T) { controls.Groups[1].Checks[11], "FAIL", }, + { + controls.Groups[1].Checks[12], + "FAIL", + }, + { + controls.Groups[1].Checks[13], + "FAIL", + }, + { + controls.Groups[1].Checks[14], + "FAIL", + }, + { + controls.Groups[1].Checks[15], + "PASS", + }, + { + controls.Groups[1].Checks[16], + "FAIL", + }, } for _, c := range cases { @@ -128,7 +167,6 @@ func TestCheckAuditConfig(t *testing.T) { func Test_runAudit(t *testing.T) { type args struct { audit string - out *bytes.Buffer output string } tests := []struct { @@ -141,7 +179,6 @@ func Test_runAudit(t *testing.T) { name: "run success", args: args{ audit: "echo 'hello world'", - out: &bytes.Buffer{}, }, errMsg: "", output: "hello world\n", @@ -156,7 +193,6 @@ hello() { hello `, - out: &bytes.Buffer{}, }, errMsg: "", output: "hello world\n", @@ -165,7 +201,6 @@ hello name: "run failed", args: args{ audit: "unknown_command", - out: &bytes.Buffer{}, }, errMsg: "failed to run: \"unknown_command\", output: \"/bin/sh: ", output: "not found\n", @@ -173,16 +208,19 @@ hello } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - errMsg := runAudit(tt.args.audit, tt.args.out) + var errMsg string + output, err := runAudit(tt.args.audit) + if err != nil { + errMsg = err.Error() + } if errMsg != "" && !strings.Contains(errMsg, tt.errMsg) { - t.Errorf("runAudit() errMsg = %q, want %q", errMsg, tt.errMsg) + t.Errorf("name %s errMsg = %q, want %q", tt.name, errMsg, tt.errMsg) } - output := tt.args.out.String() if errMsg == "" && output != tt.output { - t.Errorf("runAudit() output = %q, want %q", output, tt.output) + t.Errorf("name %s output = %q, want %q", tt.name, output, tt.output) } if errMsg != "" && !strings.Contains(output, tt.output) { - t.Errorf("runAudit() output = %q, want %q", output, tt.output) + t.Errorf("name %s output = %q, want %q", tt.name, output, tt.output) } }) } diff --git a/check/data b/check/data index 33d6ce8..6df4cf2 100644 --- a/check/data +++ b/check/data @@ -166,7 +166,7 @@ groups: op: eq value: some-val set: true - + - id: 15 text: "jsonpath correct value on field" tests: @@ -476,3 +476,84 @@ groups: value: "600" set: true scored: true + - id: 12 + text: "audit is present and wrong, audit_config is right -> fail (command line parameters override config file)" + audit: "echo flag=wrong" + audit_config: "echo 'flag: correct'" + tests: + test_items: + - flag: "flag" + path: "{.flag}" + compare: + op: eq + value: "correct" + set: true + scored: true + - id: 13 + text: "parameter and config file don't have same default - parameter has failing value" + audit: "echo '--read-only-port=1'" + audit_config: "echo 'readOnlyPort: 0'" + tests: + bin_op: and + test_items: + - flag: "--read-only-port" + path: "{.readOnlyPort}" + set: true + compare: + op: eq + value: 0 + - flag: "--read-only-port" + path: '{.readOnlyPort}' + set: false + scored: true + - id: 14 + text: "parameter and config file don't have same default - config file has failing value" + audit: "echo ''" + audit_config: "echo 'readOnlyPort: 1'" + tests: + bin_op: or + test_items: + - flag: "--read-only-port" + path: '{.readOnlyPort}' + set: true + compare: + op: eq + value: 0 + - flag: "--read-only-port" + path: '{.readOnlyPort}' + set: false + scored: true + - id: 15 + text: "parameter and config file don't have same default - passing" + audit: "echo ''" + audit_config: "echo ''" + tests: + bin_op: or + test_items: + - flag: "--read-only-port" + path: '{.readOnlyPort}' + set: true + compare: + op: eq + value: 0 + - flag: "--read-only-port" + path: '{.readOnlyPort}' + set: false + scored: true + - id: 15 + text: "parameter and config file don't have same default - parameter has bad value and config is not present - failing" + audit: "echo '--read-only-port=1'" + audit_config: "echo ''" + tests: + bin_op: or + test_items: + - flag: "--read-only-port" + path: '{.readOnlyPort}' + set: true + compare: + op: eq + value: 0 + - flag: "--read-only-port" + path: '{.readOnlyPort}' + set: false + scored: true diff --git a/check/test.go b/check/test.go index 0df8eba..165c602 100644 --- a/check/test.go +++ b/check/test.go @@ -43,15 +43,25 @@ const ( defaultArraySeparator = "," ) +type tests struct { + TestItems []*testItem `yaml:"test_items"` + BinOp binOp `yaml:"bin_op"` +} + type testItem struct { - Flag string - Path string - Output string - Value string - Set bool - Compare compare + Flag string + Path string + Output string + Value string + Set bool + Compare compare + isMultipleOutput bool + isConfigSetting bool } +type pathTestItem testItem +type flagTestItem testItem + type compare struct { Op string Value string @@ -59,6 +69,7 @@ type compare struct { type testOutput struct { testResult bool + flagFound bool actualResult string ExpectedResult string } @@ -67,99 +78,124 @@ func failTestItem(s string) *testOutput { return &testOutput{testResult: false, actualResult: s} } -func (t *testItem) execute(s string, isMultipleOutput bool) *testOutput { - result := &testOutput{} - s = strings.TrimRight(s, " \n") +func (t testItem) flagValue() string { + if t.isConfigSetting { + return t.Path + } - // If the test has output that should be evaluated for each row - if isMultipleOutput { - output := strings.Split(s, "\n") - for _, op := range output { - result = t.evaluate(op) - // If the test failed for the current row, no need to keep testing for this output - if !result.testResult { - break + return t.Flag +} + +func (t testItem) findValue(s string) (match bool, value string, err error) { + if t.isConfigSetting { + pt := pathTestItem(t) + return pt.findValue(s) + } + + ft := flagTestItem(t) + return ft.findValue(s) +} + +func (t flagTestItem) findValue(s string) (match bool, value string, err error) { + if s == "" || t.Flag == "" { + return + } + match = strings.Contains(s, t.Flag) + if match { + // Expects flags in the form; + // --flag=somevalue + // flag: somevalue + // --flag + // somevalue + pttn := `(` + t.Flag + `)(=|: *)*([^\s]*) *` + flagRe := regexp.MustCompile(pttn) + vals := flagRe.FindStringSubmatch(s) + + if len(vals) > 0 { + if vals[3] != "" { + value = vals[3] + } else { + // --bool-flag + if strings.HasPrefix(t.Flag, "--") { + value = "true" + } else { + value = vals[1] + } } + } else { + err = fmt.Errorf("invalid flag in testItem definition: %s", s) } - } else { - result = t.evaluate(s) } + glog.V(3).Infof("In flagTestItem.findValue %s, match %v, s %s, t.Flag %s", value, match, s, t.Flag) - return result + return match, value, err +} + +func (t pathTestItem) findValue(s string) (match bool, value string, err error) { + var jsonInterface interface{} + + err = unmarshal(s, &jsonInterface) + if err != nil { + return false, "", fmt.Errorf("failed to load YAML or JSON from input \"%s\": %v", s, err) + } + + value, err = executeJSONPath(t.Path, &jsonInterface) + if err != nil { + return false, "", fmt.Errorf("unable to parse path expression \"%s\": %v", t.Path, err) + } + + glog.V(3).Infof("In pathTestItem.findValue %s", value) + match = (value != "") + return match, value, err } -func (t *testItem) evaluate(s string) *testOutput { +func (t testItem) execute(s string) *testOutput { result := &testOutput{} - var match bool - var flagVal string + s = strings.TrimRight(s, " \n") - if t.Flag != "" { - // Flag comparison: check if the flag is present in the input - match = strings.Contains(s, t.Flag) + // If the test has output that should be evaluated for each row + var output []string + if t.isMultipleOutput { + output = strings.Split(s, "\n") } else { - // Path != "" - we don't know whether it's YAML or JSON but - // we can just try one then the other - var jsonInterface interface{} - - if t.Path != "" { - err := unmarshal(s, &jsonInterface) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to load YAML or JSON from provided input \"%s\": %v\n", s, err) - return failTestItem("failed to load YAML or JSON") - } + output = []string{s} + } + for _, op := range output { + result = t.evaluate(op) + // If the test failed for the current row, no need to keep testing for this output + if !result.testResult { + break } + } - jsonpathResult, err := executeJSONPath(t.Path, &jsonInterface) - if err != nil { - fmt.Fprintf(os.Stderr, "unable to parse path expression \"%s\": %v\n", t.Path, err) - return failTestItem("error executing path expression") - } - match = (jsonpathResult != "") - flagVal = jsonpathResult + return result +} + +func (t testItem) evaluate(s string) *testOutput { + result := &testOutput{} + + match, value, err := t.findValue(s) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + return failTestItem(err.Error()) } if t.Set { - isset := match - - if isset && t.Compare.Op != "" { - if t.Flag != "" { - // Expects flags in the form; - // --flag=somevalue - // flag: somevalue - // --flag - // somevalue - pttn := `(` + t.Flag + `)(=|: *)*([^\s]*) *` - flagRe := regexp.MustCompile(pttn) - vals := flagRe.FindStringSubmatch(s) - - if len(vals) > 0 { - if vals[3] != "" { - flagVal = vals[3] - } else { - // --bool-flag - if strings.HasPrefix(t.Flag, "--") { - flagVal = "true" - } else { - flagVal = vals[1] - } - } - } else { - glog.V(1).Infof(fmt.Sprintf("invalid flag in testitem definition")) - return failTestItem("error invalid flag in testitem definition") - } - } - - result.ExpectedResult, result.testResult = compareOp(t.Compare.Op, flagVal, t.Compare.Value) + if match && t.Compare.Op != "" { + result.ExpectedResult, result.testResult = compareOp(t.Compare.Op, value, t.Compare.Value) } else { - result.ExpectedResult = fmt.Sprintf("'%s' is present", t.Flag) - result.testResult = isset + result.ExpectedResult = fmt.Sprintf("'%s' is present", t.flagValue()) + result.testResult = match } } else { - result.ExpectedResult = fmt.Sprintf("'%s' is not present", t.Flag) - notset := !match - result.testResult = notset + result.ExpectedResult = fmt.Sprintf("'%s' is not present", t.flagValue()) + result.testResult = !match } + + result.flagFound = match + glog.V(3).Info(fmt.Sprintf("flagFound %v", result.flagFound)) + return result } @@ -326,66 +362,6 @@ func splitAndRemoveLastSeparator(s, sep string) []string { return ts } -type tests struct { - TestItems []*testItem `yaml:"test_items"` - BinOp binOp `yaml:"bin_op"` -} - -func (ts *tests) execute(s string, isMultipleOutput bool) *testOutput { - finalOutput := &testOutput{} - - // If no tests are defined return with empty finalOutput. - // This may be the case for checks of type: "skip". - if ts == nil { - return finalOutput - } - - res := make([]testOutput, len(ts.TestItems)) - if len(res) == 0 { - return finalOutput - } - - expectedResultArr := make([]string, len(res)) - - for i, t := range ts.TestItems { - res[i] = *(t.execute(s, isMultipleOutput)) - expectedResultArr[i] = res[i].ExpectedResult - } - - var result bool - // If no binary operation is specified, default to AND - switch ts.BinOp { - default: - glog.V(2).Info(fmt.Sprintf("unknown binary operator for tests %s\n", ts.BinOp)) - finalOutput.actualResult = fmt.Sprintf("unknown binary operator for tests %s\n", ts.BinOp) - return finalOutput - case and, "": - result = true - for i := range res { - result = result && res[i].testResult - } - // Generate an AND expected result - finalOutput.ExpectedResult = strings.Join(expectedResultArr, " AND ") - - case or: - result = false - for i := range res { - result = result || res[i].testResult - } - // Generate an OR expected result - finalOutput.ExpectedResult = strings.Join(expectedResultArr, " OR ") - } - - finalOutput.testResult = result - finalOutput.actualResult = res[0].actualResult - - if finalOutput.actualResult == "" { - finalOutput.actualResult = s - } - - return finalOutput -} - func toNumeric(a, b string) (c, d int, err error) { c, err = strconv.Atoi(strings.TrimSpace(a)) if err != nil { diff --git a/check/test_test.go b/check/test_test.go index 933d451..4521a99 100644 --- a/check/test_test.go +++ b/check/test_test.go @@ -48,143 +48,181 @@ func TestTestExecute(t *testing.T) { cases := []struct { *Check - str string + str string + strConfig string }{ { controls.Groups[0].Checks[0], "2:45 ../kubernetes/kube-apiserver --allow-privileged=false --option1=20,30,40", + "", }, { controls.Groups[0].Checks[1], "2:45 ../kubernetes/kube-apiserver --allow-privileged=false", + "", }, { controls.Groups[0].Checks[2], "niinai 13617 2635 99 19:26 pts/20 00:03:08 ./kube-apiserver --insecure-port=0 --anonymous-auth", + "", }, { controls.Groups[0].Checks[3], "2:45 ../kubernetes/kube-apiserver --secure-port=0 --audit-log-maxage=40 --option", + "", }, { controls.Groups[0].Checks[4], "2:45 ../kubernetes/kube-apiserver --max-backlog=20 --secure-port=0 --audit-log-maxage=40 --option", + "", }, { controls.Groups[0].Checks[5], "2:45 ../kubernetes/kube-apiserver --option --admission-control=WebHook,RBAC ---audit-log-maxage=40", + "", }, { controls.Groups[0].Checks[6], "2:45 .. --kubelet-clientkey=foo --kubelet-client-certificate=bar --admission-control=Webhook,RBAC", + "", }, { controls.Groups[0].Checks[7], "2:45 .. --secure-port=0 --kubelet-client-certificate=bar --admission-control=Webhook,RBAC", + "", }, { controls.Groups[0].Checks[8], "644", + "", }, { controls.Groups[0].Checks[9], "640", + "", }, { controls.Groups[0].Checks[9], "600", + "", }, { controls.Groups[0].Checks[10], "2:45 ../kubernetes/kube-apiserver --option --admission-control=WebHook,RBAC ---audit-log-maxage=40", + "", }, { controls.Groups[0].Checks[11], "2:45 ../kubernetes/kube-apiserver --option --admission-control=WebHook,RBAC ---audit-log-maxage=40", + "", }, { controls.Groups[0].Checks[12], "2:45 ../kubernetes/kube-apiserver --option --admission-control=WebHook,Something,RBAC ---audit-log-maxage=40", + "", }, { controls.Groups[0].Checks[13], "2:45 ../kubernetes/kube-apiserver --option --admission-control=Something ---audit-log-maxage=40", + "", }, { // check for ':' as argument-value separator, with space between arg and val controls.Groups[0].Checks[14], "2:45 kube-apiserver some-arg: some-val --admission-control=Something ---audit-log-maxage=40", + "", }, { // check for ':' as argument-value separator, with no space between arg and val controls.Groups[0].Checks[14], "2:45 kube-apiserver some-arg:some-val --admission-control=Something ---audit-log-maxage=40", + "", }, { controls.Groups[0].Checks[15], + "", "{\"readOnlyPort\": 15000}", }, { controls.Groups[0].Checks[16], + "", "{\"stringValue\": \"WebHook,Something,RBAC\"}", }, { controls.Groups[0].Checks[17], + "", "{\"trueValue\": true}", }, { controls.Groups[0].Checks[18], + "", "{\"readOnlyPort\": 15000}", }, { controls.Groups[0].Checks[19], + "", "{\"authentication\": { \"anonymous\": {\"enabled\": false}}}", }, { controls.Groups[0].Checks[20], + "", "readOnlyPort: 15000", }, { controls.Groups[0].Checks[21], + "", "readOnlyPort: 15000", }, { controls.Groups[0].Checks[22], + "", "authentication:\n anonymous:\n enabled: false", }, { controls.Groups[0].Checks[26], + "", "currentMasterVersion: 1.12.7", }, { controls.Groups[0].Checks[27], "--peer-client-cert-auth", + "", }, { controls.Groups[0].Checks[27], "--abc=true --peer-client-cert-auth --efg=false", + "", }, { controls.Groups[0].Checks[27], "--abc --peer-client-cert-auth --efg", + "", }, { controls.Groups[0].Checks[27], "--peer-client-cert-auth=true", + "", }, { controls.Groups[0].Checks[27], "--abc --peer-client-cert-auth=true --efg", + "", }, { controls.Groups[0].Checks[28], "--abc --peer-client-cert-auth=false --efg", + "", }, } for _, c := range cases { - res := c.Tests.execute(c.str, c.IsMultiple).testResult - if !res { + c.Check.AuditOutput = c.str + c.Check.AuditConfigOutput = c.strConfig + res, err := c.Check.execute() + if err != nil { + t.Errorf(err.Error()) + } + if !res.testResult { t.Errorf("%s, expected:%v, got:%v\n", c.Text, true, res) } } @@ -219,8 +257,12 @@ func TestTestExecuteExceptions(t *testing.T) { } for _, c := range cases { - res := c.Tests.execute(c.str, c.IsMultiple).testResult - if res { + c.Check.AuditConfigOutput = c.str + res, err := c.Check.execute() + if err != nil { + t.Errorf(err.Error()) + } + if res.testResult { t.Errorf("%s, expected:%v, got:%v\n", c.Text, false, res) } } diff --git a/integration/testdata/cis-1.5/job-node.data b/integration/testdata/cis-1.5/job-node.data index 843a247..15d0a44 100644 --- a/integration/testdata/cis-1.5/job-node.data +++ b/integration/testdata/cis-1.5/job-node.data @@ -14,7 +14,7 @@ [PASS] 4.2.1 Ensure that the --anonymous-auth argument is set to false (Scored) [PASS] 4.2.2 Ensure that the --authorization-mode argument is not set to AlwaysAllow (Scored) [PASS] 4.2.3 Ensure that the --client-ca-file argument is set as appropriate (Scored) -[FAIL] 4.2.4 Ensure that the --read-only-port argument is set to 0 (Scored) +[PASS] 4.2.4 Ensure that the --read-only-port argument is set to 0 (Scored) [PASS] 4.2.5 Ensure that the --streaming-connection-idle-timeout argument is not set to 0 (Scored) [FAIL] 4.2.6 Ensure that the --protect-kernel-defaults argument is set to true (Scored) [PASS] 4.2.7 Ensure that the --make-iptables-util-chains argument is set to true (Scored) @@ -33,15 +33,6 @@ chmod 644 /etc/kubernetes/proxy.conf 4.1.4 Run the below command (based on the file location on your system) on the each worker node. For example, chown root:root /etc/kubernetes/proxy.conf -4.2.4 If using a Kubelet config file, edit the file to set readOnlyPort to 0. -If using command line arguments, edit the kubelet service file -/etc/systemd/system/kubelet.service.d/10-kubeadm.conf on each worker node and -set the below parameter in KUBELET_SYSTEM_PODS_ARGS variable. ---read-only-port=0 -Based on your system, restart the kubelet service. For example: -systemctl daemon-reload -systemctl restart kubelet.service - 4.2.6 If using a Kubelet config file, edit the file to set protectKernelDefaults: true. If using command line arguments, edit the kubelet service file /etc/systemd/system/kubelet.service.d/10-kubeadm.conf on each worker node and @@ -80,7 +71,7 @@ systemctl restart kubelet.service == Summary == -16 checks PASS -6 checks FAIL +17 checks PASS +5 checks FAIL 1 checks WARN 0 checks INFO diff --git a/integration/testdata/cis-1.5/job.data b/integration/testdata/cis-1.5/job.data index fdfd568..9fcaabe 100644 --- a/integration/testdata/cis-1.5/job.data +++ b/integration/testdata/cis-1.5/job.data @@ -227,7 +227,7 @@ minimum. [PASS] 4.2.1 Ensure that the --anonymous-auth argument is set to false (Scored) [PASS] 4.2.2 Ensure that the --authorization-mode argument is not set to AlwaysAllow (Scored) [PASS] 4.2.3 Ensure that the --client-ca-file argument is set as appropriate (Scored) -[FAIL] 4.2.4 Ensure that the --read-only-port argument is set to 0 (Scored) +[PASS] 4.2.4 Ensure that the --read-only-port argument is set to 0 (Scored) [PASS] 4.2.5 Ensure that the --streaming-connection-idle-timeout argument is not set to 0 (Scored) [FAIL] 4.2.6 Ensure that the --protect-kernel-defaults argument is set to true (Scored) [PASS] 4.2.7 Ensure that the --make-iptables-util-chains argument is set to true (Scored) @@ -246,15 +246,6 @@ chmod 644 /etc/kubernetes/proxy.conf 4.1.4 Run the below command (based on the file location on your system) on the each worker node. For example, chown root:root /etc/kubernetes/proxy.conf -4.2.4 If using a Kubelet config file, edit the file to set readOnlyPort to 0. -If using command line arguments, edit the kubelet service file -/etc/systemd/system/kubelet.service.d/10-kubeadm.conf on each worker node and -set the below parameter in KUBELET_SYSTEM_PODS_ARGS variable. ---read-only-port=0 -Based on your system, restart the kubelet service. For example: -systemctl daemon-reload -systemctl restart kubelet.service - 4.2.6 If using a Kubelet config file, edit the file to set protectKernelDefaults: true. If using command line arguments, edit the kubelet service file /etc/systemd/system/kubelet.service.d/10-kubeadm.conf on each worker node and @@ -293,8 +284,8 @@ systemctl restart kubelet.service == Summary == -16 checks PASS -6 checks FAIL +17 checks PASS +5 checks FAIL 1 checks WARN 0 checks INFO [INFO] 5 Kubernetes Policies