diff --git a/check/check.go b/check/check.go index ab52f06..bc00f7b 100644 --- a/check/check.go +++ b/check/check.go @@ -310,4 +310,4 @@ func runAudit(audit string) (output string, err error) { glog.V(3).Infof("Output:\n %q", output) } return output, err -} +} \ No newline at end of file diff --git a/check/controls.go b/check/controls.go index 7725f56..7051ae8 100644 --- a/check/controls.go +++ b/check/controls.go @@ -33,7 +33,7 @@ const ( // UNKNOWN is when the AWS account can't be found UNKNOWN = "Unknown" // ARN for the AWS Security Hub service - ARN = "arn:aws:securityhub:%s::product/khulnasoft-security/kube-bench" + ARN = "arn:aws:securityhub:%s::product/aqua-security/kube-bench" // SCHEMA for the AWS Security Hub service SCHEMA = "2018-10-08" // TYPE is type of Security Hub finding @@ -237,7 +237,7 @@ func (controls *Controls) ASFF() ([]types.AwsSecurityFinding, error) { actualValue = check.ActualValue[0:1023] } - // Fix issue https://github.com/khulnasoft-lab/kube-bench/issues/903 + // Fix issue https://github.com/aquasecurity/kube-bench/issues/903 if len(check.Remediation) > 512 { remediation = check.Remediation[0:511] } @@ -327,4 +327,4 @@ func summarizeGroup(group *Group, state State) { default: glog.Warningf("Unrecognized state %s", state) } -} +} \ No newline at end of file diff --git a/check/controls_test.go b/check/controls_test.go index 8df0563..5e492c8 100644 --- a/check/controls_test.go +++ b/check/controls_test.go @@ -15,451 +15,231 @@ package check import ( - "bytes" - "encoding/json" - "encoding/xml" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "reflect" + "strings" "testing" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/securityhub/types" - "github.com/onsi/ginkgo/reporters" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "gopkg.in/yaml.v2" ) -const cfgDir = "../cfg/" - -type mockRunner struct { - mock.Mock -} - -func (m *mockRunner) Run(c *Check) State { - args := m.Called(c) - return args.Get(0).(State) -} - -// validate that the files we're shipping are valid YAML -func TestYamlFiles(t *testing.T) { - err := filepath.Walk(cfgDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - t.Fatalf("failure accessing path %q: %v\n", path, err) - } - if !info.IsDir() { - t.Logf("reading file: %s", path) - in, err := ioutil.ReadFile(path) - if err != nil { - t.Fatalf("error opening file %s: %v", path, err) - } - - c := new(Controls) - err = yaml.Unmarshal(in, c) - if err == nil { - t.Logf("YAML file successfully unmarshalled: %s", path) - } else { - t.Fatalf("failed to load YAML from %s: %v", path, err) - } - } - return nil - }) - if err != nil { - t.Fatalf("failure walking cfg dir: %v\n", err) +func TestCheck_Run(t *testing.T) { + type TestCase struct { + name string + check Check + Expected State } -} - -func TestNewControls(t *testing.T) { - t.Run("Should return error when node type is not specified", func(t *testing.T) { - // given - in := []byte(` ---- -controls: -type: # not specified -groups: -`) - // when - _, err := NewControls(MASTER, in, "") - // then - assert.EqualError(t, err, "non-master controls file specified") - }) - - t.Run("Should return error when input YAML is invalid", func(t *testing.T) { - // given - in := []byte("BOOM") - // when - _, err := NewControls(MASTER, in, "") - // then - assert.EqualError(t, err, "failed to unmarshal YAML: yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `BOOM` into check.Controls") - }) - -} - -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 - type: skip - 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) - // and - in := []byte(` ---- -type: "master" -groups: -- id: G1 - checks: - - id: G1/C1 -- id: G2 - checks: - - id: G2/C1 - text: "Verify that the SomeSampleFlag argument is set to true" - audit: "grep -B1 SomeSampleFlag=true /this/is/a/file/path" - tests: - test_items: - - flag: "SomeSampleFlag=true" - compare: - op: has - value: "true" - set: true - remediation: | - Edit the config file /this/is/a/file/path and set SomeSampleFlag to true. - scored: true -`) - // and - controls, err := NewControls(MASTER, in, "") - assert.NoError(t, err) - // and - runner.On("Run", controls.Groups[0].Checks[0]).Return(PASS) - runner.On("Run", controls.Groups[1].Checks[0]).Return(FAIL) - // and - var runAll Predicate = func(group *Group, c *Check) bool { - return true - } - var emptySkipList = make(map[string]bool, 0) - // when - controls.RunChecks(runner, runAll, emptySkipList) - // then - assert.Equal(t, 2, len(controls.Groups)) - // and - G1 := controls.Groups[0] - assert.Equal(t, "G1", G1.ID) - assert.Equal(t, "G1/C1", G1.Checks[0].ID) - assertEqualGroupSummary(t, 1, 0, 0, 0, G1) - // and - G2 := controls.Groups[1] - assert.Equal(t, "G2", G2.ID) - assert.Equal(t, "G2/C1", G2.Checks[0].ID) - assert.Equal(t, "has", G2.Checks[0].Tests.TestItems[0].Compare.Op) - assert.Equal(t, "true", G2.Checks[0].Tests.TestItems[0].Compare.Value) - assert.Equal(t, true, G2.Checks[0].Tests.TestItems[0].Set) - assert.Equal(t, "SomeSampleFlag=true", G2.Checks[0].Tests.TestItems[0].Flag) - assert.Equal(t, "Edit the config file /this/is/a/file/path and set SomeSampleFlag to true.\n", G2.Checks[0].Remediation) - assert.Equal(t, true, G2.Checks[0].Scored) - assertEqualGroupSummary(t, 0, 1, 0, 0, G2) - // and - assert.Equal(t, 1, controls.Summary.Pass) - assert.Equal(t, 1, controls.Summary.Fail) - assert.Equal(t, 0, controls.Summary.Info) - assert.Equal(t, 0, controls.Summary.Warn) - // and - runner.AssertExpectations(t) - }) -} + testCases := []TestCase{ + {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}, + { + name: "Unscored check that pass should PASS", + check: Check{ + Scored: false, + Audit: "echo hello", + Tests: &tests{TestItems: []*testItem{{ + Flag: "hello", + Set: true, + }}}, + }, + Expected: PASS, + }, -func TestControls_JUnitIncludesJSON(t *testing.T) { - testCases := []struct { - desc string - input *Controls - expect []byte - }{ + {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}, + { + name: "Scored check that doesn't pass should FAIL", + check: Check{ + Scored: true, + Audit: "echo hello", + Tests: &tests{TestItems: []*testItem{{ + Flag: "hello", + Set: false, + }}}, + }, + Expected: FAIL, + }, { - desc: "Serializes to junit", - input: &Controls{ - Groups: []*Group{ - { - ID: "g1", - Checks: []*Check{ - {ID: "check1id", Text: "check1text", State: PASS}, - }, - }, - }, + name: "Scored checks that pass should PASS", + check: Check{ + Scored: true, + Audit: "echo hello", + Tests: &tests{TestItems: []*testItem{{ + Flag: "hello", + Set: true, + }}}, }, - expect: []byte(` - - {"test_number":"check1id","test_desc":"check1text","audit":"","AuditEnv":"","AuditConfig":"","type":"","remediation":"","test_info":null,"status":"PASS","actual_value":"","scored":false,"IsMultiple":false,"expected_result":""} - -`), - }, { - desc: "Summary values come from summary not checks", - input: &Controls{ - Summary: Summary{ - Fail: 99, - Pass: 100, - Warn: 101, - Info: 102, - }, - Groups: []*Group{ - { - ID: "g1", - Checks: []*Check{ - {ID: "check1id", Text: "check1text", State: PASS}, - }, - }, - }, + Expected: PASS, + }, + { + name: "Scored checks that pass should PASS when config file is not present", + check: Check{ + Scored: true, + Audit: "echo hello", + AuditConfig: "/test/config.yaml", + Tests: &tests{TestItems: []*testItem{{ + Flag: "hello", + Set: true, + }}}, }, - expect: []byte(` - - {"test_number":"check1id","test_desc":"check1text","audit":"","AuditEnv":"","AuditConfig":"","type":"","remediation":"","test_info":null,"status":"PASS","actual_value":"","scored":false,"IsMultiple":false,"expected_result":""} - -`), - }, { - desc: "Warn and Info are considered skips and failed tests properly reported", - input: &Controls{ - Groups: []*Group{ - { - ID: "g1", - Checks: []*Check{ - {ID: "check1id", Text: "check1text", State: PASS}, - {ID: "check2id", Text: "check2text", State: INFO}, - {ID: "check3id", Text: "check3text", State: WARN}, - {ID: "check4id", Text: "check4text", State: FAIL}, - }, - }, - }, + Expected: PASS, + }, + { + name: "Scored checks that pass should FAIL when config file is not present", + check: Check{ + Scored: true, + AuditConfig: "/test/config.yaml", + Tests: &tests{TestItems: []*testItem{{ + Flag: "hello", + Set: true, + }}}, }, - expect: []byte(` - - {"test_number":"check1id","test_desc":"check1text","audit":"","AuditEnv":"","AuditConfig":"","type":"","remediation":"","test_info":null,"status":"PASS","actual_value":"","scored":false,"IsMultiple":false,"expected_result":""} - - - - {"test_number":"check2id","test_desc":"check2text","audit":"","AuditEnv":"","AuditConfig":"","type":"","remediation":"","test_info":null,"status":"INFO","actual_value":"","scored":false,"IsMultiple":false,"expected_result":""} - - - - {"test_number":"check3id","test_desc":"check3text","audit":"","AuditEnv":"","AuditConfig":"","type":"","remediation":"","test_info":null,"status":"WARN","actual_value":"","scored":false,"IsMultiple":false,"expected_result":""} - - - - {"test_number":"check4id","test_desc":"check4text","audit":"","AuditEnv":"","AuditConfig":"","type":"","remediation":"","test_info":null,"status":"FAIL","actual_value":"","scored":false,"IsMultiple":false,"expected_result":""} - -`), + Expected: FAIL, }, } - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - junitBytes, err := tc.input.JUnit() - if err != nil { - t.Fatalf("Failed to serialize to JUnit: %v", err) - } - var out reporters.JUnitTestSuite - if err := xml.Unmarshal(junitBytes, &out); err != nil { - t.Fatalf("Unable to deserialize from resulting JUnit: %v", err) + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + testCase.check.run() + if testCase.check.State != testCase.Expected { + t.Errorf("expected %s, actual %s", testCase.Expected, testCase.check.State) } + }) + } +} + +func TestCheckAuditEnv(t *testing.T) { + passingCases := []*Check{ + controls.Groups[2].Checks[0], + controls.Groups[2].Checks[2], + controls.Groups[2].Checks[3], + controls.Groups[2].Checks[4], + } - // Check that each check was serialized as json and stored as systemOut. - for iGroup, group := range tc.input.Groups { - for iCheck, check := range group.Checks { - jsonBytes, err := json.Marshal(check) - if err != nil { - t.Fatalf("Failed to serialize to JUnit: %v", err) - } + failingCases := []*Check{ + controls.Groups[2].Checks[1], + controls.Groups[2].Checks[5], + controls.Groups[2].Checks[6], + } - if out.TestCases[iGroup*iCheck+iCheck].SystemOut != string(jsonBytes) { - t.Errorf("Expected\n\t%v\n\tbut got\n\t%v", - out.TestCases[iGroup*iCheck+iCheck].SystemOut, - string(jsonBytes), - ) - } - } + for _, c := range passingCases { + t.Run(c.Text, func(t *testing.T) { + c.run() + if c.State != "PASS" { + t.Errorf("Should PASS, got: %v", c.State) } + }) + } - if !bytes.Equal(junitBytes, tc.expect) { - t.Errorf("Expected\n\t%v\n\tbut got\n\t%v", - string(tc.expect), - string(junitBytes), - ) + for _, c := range failingCases { + t.Run(c.Text, func(t *testing.T) { + c.run() + if c.State != "FAIL" { + t.Errorf("Should FAIL, got: %v", c.State) } }) } } -func assertEqualGroupSummary(t *testing.T, pass, fail, info, warn int, actual *Group) { - t.Helper() - assert.Equal(t, pass, actual.Pass) - assert.Equal(t, fail, actual.Fail) - assert.Equal(t, info, actual.Info) - assert.Equal(t, warn, actual.Warn) +func TestCheckAuditConfig(t *testing.T) { + passingCases := []*Check{ + controls.Groups[1].Checks[0], + controls.Groups[1].Checks[3], + controls.Groups[1].Checks[5], + controls.Groups[1].Checks[7], + controls.Groups[1].Checks[9], + controls.Groups[1].Checks[15], + } + + failingCases := []*Check{ + controls.Groups[1].Checks[1], + controls.Groups[1].Checks[2], + controls.Groups[1].Checks[4], + controls.Groups[1].Checks[6], + controls.Groups[1].Checks[8], + controls.Groups[1].Checks[10], + controls.Groups[1].Checks[11], + controls.Groups[1].Checks[12], + controls.Groups[1].Checks[13], + controls.Groups[1].Checks[14], + controls.Groups[1].Checks[16], + } + + for _, c := range passingCases { + t.Run(c.Text, func(t *testing.T) { + c.run() + if c.State != "PASS" { + t.Errorf("Should PASS, got: %v", c.State) + } + }) + } + + for _, c := range failingCases { + t.Run(c.Text, func(t *testing.T) { + c.run() + if c.State != "FAIL" { + t.Errorf("Should FAIL, got: %v", c.State) + } + }) + } } -func TestControls_ASFF(t *testing.T) { - type fields struct { - ID string - Version string - Text string - Groups []*Group - Summary Summary +func Test_runAudit(t *testing.T) { + type args struct { + audit string + output string } tests := []struct { - name string - fields fields - want []types.AwsSecurityFinding - wantErr bool + name string + args args + errMsg string + output string }{ { - name: "Test simple conversion", - fields: fields{ - ID: "test1", - Version: "1", - Text: "test runnner", - Summary: Summary{ - Fail: 99, - Pass: 100, - Warn: 101, - Info: 102, - }, - Groups: []*Group{ - { - ID: "g1", - Text: "Group text", - Checks: []*Check{ - {ID: "check1id", - Text: "check1text", - State: FAIL, - Remediation: "fix me", - Reason: "failed", - ExpectedResult: "failed", - ActualValue: "failed", - }, - }, - }, - }}, - want: []types.AwsSecurityFinding{ - { - AwsAccountId: aws.String("foo account"), - Confidence: *aws.Int32(100), - GeneratorId: aws.String(fmt.Sprintf("%s/cis-kubernetes-benchmark/%s/%s", fmt.Sprintf(ARN, "somewhere"), "1", "check1id")), - Description: aws.String("check1text"), - ProductArn: aws.String(fmt.Sprintf(ARN, "somewhere")), - SchemaVersion: aws.String(SCHEMA), - Title: aws.String(fmt.Sprintf("%s %s", "check1id", "check1text")), - Types: []string{*aws.String(TYPE)}, - Severity: &types.Severity{ - Label: types.SeverityLabelHigh, - }, - Remediation: &types.Remediation{ - Recommendation: &types.Recommendation{ - Text: aws.String("fix me"), - }, - }, - ProductFields: map[string]string{ - "Reason": "failed", - "Actual result": "failed", - "Expected result": "failed", - "Section": fmt.Sprintf("%s %s", "test1", "test runnner"), - "Subsection": fmt.Sprintf("%s %s", "g1", "Group text"), - }, - Resources: []types.Resource{ - { - Id: aws.String("foo Cluster"), - Type: aws.String(TYPE), - }, - }, - }, + name: "run success", + args: args{ + audit: "echo 'hello world'", }, - wantErr: false, + errMsg: "", + output: "hello world\n", + }, + { + name: "run multiple lines script", + args: args{ + audit: ` +hello() { + echo "hello world" +} + +hello +`, + }, + errMsg: "", + output: "hello world\n", + }, + { + name: "run failed", + args: args{ + audit: "unknown_command", + }, + errMsg: "failed to run: \"unknown_command\", output: \"/bin/sh: ", + output: "not found\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - viper.Set("AWS_ACCOUNT", "foo account") - viper.Set("CLUSTER_ARN", "foo Cluster") - viper.Set("AWS_REGION", "somewhere") - controls := &Controls{ - ID: tt.fields.ID, - Version: tt.fields.Version, - Text: tt.fields.Text, - Groups: tt.fields.Groups, - Summary: tt.fields.Summary, + var errMsg string + output, err := runAudit(tt.args.audit) + if err != nil { + errMsg = err.Error() + } + if errMsg != "" && !strings.Contains(errMsg, tt.errMsg) { + t.Errorf("name %s errMsg = %q, want %q", tt.name, errMsg, tt.errMsg) } - got, _ := controls.ASFF() - tt.want[0].CreatedAt = got[0].CreatedAt - tt.want[0].UpdatedAt = got[0].UpdatedAt - tt.want[0].Id = got[0].Id - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Controls.ASFF() = %v, want %v", got, tt.want) + if errMsg == "" && 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("name %s output = %q, want %q", tt.name, output, tt.output) } }) } -} +} \ No newline at end of file diff --git a/check/data b/check/data index fa3c2fe..6a3f116 100644 --- a/check/data +++ b/check/data @@ -733,4 +733,4 @@ groups: op: eq value: "correct" set: true - scored: true + scored: true \ No newline at end of file diff --git a/check/test.go b/check/test.go index 44ba464..6c777d4 100644 --- a/check/test.go +++ b/check/test.go @@ -443,4 +443,4 @@ func (t *testItem) UnmarshalYAML(unmarshal func(interface{}) error) error { } *t = testItem(newTestItem) return nil -} +} \ No newline at end of file diff --git a/check/test_test.go b/check/test_test.go index 7bbb721..5ab61bf 100644 --- a/check/test_test.go +++ b/check/test_test.go @@ -1405,4 +1405,4 @@ func TestExecuteJSONPathOnEncryptionConfig(t *testing.T) { } }) } -} +} \ No newline at end of file