diff --git a/cmd/scan/image.go b/cmd/scan/image.go index f6799cb90d..2d27828a43 100644 --- a/cmd/scan/image.go +++ b/cmd/scan/image.go @@ -25,6 +25,9 @@ var ( # Scan the 'nginx' image and see the full report %[1]s scan image "nginx" -v + # Scan the 'nginx' image and use exceptions + %[1]s scan image "nginx" -E exceptions.json + `, cautils.ExecName()) ) diff --git a/core/core/image_scan.go b/core/core/image_scan.go index 8e00a53d59..0953b73e0e 100644 --- a/core/core/image_scan.go +++ b/core/core/image_scan.go @@ -44,6 +44,7 @@ type VulnerabilitiesIgnorePolicy struct { Severities []string `json:"severities"` } +// Loads excpetion policies from exceptions json object. func GetImageExceptionsFromFile(filePath string) ([]VulnerabilitiesIgnorePolicy, error) { // Read the JSON file jsonFile, err := os.ReadFile(filePath) @@ -61,6 +62,7 @@ func GetImageExceptionsFromFile(filePath string) ([]VulnerabilitiesIgnorePolicy, return policies, nil } +// This function will identify the registry, organization and image tag from the image name func getAttributesFromImage(imgName string) (Attributes, error) { canonicalImageName, err := cautils.NormalizeImageName(imgName) if err != nil { @@ -90,6 +92,7 @@ func getAttributesFromImage(imgName string) (Attributes, error) { return attributes, nil } +// Checks if the target string matches the regex pattern func regexStringMatch(pattern, target string) bool { re, err := regexp.Compile(pattern) if err != nil { @@ -104,6 +107,9 @@ func regexStringMatch(pattern, target string) bool { return false } +// Compares the registry, organization, image name, image tag against the targets specified +// in the exception policy object to check if the image being scanned qualifies for an +// exception policy. func isTargetImage(targets []Target, attributes Attributes) bool { for _, target := range targets { return regexStringMatch(target.Attributes.Registry, attributes.Registry) && regexStringMatch(target.Attributes.Organization, attributes.Organization) && regexStringMatch(target.Attributes.ImageName, attributes.ImageName) && regexStringMatch(target.Attributes.ImageTag, attributes.ImageTag) @@ -112,6 +118,8 @@ func isTargetImage(targets []Target, attributes Attributes) bool { return false } +// Generates a list of unique CVE-IDs and the severities which are to be excluded for +// the image being scanned. func getUniqueVulnerabilitiesAndSeverities(policies []VulnerabilitiesIgnorePolicy, image string) ([]string, []string) { // Create maps with slices as values to store unique vulnerabilities and severities (case-insensitive) uniqueVulns := make(map[string][]string) diff --git a/core/core/image_scan_test.go b/core/core/image_scan_test.go new file mode 100644 index 0000000000..f4d86a2c0f --- /dev/null +++ b/core/core/image_scan_test.go @@ -0,0 +1,378 @@ +package core + +import ( + "context" + "testing" + + "github.com/kubescape/kubescape/v3/core/cautils" + ksmetav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1" + "github.com/stretchr/testify/assert" +) + +func TestGetImageExceptionsFromFile(t *testing.T) { + tests := []struct { + filePath string + expectedPolicies []VulnerabilitiesIgnorePolicy + expectedErr error + }{ + { + filePath: "./testdata/exceptions.json", + expectedPolicies: []VulnerabilitiesIgnorePolicy{ + { + Metadata: Metadata{ + Name: "medium-severity-vulnerabilites-exceptions", + }, + Kind: "VulnerabilitiesIgnorePolicy", + Targets: []Target{ + { + DesignatorType: "Attributes", + Attributes: Attributes{ + Registry: "docker.io", + Organization: "", + ImageName: "", + ImageTag: "", + }, + }, + }, + Vulnerabilities: []string{}, + Severities: []string{"medium"}, + }, + { + Metadata: Metadata{ + Name: "exclude-allowed-hostPath-control", + }, + Kind: "VulnerabilitiesIgnorePolicy", + Targets: []Target{ + { + DesignatorType: "Attributes", + Attributes: Attributes{ + Registry: "", + Organization: "", + ImageName: "", + ImageTag: "", + }, + }, + }, + Vulnerabilities: []string{"CVE-2023-42366", "CVE-2023-42365"}, + Severities: []string{"critical", "low"}, + }, + }, + expectedErr: nil, + }, + { + filePath: "./testdata/empty_exceptions.json", + expectedPolicies: []VulnerabilitiesIgnorePolicy{}, + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.filePath, func(t *testing.T) { + policies, err := GetImageExceptionsFromFile(tt.filePath) + assert.Equal(t, tt.expectedPolicies, policies) + assert.Equal(t, tt.expectedErr, err) + }) + } +} + +func TestGetAttributesFromImage(t *testing.T) { + tests := []struct { + imageName string + expectedAttributes Attributes + expectedErr error + }{ + { + imageName: "quay.io/kubescape/kubescape-cli:v3.0.0", + expectedAttributes: Attributes{ + Registry: "quay.io", + Organization: "kubescape", + ImageName: "kubescape-cli", + ImageTag: "v3.0.0", + }, + expectedErr: nil, + }, + { + imageName: "alpine", + expectedAttributes: Attributes{ + Registry: "docker.io", + Organization: "library", + ImageName: "alpine", + ImageTag: "latest", + }, + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.imageName, func(t *testing.T) { + attributes, err := getAttributesFromImage(tt.imageName) + assert.Equal(t, tt.expectedErr, err) + assert.Equal(t, tt.expectedAttributes, attributes) + }) + } +} + +func TestRegexStringMatch(t *testing.T) { + tests := []struct { + pattern string + target string + expected bool + }{ + { + pattern: ".*", + target: "quay.io", + expected: true, + }, + { + pattern: "kubescape", + target: "kubescape", + expected: true, + }, + { + pattern: "kubescape*", + target: "kubescape-cli", + expected: true, + }, + { + pattern: "", + target: "v3.0.0", + expected: true, + }, + { + pattern: "docker.io", + target: "quay.io", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.target+"/"+tt.pattern, func(t *testing.T) { + assert.Equal(t, tt.expected, regexStringMatch(tt.pattern, tt.target)) + }) + } +} + +func TestIsTargetImage(t *testing.T) { + tests := []struct { + targets []Target + attributes Attributes + expected bool + }{ + { + targets: []Target{ + { + Attributes: Attributes{ + Registry: "docker.io", + Organization: ".*", + ImageName: ".*", + ImageTag: "", + }, + }, + }, + attributes: Attributes{ + Registry: "quay.io", + Organization: "kubescape", + ImageName: "kubescape-cli", + ImageTag: "v3.0.0", + }, + expected: false, + }, + { + targets: []Target{ + { + Attributes: Attributes{ + Registry: "quay.io", + Organization: "kubescape", + ImageName: "kubescape*", + ImageTag: "", + }, + }, + }, + attributes: Attributes{ + Registry: "quay.io", + Organization: "kubescape", + ImageName: "kubescape-cli", + ImageTag: "v3.0.0", + }, + expected: true, + }, + { + targets: []Target{ + { + Attributes: Attributes{ + Registry: "docker.io", + Organization: "library", + ImageName: "alpine", + ImageTag: "", + }, + }, + }, + attributes: Attributes{ + Registry: "docker.io", + Organization: "library", + ImageName: "alpine", + ImageTag: "latest", + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.attributes.Registry+"/"+tt.attributes.ImageName, func(t *testing.T) { + assert.Equal(t, tt.expected, isTargetImage(tt.targets, tt.attributes)) + }) + } +} + +func TestGetVulnerabilitiesAndSeverities(t *testing.T) { + tests := []struct { + policies []VulnerabilitiesIgnorePolicy + image string + expectedVulnerabilities []string + expectedSeverities []string + }{ + { + policies: []VulnerabilitiesIgnorePolicy{ + { + Metadata: Metadata{ + Name: "vulnerabilites-exceptions", + }, + Kind: "VulnerabilitiesIgnorePolicy", + Targets: []Target{ + { + DesignatorType: "Attributes", + Attributes: Attributes{ + Registry: "", + Organization: "kubescape*", + ImageName: "", + ImageTag: "", + }, + }, + }, + Vulnerabilities: []string{"CVE-2023-42365"}, + Severities: []string{}, + }, + { + Metadata: Metadata{ + Name: "exclude-allowed-hostPath-control", + }, + Kind: "VulnerabilitiesIgnorePolicy", + Targets: []Target{ + { + DesignatorType: "Attributes", + Attributes: Attributes{ + Registry: "docker.io", + Organization: "", + ImageName: "", + ImageTag: "", + }, + }, + }, + Vulnerabilities: []string{"CVE-2023-42366", "CVE-2023-42365"}, + Severities: []string{"critical", "low"}, + }, + }, + image: "quay.io/kubescape/kubescape-cli:v3.0.0", + expectedVulnerabilities: []string{"CVE-2023-42365"}, + expectedSeverities: []string{}, + }, + { + policies: []VulnerabilitiesIgnorePolicy{ + { + Metadata: Metadata{ + Name: "medium-severity-vulnerabilites-exceptions", + }, + Kind: "VulnerabilitiesIgnorePolicy", + Targets: []Target{ + { + DesignatorType: "Attributes", + Attributes: Attributes{ + Registry: "docker.io", + Organization: "", + ImageName: "", + ImageTag: "", + }, + }, + }, + Vulnerabilities: []string{}, + Severities: []string{"medium"}, + }, + { + Metadata: Metadata{ + Name: "exclude-allowed-hostPath-control", + }, + Kind: "VulnerabilitiesIgnorePolicy", + Targets: []Target{ + { + DesignatorType: "Attributes", + Attributes: Attributes{ + Registry: "", + Organization: "", + ImageName: "", + ImageTag: "", + }, + }, + }, + Vulnerabilities: []string{"CVE-2023-42366", "CVE-2023-42365"}, + Severities: []string{"critical", "low"}, + }, + }, + image: "alpine", + expectedVulnerabilities: []string{"CVE-2023-42366", "CVE-2023-42365"}, + expectedSeverities: []string{"MEDIUM", "CRITICAL", "LOW"}, + }, + } + + for _, tt := range tests { + t.Run(tt.image, func(t *testing.T) { + vulnerabilities, severities := getUniqueVulnerabilitiesAndSeverities(tt.policies, tt.image) + assert.Equal(t, tt.expectedVulnerabilities, vulnerabilities) + assert.Equal(t, tt.expectedSeverities, severities) + }) + } +} + +func TestScanImage(t *testing.T) { + ctx := context.Background() + tests := []struct { + name string + imgScanInfo *ksmetav1.ImageScanInfo + scanInfo *cautils.ScanInfo + ignoredMatchesPresent bool + expectedErr error + }{ + { + name: "alpine-with-exceptions", + imgScanInfo: &ksmetav1.ImageScanInfo{ + Image: "alpine", + Exceptions: "./testdata/exceptions.json", + }, + scanInfo: &cautils.ScanInfo{}, + ignoredMatchesPresent: true, + expectedErr: nil, + }, + { + name: "alpine-without-exceptions", + imgScanInfo: &ksmetav1.ImageScanInfo{ + Image: "alpine", + Exceptions: "./testdata/empty_exceptions.json", + }, + scanInfo: &cautils.ScanInfo{}, + ignoredMatchesPresent: false, + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ks := NewKubescape() + pconfig, err := ks.ScanImage(ctx, tt.imgScanInfo, tt.scanInfo) + if tt.ignoredMatchesPresent { + assert.NotEmpty(t, pconfig.IgnoredMatches, err) + } else { + assert.Empty(t, pconfig.IgnoredMatches, err) + } + assert.Equal(t, tt.expectedErr, err) + }) + } +} diff --git a/core/core/testdata/empty_exceptions.json b/core/core/testdata/empty_exceptions.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/core/core/testdata/empty_exceptions.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/core/core/testdata/exceptions.json b/core/core/testdata/exceptions.json new file mode 100644 index 0000000000..403d036bb5 --- /dev/null +++ b/core/core/testdata/exceptions.json @@ -0,0 +1,44 @@ +[ + { + "metadata": { + "name": "medium-severity-vulnerabilites-exceptions" + }, + "kind": "VulnerabilitiesIgnorePolicy", + "targets": [ + { + "designatorType": "Attributes", + "attributes": { + "Registry": "docker.io", + "Organization": "", + "ImageName": "" + } + } + ], + "vulnerabilities": [ + ], + "severities": [ + "medium" + ] + }, + { + "metadata": { + "name": "exclude-allowed-hostPath-control" + }, + "kind": "VulnerabilitiesIgnorePolicy", + "targets": [ + { + "designatorType": "Attributes", + "attributes": { + } + } + ], + "vulnerabilities": [ + "CVE-2023-42366", + "CVE-2023-42365" + ], + "severities": [ + "critical", + "low" + ] + } +] diff --git a/core/meta/datastructures/v1/image_scan.go b/core/meta/datastructures/v1/image_scan.go index 17e11b1ace..3bf3bb6caf 100644 --- a/core/meta/datastructures/v1/image_scan.go +++ b/core/meta/datastructures/v1/image_scan.go @@ -1,9 +1,8 @@ package v1 type ImageScanInfo struct { - Username string - Password string - Image string - Exceptions string - CanonicalImageName string + Username string + Password string + Image string + Exceptions string }