// Copyright © 2017-2019 Aqua Security Software Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package check import ( "bytes" "encoding/json" "encoding/xml" "fmt" "os" "path/filepath" "reflect" "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 := os.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 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) }) } func TestControls_JUnitIncludesJSON(t *testing.T) { testCases := []struct { desc string input *Controls expect []byte }{ { desc: "Serializes to junit", input: &Controls{ Groups: []*Group{ { ID: "g1", Checks: []*Check{ {ID: "check1id", Text: "check1text", State: PASS}, }, }, }, }, 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}, }, }, }, }, 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}, }, }, }, }, 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":""} `), }, } 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) } // 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) } 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), ) } } } if !bytes.Equal(junitBytes, tc.expect) { t.Errorf("Expected\n\t%v\n\tbut got\n\t%v", string(tc.expect), string(junitBytes), ) } }) } } 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 TestControls_ASFF(t *testing.T) { type fields struct { ID string Version string Text string Groups []*Group Summary Summary } tests := []struct { name string fields fields want []types.AwsSecurityFinding wantErr bool }{ { 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), }, }, }, }, wantErr: false, }, } 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, } 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) } }) } }