diff --git a/cmd/grype/cli/commands/db_search.go b/cmd/grype/cli/commands/db_search.go index 0056f0a51597..de58f6e1c7fa 100644 --- a/cmd/grype/cli/commands/db_search.go +++ b/cmd/grype/cli/commands/db_search.go @@ -18,6 +18,7 @@ import ( v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" + "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" @@ -242,7 +243,7 @@ func legacyDBSearchPackages(opts dbSearchMatchOptions, vulnerabilityIDs []string var vulnerabilities []vulnerability.Vulnerability for _, vulnerabilityID := range vulnerabilityIDs { - vulns, err := str.Get(vulnerabilityID, "") + vulns, err := str.FindVulnerabilities(search.ByID(vulnerabilityID)) if err != nil { return fmt.Errorf("unable to get vulnerability %q: %w", vulnerabilityID, err) } @@ -305,7 +306,7 @@ func renderDBSearchPackagesTableRows(structuredRows []dbsearch.AffectedPackage) ranges = append(ranges, ra.Version.Constraint) } rangeStr := strings.Join(ranges, " || ") - rows = append(rows, []string{rr.Vulnerability.ID, pkgOrCPE, ecosystem, v5Namespace(rr), rangeStr}) + rows = append(rows, []string{rr.Vulnerability.ID, pkgOrCPE, ecosystem, mimicV5Namespace(rr), rangeStr}) } // sort rows by each column @@ -321,70 +322,6 @@ func renderDBSearchPackagesTableRows(structuredRows []dbsearch.AffectedPackage) return rows } -// v5Namespace returns the namespace for a given affected package based on what schema v5 did. -func v5Namespace(row dbsearch.AffectedPackage) string { - switch row.Vulnerability.Provider { - case "nvd": - return "nvd:cpe" - case "github": - language := row.Package.Ecosystem - // normalize from purl type, github ecosystem types, and vunnel mappings - switch strings.ToLower(row.Package.Ecosystem) { - case "golang", "go-module": - language = "go" - case "composer", "php-composer": - language = "php" - case "cargo", "rust-crate": - language = "rust" - case "dart-pub", "pub": - language = "dart" - case "nuget": - language = "dotnet" - case "maven": - language = "java" - case "swifturl": - language = "swift" - case "npm", "node": - language = "javascript" - case "pypi", "pip": - language = "python" - case "rubygems", "gem": - language = "ruby" - } - return fmt.Sprintf("github:language:%s", language) - } - if row.OS != nil { - // distro family fixes - family := row.OS.Name - switch row.OS.Name { - case "amazon": - family = "amazonlinux" - case "mariner": - switch row.OS.Version { - case "1.0", "2.0": - family = "mariner" - default: - family = "azurelinux" - } - case "oracle": - family = "oraclelinux" - } - - // provider fixes - pr := row.Vulnerability.Provider - if pr == "rhel" { - pr = "redhat" - } - - // version fixes - ver := row.OS.Version - switch row.Vulnerability.Provider { - case "rhel", "oracle": - // ensure we only keep the major version - ver = strings.Split(row.OS.Version, ".")[0] - } - - return fmt.Sprintf("%s:distro:%s:%s", pr, family, ver) - } - return row.Vulnerability.Provider +func mimicV5Namespace(row dbsearch.AffectedPackage) string { + return v6.MimicV5Namespace(&row.Vulnerability.Model, row.Model) } diff --git a/cmd/grype/cli/commands/db_search_test.go b/cmd/grype/cli/commands/db_search_test.go index 2550bca31e98..203ab0158967 100644 --- a/cmd/grype/cli/commands/db_search_test.go +++ b/cmd/grype/cli/commands/db_search_test.go @@ -5,10 +5,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch" "github.com/anchore/grype/cmd/grype/cli/options" ) @@ -106,388 +104,3 @@ func TestDBSearchMatchOptionsApplyArgs(t *testing.T) { }) } } -func TestV5Namespace(t *testing.T) { - // provider input should be derived from the Providers table: - // +------------+---------+---------------+----------------------------------+------------------------+ - // | id | version | processor | date_captured | input_digest | - // +------------+---------+---------------+----------------------------------+------------------------+ - // | nvd | 2 | vunnel@0.29.0 | 2025-01-08 01:32:55.179881+00:00 | xxh64:0a160d2b53dd0208 | - // | alpine | 1 | vunnel@0.29.0 | 2025-01-08 01:31:28.824872+00:00 | xxh64:30c5b7b8efa0c087 | - // | amazon | 1 | vunnel@0.29.0 | 2025-01-08 01:31:28.837469+00:00 | xxh64:7d90b3fa66b183bc | - // | chainguard | 1 | vunnel@0.29.0 | 2025-01-08 01:31:26.969865+00:00 | xxh64:25a82fa97ac9e077 | - // | debian | 1 | vunnel@0.29.0 | 2025-01-08 01:31:50.718966+00:00 | xxh64:4b1834b9e4e68987 | - // | github | 1 | vunnel@0.29.0 | 2025-01-08 01:31:27.450124+00:00 | xxh64:a3ee6b48d37a0124 | - // | mariner | 1 | vunnel@0.29.0 | 2025-01-08 01:32:35.005761+00:00 | xxh64:cb4f5861a1fda0af | - // | oracle | 1 | vunnel@0.29.0 | 2025-01-08 01:32:33.696274+00:00 | xxh64:72c0a15731e96ab3 | - // | rhel | 1 | vunnel@0.29.0 | 2025-01-08 01:32:32.192345+00:00 | xxh64:abf5d2fd5a26c194 | - // | sles | 1 | vunnel@0.29.0 | 2025-01-08 01:32:42.988937+00:00 | xxh64:8f558f8f28a04489 | - // | ubuntu | 3 | vunnel@0.29.0 | 2025-01-08 01:33:25.795537+00:00 | xxh64:97ef8421c0093620 | - // | wolfi | 1 | vunnel@0.29.0 | 2025-01-08 01:32:58.571417+00:00 | xxh64:f294f3474d35b1a9 | - // +------------+---------+---------------+----------------------------------+------------------------+ - - // the expected results should mimic what is found as v5 namespace values: - // +--------------------------------------+ - // | namespace | - // +--------------------------------------+ - // | nvd:cpe | - // | github:language:javascript | - // | ubuntu:distro:ubuntu:14.04 | - // | ubuntu:distro:ubuntu:16.04 | - // | ubuntu:distro:ubuntu:18.04 | - // | ubuntu:distro:ubuntu:20.04 | - // | ubuntu:distro:ubuntu:22.04 | - // | ubuntu:distro:ubuntu:22.10 | - // | ubuntu:distro:ubuntu:23.04 | - // | ubuntu:distro:ubuntu:23.10 | - // | ubuntu:distro:ubuntu:24.10 | - // | debian:distro:debian:8 | - // | debian:distro:debian:9 | - // | ubuntu:distro:ubuntu:12.04 | - // | ubuntu:distro:ubuntu:15.04 | - // | sles:distro:sles:15 | - // | sles:distro:sles:15.1 | - // | sles:distro:sles:15.2 | - // | sles:distro:sles:15.3 | - // | sles:distro:sles:15.4 | - // | sles:distro:sles:15.5 | - // | sles:distro:sles:15.6 | - // | amazon:distro:amazonlinux:2 | - // | debian:distro:debian:10 | - // | debian:distro:debian:11 | - // | debian:distro:debian:12 | - // | debian:distro:debian:unstable | - // | oracle:distro:oraclelinux:6 | - // | oracle:distro:oraclelinux:7 | - // | oracle:distro:oraclelinux:8 | - // | oracle:distro:oraclelinux:9 | - // | redhat:distro:redhat:6 | - // | redhat:distro:redhat:7 | - // | redhat:distro:redhat:8 | - // | redhat:distro:redhat:9 | - // | ubuntu:distro:ubuntu:12.10 | - // | ubuntu:distro:ubuntu:13.04 | - // | ubuntu:distro:ubuntu:14.10 | - // | ubuntu:distro:ubuntu:15.10 | - // | ubuntu:distro:ubuntu:16.10 | - // | ubuntu:distro:ubuntu:17.04 | - // | ubuntu:distro:ubuntu:17.10 | - // | ubuntu:distro:ubuntu:18.10 | - // | ubuntu:distro:ubuntu:19.04 | - // | ubuntu:distro:ubuntu:19.10 | - // | ubuntu:distro:ubuntu:20.10 | - // | ubuntu:distro:ubuntu:21.04 | - // | ubuntu:distro:ubuntu:21.10 | - // | ubuntu:distro:ubuntu:24.04 | - // | github:language:php | - // | debian:distro:debian:13 | - // | debian:distro:debian:7 | - // | redhat:distro:redhat:5 | - // | sles:distro:sles:11.1 | - // | sles:distro:sles:11.3 | - // | sles:distro:sles:11.4 | - // | sles:distro:sles:11.2 | - // | sles:distro:sles:12 | - // | sles:distro:sles:12.1 | - // | sles:distro:sles:12.2 | - // | sles:distro:sles:12.3 | - // | sles:distro:sles:12.4 | - // | sles:distro:sles:12.5 | - // | chainguard:distro:chainguard:rolling | - // | wolfi:distro:wolfi:rolling | - // | github:language:go | - // | alpine:distro:alpine:3.20 | - // | alpine:distro:alpine:3.21 | - // | alpine:distro:alpine:edge | - // | github:language:rust | - // | github:language:python | - // | sles:distro:sles:11 | - // | oracle:distro:oraclelinux:5 | - // | github:language:ruby | - // | github:language:dotnet | - // | alpine:distro:alpine:3.12 | - // | alpine:distro:alpine:3.13 | - // | alpine:distro:alpine:3.14 | - // | alpine:distro:alpine:3.15 | - // | alpine:distro:alpine:3.16 | - // | alpine:distro:alpine:3.17 | - // | alpine:distro:alpine:3.18 | - // | alpine:distro:alpine:3.19 | - // | mariner:distro:mariner:2.0 | - // | github:language:java | - // | github:language:dart | - // | amazon:distro:amazonlinux:2023 | - // | alpine:distro:alpine:3.10 | - // | alpine:distro:alpine:3.11 | - // | alpine:distro:alpine:3.4 | - // | alpine:distro:alpine:3.5 | - // | alpine:distro:alpine:3.7 | - // | alpine:distro:alpine:3.8 | - // | alpine:distro:alpine:3.9 | - // | mariner:distro:azurelinux:3.0 | - // | mariner:distro:mariner:1.0 | - // | alpine:distro:alpine:3.3 | - // | alpine:distro:alpine:3.6 | - // | amazon:distro:amazonlinux:2022 | - // | alpine:distro:alpine:3.2 | - // | github:language:swift | - // +--------------------------------------+ - - type testCase struct { - name string - provider string // from Providers.id - ecosystem string // only used when provider is "github" - osName string // only used for OS-based providers - osVersion string // only used for OS-based providers - expected string - } - - tests := []testCase{ - // NVD - { - name: "nvd provider", - provider: "nvd", - expected: "nvd:cpe", - }, - - // GitHub ecosystem tests - { - name: "github golang direct", - provider: "github", - ecosystem: "golang", - expected: "github:language:go", - }, - { - name: "github go-module ecosystem", - provider: "github", - ecosystem: "go-module", - expected: "github:language:go", - }, - { - name: "github composer ecosystem", - provider: "github", - ecosystem: "composer", - expected: "github:language:php", - }, - { - name: "github php-composer ecosystem", - provider: "github", - ecosystem: "php-composer", - expected: "github:language:php", - }, - { - name: "github cargo ecosystem", - provider: "github", - ecosystem: "cargo", - expected: "github:language:rust", - }, - { - name: "github rust-crate ecosystem", - provider: "github", - ecosystem: "rust-crate", - expected: "github:language:rust", - }, - { - name: "github pub ecosystem", - provider: "github", - ecosystem: "pub", - expected: "github:language:dart", - }, - { - name: "github dart-pub ecosystem", - provider: "github", - ecosystem: "dart-pub", - expected: "github:language:dart", - }, - { - name: "github nuget ecosystem", - provider: "github", - ecosystem: "nuget", - expected: "github:language:dotnet", - }, - { - name: "github maven ecosystem", - provider: "github", - ecosystem: "maven", - expected: "github:language:java", - }, - { - name: "github swifturl ecosystem", - provider: "github", - ecosystem: "swifturl", - expected: "github:language:swift", - }, - { - name: "github npm ecosystem", - provider: "github", - ecosystem: "npm", - expected: "github:language:javascript", - }, - { - name: "github node ecosystem", - provider: "github", - ecosystem: "node", - expected: "github:language:javascript", - }, - { - name: "github pypi ecosystem", - provider: "github", - ecosystem: "pypi", - expected: "github:language:python", - }, - { - name: "github pip ecosystem", - provider: "github", - ecosystem: "pip", - expected: "github:language:python", - }, - { - name: "github rubygems ecosystem", - provider: "github", - ecosystem: "rubygems", - expected: "github:language:ruby", - }, - { - name: "github gem ecosystem", - provider: "github", - ecosystem: "gem", - expected: "github:language:ruby", - }, - - // OS Distribution tests - { - name: "ubuntu distribution", - provider: "ubuntu", - osName: "ubuntu", - osVersion: "22.04", - expected: "ubuntu:distro:ubuntu:22.04", - }, - { - name: "redhat distribution", - provider: "rhel", - osName: "redhat", - osVersion: "8", - expected: "redhat:distro:redhat:8", - }, - { - name: "debian distribution", - provider: "debian", - osName: "debian", - osVersion: "11", - expected: "debian:distro:debian:11", - }, - { - name: "sles distribution", - provider: "sles", - osName: "sles", - osVersion: "15.5", - expected: "sles:distro:sles:15.5", - }, - { - name: "alpine distribution", - provider: "alpine", - osName: "alpine", - osVersion: "3.18", - expected: "alpine:distro:alpine:3.18", - }, - { - name: "chainguard distribution", - provider: "chainguard", - osName: "chainguard", - osVersion: "rolling", - expected: "chainguard:distro:chainguard:rolling", - }, - { - name: "wolfi distribution", - provider: "wolfi", - osName: "wolfi", - osVersion: "rolling", - expected: "wolfi:distro:wolfi:rolling", - }, - { - name: "amazon linux distribution", - provider: "amazon", - osName: "amazon", - osVersion: "2023", - expected: "amazon:distro:amazonlinux:2023", - }, - { - name: "mariner regular version", - provider: "mariner", - osName: "mariner", - osVersion: "2.0", - expected: "mariner:distro:mariner:2.0", - }, - { - name: "mariner azure version", - provider: "mariner", - osName: "mariner", - osVersion: "3.0", - expected: "mariner:distro:azurelinux:3.0", - }, - { - name: "oracle linux distribution", - provider: "oracle", - osName: "oracle", - osVersion: "8", - expected: "oracle:distro:oraclelinux:8", - }, - - // Version truncation tests - { - name: "rhel with minor version", - provider: "rhel", - osName: "redhat", - osVersion: "8.6", - expected: "redhat:distro:redhat:8", - }, - { - name: "rhel with patch version", - provider: "rhel", - osName: "redhat", - osVersion: "9.2.1", - expected: "redhat:distro:redhat:9", - }, - { - name: "oracle with minor version", - provider: "oracle", - osName: "oracle", - osVersion: "8.7", - expected: "oracle:distro:oraclelinux:8", - }, - { - name: "oracle with patch version", - provider: "oracle", - osName: "oracle", - osVersion: "9.3.1", - expected: "oracle:distro:oraclelinux:9", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - input := dbsearch.AffectedPackage{ - Vulnerability: dbsearch.VulnerabilityInfo{ - Provider: tt.provider, - }, - } - - // Add OS info for OS-based providers - if tt.osName != "" { - input.AffectedPackageInfo.OS = &dbsearch.OperatingSystem{ - Name: tt.osName, - Version: tt.osVersion, - } - } - - // Add package info for GitHub provider - if tt.provider == "github" { - input.AffectedPackageInfo.Package = &dbsearch.Package{ - Ecosystem: tt.ecosystem, - } - } - - result := v5Namespace(input) - assert.Equal(t, tt.expected, result) - }) - } -} diff --git a/cmd/grype/cli/commands/internal/dbsearch/affected_packages.go b/cmd/grype/cli/commands/internal/dbsearch/affected_packages.go index 9a325830559e..7269290dcc2a 100644 --- a/cmd/grype/cli/commands/internal/dbsearch/affected_packages.go +++ b/cmd/grype/cli/commands/internal/dbsearch/affected_packages.go @@ -21,6 +21,9 @@ type AffectedPackage struct { } type AffectedPackageInfo struct { + // TODO: remove this when namespace is no longer used + Model *v6.AffectedPackageHandle `json:"-"` // tracking package handle info is necessary for namespace lookup (note CPE handles are not tracked) + // OS identifies the operating system release that the affected package is released for OS *OperatingSystem `json:"os,omitempty"` @@ -68,7 +71,8 @@ type AffectedPackagesOptions struct { } func newAffectedPackageRows(affectedPkgs []v6.AffectedPackageHandle, affectedCPEs []v6.AffectedCPEHandle) (rows []AffectedPackage) { - for _, pkg := range affectedPkgs { + for i := range affectedPkgs { + pkg := affectedPkgs[i] var detail v6.AffectedPackageBlob if pkg.BlobValue != nil { detail = *pkg.BlobValue @@ -81,6 +85,7 @@ func newAffectedPackageRows(affectedPkgs []v6.AffectedPackageHandle, affectedCPE rows = append(rows, AffectedPackage{ Vulnerability: newVulnerabilityInfo(*pkg.Vulnerability), AffectedPackageInfo: AffectedPackageInfo{ + Model: &pkg, OS: toOS(pkg.OperatingSystem), Package: toPackage(pkg.Package), Detail: detail, @@ -105,6 +110,7 @@ func newAffectedPackageRows(affectedPkgs []v6.AffectedPackageHandle, affectedCPE } rows = append(rows, AffectedPackage{ + // tracking model information is not possible with CPE handles Vulnerability: newVulnerabilityInfo(*ac.Vulnerability), AffectedPackageInfo: AffectedPackageInfo{ CPE: c, diff --git a/cmd/grype/cli/commands/internal/dbsearch/affected_packages_test.go b/cmd/grype/cli/commands/internal/dbsearch/affected_packages_test.go index 796a14e1e530..3c49b02a808b 100644 --- a/cmd/grype/cli/commands/internal/dbsearch/affected_packages_test.go +++ b/cmd/grype/cli/commands/internal/dbsearch/affected_packages_test.go @@ -212,7 +212,7 @@ func TestNewAffectedPackageRows(t *testing.T) { }, } - if diff := cmp.Diff(expected, rows); diff != "" { + if diff := cmp.Diff(expected, rows, cmpOpts()...); diff != "" { t.Errorf("unexpected rows (-want +got):\n%s", diff) } } @@ -344,7 +344,7 @@ func TestAffectedPackages(t *testing.T) { }, } - if diff := cmp.Diff(expected, results); diff != "" { + if diff := cmp.Diff(expected, results, cmpOpts()...); diff != "" { t.Errorf("unexpected results (-want +got):\n%s", diff) } } diff --git a/cmd/grype/cli/commands/internal/dbsearch/matches.go b/cmd/grype/cli/commands/internal/dbsearch/matches.go index 360a415a268a..12185fe4175c 100644 --- a/cmd/grype/cli/commands/internal/dbsearch/matches.go +++ b/cmd/grype/cli/commands/internal/dbsearch/matches.go @@ -45,7 +45,8 @@ func newMatchesRows(affectedPkgs []v6.AffectedPackageHandle, affectedCPEs []v6.A var affectedPkgsByVuln = make(map[v6.ID][]AffectedPackageInfo) var vulnsByID = make(map[v6.ID]v6.VulnerabilityHandle) - for _, pkg := range affectedPkgs { + for i := range affectedPkgs { + pkg := affectedPkgs[i] var detail v6.AffectedPackageBlob if pkg.BlobValue != nil { detail = *pkg.BlobValue @@ -59,6 +60,7 @@ func newMatchesRows(affectedPkgs []v6.AffectedPackageHandle, affectedCPEs []v6.A } aff := AffectedPackageInfo{ + Model: &pkg, OS: toOS(pkg.OperatingSystem), Package: toPackage(pkg.Package), Detail: detail, @@ -88,6 +90,7 @@ func newMatchesRows(affectedPkgs []v6.AffectedPackageHandle, affectedCPEs []v6.A } aff := AffectedPackageInfo{ + // tracking model information is not possible with CPE handles CPE: c, Detail: detail, } diff --git a/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go b/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go index fc9ea5cbb3ca..122b9541b128 100644 --- a/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go +++ b/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go @@ -25,6 +25,9 @@ type Vulnerability struct { } type VulnerabilityInfo struct { + // TODO: remove this when namespace is no longer used + Model v6.VulnerabilityHandle `json:"-"` // tracking package handle info is necessary for namespace lookup + v6.VulnerabilityBlob `json:",inline"` // Provider is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider @@ -86,6 +89,7 @@ func newVulnerabilityInfo(vuln v6.VulnerabilityHandle) VulnerabilityInfo { blob = *vuln.BlobValue } return VulnerabilityInfo{ + Model: vuln, VulnerabilityBlob: blob, Provider: vuln.Provider.ID, Status: string(vuln.Status), diff --git a/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities_test.go b/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities_test.go index 4eca1219ddcc..6db7c6ce81eb 100644 --- a/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities_test.go +++ b/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -47,7 +48,7 @@ func TestNewVulnerabilityRows(t *testing.T) { }, } - if diff := cmp.Diff(expected, rows); diff != "" { + if diff := cmp.Diff(expected, rows, cmpOpts()...); diff != "" { t.Errorf("unexpected rows (-want +got):\n%s", diff) } } @@ -96,7 +97,7 @@ func TestVulnerabilities(t *testing.T) { }, } - if diff := cmp.Diff(expected, results); diff != "" { + if diff := cmp.Diff(expected, results, cmpOpts()...); diff != "" { t.Errorf("unexpected results (-want +got):\n%s", diff) } } @@ -118,3 +119,10 @@ func (m *mockVulnReader) GetAffectedPackages(pkg *v6.PackageSpecifier, config *v func ptrTime(t time.Time) *time.Time { return &t } + +func cmpOpts() []cmp.Option { + return []cmp.Option{ + cmpopts.IgnoreFields(AffectedPackageInfo{}, "Model"), + cmpopts.IgnoreFields(VulnerabilityInfo{}, "Model"), + } +} diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go index 5cefd9a9d454..d20ea553fd05 100644 --- a/cmd/grype/cli/commands/root.go +++ b/cmd/grype/cli/commands/root.go @@ -12,19 +12,18 @@ import ( "github.com/anchore/grype/cmd/grype/cli/options" "github.com/anchore/grype/grype" "github.com/anchore/grype/grype/db/legacy/distribution" - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/matcher" - "github.com/anchore/grype/grype/db/v5/matcher/dotnet" - "github.com/anchore/grype/grype/db/v5/matcher/golang" - "github.com/anchore/grype/grype/db/v5/matcher/java" - "github.com/anchore/grype/grype/db/v5/matcher/javascript" - "github.com/anchore/grype/grype/db/v5/matcher/python" - "github.com/anchore/grype/grype/db/v5/matcher/ruby" - "github.com/anchore/grype/grype/db/v5/matcher/stock" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/grype/event/parsers" "github.com/anchore/grype/grype/grypeerr" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher" + "github.com/anchore/grype/grype/matcher/dotnet" + "github.com/anchore/grype/grype/matcher/golang" + "github.com/anchore/grype/grype/matcher/java" + "github.com/anchore/grype/grype/matcher/javascript" + "github.com/anchore/grype/grype/matcher/python" + "github.com/anchore/grype/grype/matcher/ruby" + "github.com/anchore/grype/grype/matcher/stock" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/vex" @@ -119,7 +118,7 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs return err } - var str *v5.ProviderStore + var vp vulnerability.Provider var status *distribution.Status var packages []pkg.Package var s *sbom.SBOM @@ -153,7 +152,11 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs }, func() (err error) { log.Debug("loading DB") - str, status, err = grype.LoadVulnerabilityDB(opts.DB.ToLegacyCuratorConfig(), opts.DB.AutoUpdate) + if opts.Experimental.DBv6 { + vp, status, err = grype.LoadVulnerabilityDBv6(opts.DB.ToClientConfig(), opts.DB.ToCuratorConfig(), opts.DB.AutoUpdate) + return err + } + vp, status, err = grype.LoadVulnerabilityDB(opts.DB.ToLegacyCuratorConfig(), opts.DB.AutoUpdate) return validateDBLoad(err, status) }, func() (err error) { @@ -174,7 +177,7 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs return err } - defer log.CloseAndLogError(str, status.Location) + defer log.CloseAndLogError(vp, status.Location) if err = applyVexRules(opts); err != nil { return fmt.Errorf("applying vex rules: %w", err) @@ -183,11 +186,11 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs applyDistroHint(packages, &pkgContext, opts) vulnMatcher := grype.VulnerabilityMatcher{ - Store: *str, - IgnoreRules: opts.Ignore, - NormalizeByCVE: opts.ByCVE, - FailSeverity: opts.FailOnSeverity(), - Matchers: getMatchers(opts), + VulnerabilityProvider: vp, + IgnoreRules: opts.Ignore, + NormalizeByCVE: opts.ByCVE, + FailSeverity: opts.FailOnSeverity(), + Matchers: getMatchers(opts), VexProcessor: vex.NewProcessor(vex.ProcessorOptions{ Documents: opts.VexDocuments, IgnoreRules: opts.Ignore, @@ -208,7 +211,7 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs IgnoredMatches: ignoredMatches, Packages: packages, Context: pkgContext, - MetadataProvider: str, + MetadataProvider: vp, SBOM: s, AppConfig: opts, DBStatus: status, @@ -280,7 +283,7 @@ func checkForAppUpdate(id clio.Identification, opts *options.Grype) { } } -func getMatchers(opts *options.Grype) []matcher.Matcher { +func getMatchers(opts *options.Grype) []match.Matcher { return matcher.NewDefaultMatchers( matcher.Config{ Java: java.MatcherConfig{ diff --git a/cmd/grype/cli/options/datasources.go b/cmd/grype/cli/options/datasources.go index a91713c15cab..c0f5c00d6d81 100644 --- a/cmd/grype/cli/options/datasources.go +++ b/cmd/grype/cli/options/datasources.go @@ -4,7 +4,7 @@ import ( "time" "github.com/anchore/clio" - "github.com/anchore/grype/grype/db/v5/matcher/java" + "github.com/anchore/grype/grype/matcher/java" ) const ( diff --git a/cmd/grype/cli/options/experimental.go b/cmd/grype/cli/options/experimental.go index 34498ad76f0c..39570e795c81 100644 --- a/cmd/grype/cli/options/experimental.go +++ b/cmd/grype/cli/options/experimental.go @@ -2,6 +2,7 @@ package options import ( "github.com/anchore/clio" + "github.com/anchore/grype/grype/db" ) // Experimental options are opt-in features that are... @@ -16,8 +17,16 @@ type Experimental struct { var _ interface { clio.FieldDescriber + clio.PostLoader } = (*Experimental)(nil) +func (cfg *Experimental) PostLoad() error { + if cfg.DBv6 { + db.SchemaVersion = 6 // FIXME -- v6.SchemaVersion + } + return nil +} + func (cfg *Experimental) DescribeFields(descriptions clio.FieldDescriptionSet) { descriptions.Add(&cfg.DBv6, `use the v6 database schema`) } diff --git a/go.mod b/go.mod index 1baeea401967..a3b27dfac517 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,8 @@ require ( github.com/hashicorp/go-getter v1.7.8 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.7.0 + github.com/iancoleman/strcase v0.3.0 + github.com/invopop/jsonschema v0.13.0 github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d github.com/masahiro331/go-mvn-version v0.0.0-20210429150710-d3157d602a08 @@ -61,14 +63,10 @@ require ( github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 - gopkg.in/yaml.v3 v3.0.1 - gorm.io/gorm v1.25.12 -) - -require ( - github.com/invopop/jsonschema v0.13.0 golang.org/x/time v0.10.0 golang.org/x/tools v0.29.0 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/gorm v1.25.12 ) require ( @@ -172,7 +170,6 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl/v2 v2.23.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect - github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jinzhu/copier v0.4.0 // indirect diff --git a/grype/db/schema.go b/grype/db/schema.go index e0fc06999293..89dcde1476fb 100644 --- a/grype/db/schema.go +++ b/grype/db/schema.go @@ -2,4 +2,4 @@ package db import v5 "github.com/anchore/grype/grype/db/v5" -const SchemaVersion = v5.SchemaVersion +var SchemaVersion = v5.SchemaVersion diff --git a/grype/db/v5/match_exclusion_provider.go b/grype/db/v5/match_exclusion_provider.go index d469351c9f11..a82dc4d3c9f0 100644 --- a/grype/db/v5/match_exclusion_provider.go +++ b/grype/db/v5/match_exclusion_provider.go @@ -44,7 +44,7 @@ func buildIgnoreRulesFromMatchExclusion(e VulnerabilityMatchExclusion) []match.I return ignoreRules } -func (pr *MatchExclusionProvider) GetRules(vulnerabilityID string) ([]match.IgnoreRule, error) { +func (pr *MatchExclusionProvider) IgnoreRules(vulnerabilityID string) ([]match.IgnoreRule, error) { matchExclusions, err := pr.reader.GetVulnerabilityMatchExclusion(vulnerabilityID) if err != nil { return nil, fmt.Errorf("match exclusion provider failed to fetch records for vulnerability id='%s': %w", vulnerabilityID, err) diff --git a/grype/db/v5/matcher/dpkg/matcher_mocks_test.go b/grype/db/v5/matcher/dpkg/matcher_mocks_test.go deleted file mode 100644 index ebfbb5661ae6..000000000000 --- a/grype/db/v5/matcher/dpkg/matcher_mocks_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package dpkg - -import ( - "strings" - - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/grype/version" - "github.com/anchore/grype/grype/vulnerability" -) - -type mockProvider struct { - data map[string]map[string][]vulnerability.Vulnerability -} - -func newMockProvider() *mockProvider { - pr := mockProvider{ - data: make(map[string]map[string][]vulnerability.Vulnerability), - } - pr.stub() - return &pr -} - -func (pr *mockProvider) stub() { - pr.data["debian:8"] = map[string][]vulnerability.Vulnerability{ - // direct... - "neutron": { - { - Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat), - Reference: vulnerability.Reference{ID: "CVE-2014-fake-1"}, - }, - }, - // indirect... - "neutron-devel": { - // expected... - { - Constraint: version.MustGetConstraint("< 2014.1.4-5", version.DebFormat), - Reference: vulnerability.Reference{ID: "CVE-2014-fake-2"}, - }, - { - Constraint: version.MustGetConstraint("< 2015.0.0-1", version.DebFormat), - Reference: vulnerability.Reference{ID: "CVE-2013-fake-3"}, - }, - // unexpected... - { - Constraint: version.MustGetConstraint("< 2014.0.4-1", version.DebFormat), - Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD"}, - }, - }, - } -} - -func (pr *mockProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) { - return pr.data[strings.ToLower(d.Type.String())+":"+d.FullVersion()][p.Name], nil -} diff --git a/grype/db/v5/matcher/java/matcher_mocks_test.go b/grype/db/v5/matcher/java/matcher_mocks_test.go deleted file mode 100644 index 2f5521528c41..000000000000 --- a/grype/db/v5/matcher/java/matcher_mocks_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package java - -import ( - "context" - - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/grype/version" - "github.com/anchore/grype/grype/vulnerability" - "github.com/anchore/syft/syft/cpe" - syftPkg "github.com/anchore/syft/syft/pkg" -) - -type mockProvider struct { - data map[syftPkg.Language]map[string][]vulnerability.Vulnerability -} - -func (mp *mockProvider) Get(id, namespace string) ([]vulnerability.Vulnerability, error) { - //TODO implement me - panic("implement me") -} - -func (mp *mockProvider) populateData() { - mp.data[syftPkg.Java] = map[string][]vulnerability.Vulnerability{ - "org.springframework.spring-webmvc": { - { - Constraint: version.MustGetConstraint(">=5.0.0,<5.1.7", version.UnknownFormat), - Reference: vulnerability.Reference{ID: "CVE-2014-fake-2"}, - }, - { - Constraint: version.MustGetConstraint(">=5.0.1,<5.1.7", version.UnknownFormat), - Reference: vulnerability.Reference{ID: "CVE-2013-fake-3"}, - }, - // unexpected... - { - Constraint: version.MustGetConstraint(">=5.0.0,<5.0.7", version.UnknownFormat), - Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD"}, - }, - }, - } -} - -func newMockProvider() *mockProvider { - mp := mockProvider{ - data: make(map[syftPkg.Language]map[string][]vulnerability.Vulnerability), - } - - mp.populateData() - - return &mp -} - -type mockMavenSearcher struct { - pkg pkg.Package -} - -func (m mockMavenSearcher) GetMavenPackageBySha(context.Context, string) (*pkg.Package, error) { - return &m.pkg, nil -} - -func newMockSearcher(pkg pkg.Package) MavenSearcher { - return mockMavenSearcher{ - pkg, - } -} - -func (mp *mockProvider) GetByCPE(p cpe.CPE) ([]vulnerability.Vulnerability, error) { - return []vulnerability.Vulnerability{}, nil -} - -func (mp *mockProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) { - return []vulnerability.Vulnerability{}, nil -} - -func (mp *mockProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]vulnerability.Vulnerability, error) { - return mp.data[l][p.Name], nil -} diff --git a/grype/db/v5/matcher/matcher.go b/grype/db/v5/matcher/matcher.go deleted file mode 100644 index c7377297ddc6..000000000000 --- a/grype/db/v5/matcher/matcher.go +++ /dev/null @@ -1,15 +0,0 @@ -package matcher - -import ( - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/pkg" - syftPkg "github.com/anchore/syft/syft/pkg" -) - -type Matcher interface { - PackageTypes() []syftPkg.Type - Type() match.MatcherType - Match(v5.VulnerabilityProvider, *distro.Distro, pkg.Package) ([]match.Match, error) -} diff --git a/grype/db/v5/matcher/msrc/matcher_test.go b/grype/db/v5/matcher/msrc/matcher_test.go deleted file mode 100644 index bd604c4fa6e5..000000000000 --- a/grype/db/v5/matcher/msrc/matcher_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package msrc - -import ( - "fmt" - "testing" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/grype/pkg" - syftPkg "github.com/anchore/syft/syft/pkg" -) - -type mockStore struct { - backend map[string]map[string][]v5.Vulnerability -} - -func (s *mockStore) GetVulnerability(namespace, id string) ([]v5.Vulnerability, error) { - //TODO implement me - panic("implement me") -} - -func (s *mockStore) SearchForVulnerabilities(namespace, name string) ([]v5.Vulnerability, error) { - namespaceMap := s.backend[namespace] - if namespaceMap == nil { - return nil, nil - } - return namespaceMap[name], nil -} - -func (s *mockStore) GetAllVulnerabilities() (*[]v5.Vulnerability, error) { - return nil, nil -} - -func (s *mockStore) GetVulnerabilityNamespaces() ([]string, error) { - keys := make([]string, 0, len(s.backend)) - for k := range s.backend { - keys = append(keys, k) - } - - return keys, nil -} - -func TestMatches(t *testing.T) { - d, err := distro.New(distro.Windows, "10816", "Windows Server 2016") - assert.NoError(t, err) - - store := mockStore{ - backend: map[string]map[string][]v5.Vulnerability{ - - // TODO: it would be ideal to test against something that constructs the namespace based on grype-db - // and not break the adaption of grype-db - fmt.Sprintf("msrc:distro:windows:%s", d.RawVersion): { - d.RawVersion: []v5.Vulnerability{ - { - ID: "CVE-2016-3333", - VersionConstraint: "3200970 || 878787 || base", - VersionFormat: "kb", - }, - { - // Does not match, version constraints do not apply - ID: "CVE-2020-made-up", - VersionConstraint: "778786 || 878787 || base", - VersionFormat: "kb", - }, - }, - // Does not match the product ID - "something-else": []v5.Vulnerability{ - { - ID: "CVE-2020-also-made-up", - VersionConstraint: "3200970 || 878787 || base", - VersionFormat: "kb", - }, - }, - }, - }, - } - - provider, err := v5.NewVulnerabilityProvider(&store) - require.NoError(t, err) - - tests := []struct { - name string - pkg pkg.Package - expectedVulnIDs []string - }{ - { - name: "direct KB match", - pkg: pkg.Package{ - ID: pkg.ID(uuid.NewString()), - Name: d.RawVersion, - Version: "3200970", - Type: syftPkg.KbPkg, - }, - expectedVulnIDs: []string{ - "CVE-2016-3333", - }, - }, - { - name: "multiple direct KB match", - pkg: pkg.Package{ - ID: pkg.ID(uuid.NewString()), - Name: d.RawVersion, - Version: "878787", - Type: syftPkg.KbPkg, - }, - expectedVulnIDs: []string{ - "CVE-2016-3333", - "CVE-2020-made-up", - }, - }, - { - name: "no KBs found", - pkg: pkg.Package{ - ID: pkg.ID(uuid.NewString()), - Name: d.RawVersion, - // this is the assumed version if no KBs are found - Version: "base", - Type: syftPkg.KbPkg, - }, - expectedVulnIDs: []string{ - "CVE-2016-3333", - "CVE-2020-made-up", - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - m := Matcher{} - matches, err := m.Match(provider, d, test.pkg) - assert.NoError(t, err) - var actualVulnIDs []string - for _, a := range matches { - actualVulnIDs = append(actualVulnIDs, a.Vulnerability.ID) - } - assert.ElementsMatch(t, test.expectedVulnIDs, actualVulnIDs) - }) - } - -} diff --git a/grype/db/v5/matcher/portage/matcher.go b/grype/db/v5/matcher/portage/matcher.go deleted file mode 100644 index 1e06f24c70cb..000000000000 --- a/grype/db/v5/matcher/portage/matcher.go +++ /dev/null @@ -1,32 +0,0 @@ -package portage - -import ( - "fmt" - - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/search" - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/pkg" - syftPkg "github.com/anchore/syft/syft/pkg" -) - -type Matcher struct { -} - -func (m *Matcher) PackageTypes() []syftPkg.Type { - return []syftPkg.Type{syftPkg.PortagePkg} -} - -func (m *Matcher) Type() match.MatcherType { - return match.PortageMatcher -} - -func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { - matches, err := search.ByPackageDistro(store, d, p, m.Type()) - if err != nil { - return nil, fmt.Errorf("failed to find vulnerabilities: %w", err) - } - - return matches, nil -} diff --git a/grype/db/v5/matcher/portage/matcher_mocks_test.go b/grype/db/v5/matcher/portage/matcher_mocks_test.go deleted file mode 100644 index b71e5ecd5a12..000000000000 --- a/grype/db/v5/matcher/portage/matcher_mocks_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package portage - -import ( - "strings" - - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/grype/version" - "github.com/anchore/grype/grype/vulnerability" - "github.com/anchore/syft/syft/cpe" - syftPkg "github.com/anchore/syft/syft/pkg" -) - -type mockProvider struct { - data map[string]map[string][]vulnerability.Vulnerability -} - -func (pr *mockProvider) Get(id, namespace string) ([]vulnerability.Vulnerability, error) { - //TODO implement me - panic("implement me") -} - -func newMockProvider() *mockProvider { - pr := mockProvider{ - data: make(map[string]map[string][]vulnerability.Vulnerability), - } - pr.stub() - return &pr -} - -func (pr *mockProvider) stub() { - pr.data["gentoo:"] = map[string][]vulnerability.Vulnerability{ - // direct... - "app-misc/neutron": { - { - Constraint: version.MustGetConstraint("< 2014.1.3", version.PortageFormat), - Reference: vulnerability.Reference{ID: "CVE-2014-fake-1"}, - }, - { - Constraint: version.MustGetConstraint("< 2014.1.4", version.PortageFormat), - Reference: vulnerability.Reference{ID: "CVE-2014-fake-2"}, - }, - }, - } -} - -func (pr *mockProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) { - return pr.data[strings.ToLower(d.Type.String())+":"+d.FullVersion()][p.Name], nil -} - -func (pr *mockProvider) GetByCPE(request cpe.CPE) (v []vulnerability.Vulnerability, err error) { - return v, err -} - -func (pr *mockProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) (v []vulnerability.Vulnerability, err error) { - return v, err -} diff --git a/grype/db/v5/matcher/rpm/matcher_mocks_test.go b/grype/db/v5/matcher/rpm/matcher_mocks_test.go deleted file mode 100644 index 3f6bd3937aa3..000000000000 --- a/grype/db/v5/matcher/rpm/matcher_mocks_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package rpm - -import ( - "strings" - - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/grype/pkg/qualifier" - "github.com/anchore/grype/grype/pkg/qualifier/rpmmodularity" - "github.com/anchore/grype/grype/version" - "github.com/anchore/grype/grype/vulnerability" - "github.com/anchore/syft/syft/cpe" - syftPkg "github.com/anchore/syft/syft/pkg" -) - -type mockProvider struct { - data map[string]map[string][]vulnerability.Vulnerability -} - -func (pr *mockProvider) Get(id, namespace string) ([]vulnerability.Vulnerability, error) { - //TODO implement me - panic("implement me") -} - -func newMockProvider(packageName, indirectName string, withEpoch bool, withPackageQualifiers bool) *mockProvider { - pr := mockProvider{ - data: make(map[string]map[string][]vulnerability.Vulnerability), - } - if withEpoch { - pr.stubWithEpoch(packageName, indirectName) - } else if withPackageQualifiers { - pr.stubWithPackageQualifiers(packageName) - } else { - pr.stub(packageName, indirectName) - } - - return &pr -} - -func (pr *mockProvider) stub(packageName, indirectName string) { - pr.data["rhel:8"] = map[string][]vulnerability.Vulnerability{ - // direct... - packageName: { - { - Constraint: version.MustGetConstraint("<= 7.1.3-6", version.RpmFormat), - Reference: vulnerability.Reference{ID: "CVE-2014-fake-1"}, - }, - }, - // indirect... - indirectName: { - // expected... - { - Constraint: version.MustGetConstraint("< 7.1.4-5", version.RpmFormat), - Reference: vulnerability.Reference{ID: "CVE-2014-fake-2"}, - }, - { - Constraint: version.MustGetConstraint("< 8.0.2-0", version.RpmFormat), - Reference: vulnerability.Reference{ID: "CVE-2013-fake-3"}, - }, - // unexpected... - { - Constraint: version.MustGetConstraint("< 7.0.4-1", version.RpmFormat), - Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD"}, - }, - }, - } -} - -func (pr *mockProvider) stubWithEpoch(packageName, indirectName string) { - pr.data["rhel:8"] = map[string][]vulnerability.Vulnerability{ - // direct... - packageName: { - { - Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat), - Reference: vulnerability.Reference{ID: "CVE-2021-1"}, - }, - { - Constraint: version.MustGetConstraint("<= 0:2.28-419.el8.", version.RpmFormat), - Reference: vulnerability.Reference{ID: "CVE-2021-2"}, - }, - }, - // indirect... - indirectName: { - { - Constraint: version.MustGetConstraint("< 5.28.3-420.el8", version.RpmFormat), - Reference: vulnerability.Reference{ID: "CVE-2021-3"}, - }, - // unexpected... - { - Constraint: version.MustGetConstraint("< 4:5.26.3-419.el8", version.RpmFormat), - Reference: vulnerability.Reference{ID: "CVE-2021-4"}, - }, - }, - } -} - -func (pr *mockProvider) stubWithPackageQualifiers(packageName string) { - pr.data["rhel:8"] = map[string][]vulnerability.Vulnerability{ - // direct... - packageName: { - { - Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat), - Reference: vulnerability.Reference{ID: "CVE-2021-1"}, - PackageQualifiers: []qualifier.Qualifier{ - rpmmodularity.New("containertools:3"), - }, - }, - { - Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat), - Reference: vulnerability.Reference{ID: "CVE-2021-2"}, - PackageQualifiers: []qualifier.Qualifier{ - rpmmodularity.New(""), - }, - }, - { - Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat), - Reference: vulnerability.Reference{ID: "CVE-2021-3"}, - }, - { - Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat), - Reference: vulnerability.Reference{ID: "CVE-2021-4"}, - PackageQualifiers: []qualifier.Qualifier{ - rpmmodularity.New("containertools:4"), - }, - }, - }, - } -} - -func (pr *mockProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) { - var ty = strings.ToLower(d.Type.String()) - if d.Type == distro.CentOS || d.Type == distro.RedHat || d.Type == distro.RockyLinux || d.Type == distro.AlmaLinux { - ty = "rhel" - } - - return pr.data[ty+":"+d.FullVersion()][p.Name], nil -} - -func (pr *mockProvider) GetByCPE(request cpe.CPE) (v []vulnerability.Vulnerability, err error) { - return v, err -} - -func (pr *mockProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) (v []vulnerability.Vulnerability, err error) { - return v, err -} diff --git a/grype/db/v5/matcher/stock/matcher.go b/grype/db/v5/matcher/stock/matcher.go deleted file mode 100644 index 4277a632f884..000000000000 --- a/grype/db/v5/matcher/stock/matcher.go +++ /dev/null @@ -1,40 +0,0 @@ -package stock - -import ( - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/search" - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/pkg" - syftPkg "github.com/anchore/syft/syft/pkg" -) - -type Matcher struct { - cfg MatcherConfig -} - -type MatcherConfig struct { - UseCPEs bool -} - -func NewStockMatcher(cfg MatcherConfig) *Matcher { - return &Matcher{ - cfg: cfg, - } -} - -func (m *Matcher) PackageTypes() []syftPkg.Type { - return nil -} - -func (m *Matcher) Type() match.MatcherType { - return match.StockMatcher -} - -func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { - criteria := search.CommonCriteria - if m.cfg.UseCPEs { - criteria = append(criteria, search.ByCPE) - } - return search.ByCriteria(store, d, p, m.Type(), criteria...) -} diff --git a/grype/db/v5/matcher/stock/matcher_test.go b/grype/db/v5/matcher/stock/matcher_test.go deleted file mode 100644 index 9680a3b9bed1..000000000000 --- a/grype/db/v5/matcher/stock/matcher_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package stock - -import ( - "testing" - - "github.com/google/uuid" - "github.com/scylladb/go-set/strset" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/grype/version" - "github.com/anchore/grype/grype/vulnerability" - "github.com/anchore/syft/syft/cpe" - syftPkg "github.com/anchore/syft/syft/pkg" -) - -func TestMatcher_JVMPackage(t *testing.T) { - p := pkg.Package{ - ID: pkg.ID(uuid.NewString()), - Name: "java_se", - Version: "1.8.0_400", - Type: syftPkg.BinaryPkg, - CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:a:oracle:java_se:1.8.0:update400:*:*:*:*:*:*", cpe.DeclaredSource), - }, - } - matcher := Matcher{ - cfg: MatcherConfig{ - UseCPEs: true, - }, - } - store := newMockProvider() - actual, err := matcher.Match(store, nil, p) - require.NoError(t, err) - - foundCVEs := strset.New() - for _, v := range actual { - foundCVEs.Add(v.Vulnerability.ID) - - require.NotEmpty(t, v.Details) - for _, d := range v.Details { - assert.Equal(t, match.CPEMatch, d.Type, "indirect match not indicated") - assert.Equal(t, matcher.Type(), d.Matcher, "failed to capture matcher type") - } - assert.Equal(t, p.Name, v.Package.Name, "failed to capture original package name") - } - - expected := strset.New( - "CVE-2024-20919-real", - "CVE-2024-20919-bonkers-format", - "CVE-2024-20919-post-jep223", - ) - - for _, id := range expected.List() { - if !foundCVEs.Has(id) { - t.Errorf("missing CVE: %s", id) - } - } - - extra := strset.Difference(foundCVEs, expected) - - for _, id := range extra.List() { - t.Errorf("unexpected CVE: %s", id) - } - - if t.Failed() { - t.Logf("discovered CVES: %d", foundCVEs.Size()) - for _, id := range foundCVEs.List() { - t.Logf(" - %s", id) - } - } -} - -func newMockProvider() *mockProvider { - mp := mockProvider{ - data: make(map[syftPkg.Language]map[string][]vulnerability.Vulnerability), - } - - mp.populateData() - - return &mp -} - -type mockProvider struct { - data map[syftPkg.Language]map[string][]vulnerability.Vulnerability -} - -func (mp *mockProvider) Get(_, _ string) ([]vulnerability.Vulnerability, error) { - // TODO implement me - panic("not implemented") -} - -func (mp *mockProvider) populateData() { - - // derived from vuln data found on CVE-2024-20919 - hit := "< 1.8.0_401 || >= 1.9-ea, < 8.0.401 || >= 9-ea, < 11.0.22 || >= 12-ea, < 17.0.10 || >= 18-ea, < 21.0.2" - - mp.data["nvd:cpe"] = map[string][]vulnerability.Vulnerability{ - "java_se": { - { - // positive cases - Constraint: version.MustGetConstraint(hit, version.JVMFormat), - Reference: vulnerability.Reference{ID: "CVE-2024-20919-real"}, - }, - { - // positive cases - Constraint: version.MustGetConstraint("< 22.22.22", version.UnknownFormat), - Reference: vulnerability.Reference{ID: "CVE-2024-20919-bonkers-format"}, - }, - { - // negative case - Constraint: version.MustGetConstraint("< 1.8.0_399 || >= 1.9-ea, < 8.0.399 || >= 9-ea", version.JVMFormat), - Reference: vulnerability.Reference{ID: "CVE-FAKE-bad-update"}, - }, - { - // positive case - Constraint: version.MustGetConstraint("< 8.0.401", version.JVMFormat), - Reference: vulnerability.Reference{ID: "CVE-2024-20919-post-jep223"}, - }, - { - // negative case - Constraint: version.MustGetConstraint("< 8.0.399", version.JVMFormat), - Reference: vulnerability.Reference{ID: "CVE-FAKE-bad-range-post-jep223"}, - }, - { - // negative case - Constraint: version.MustGetConstraint("< 7.0.0", version.JVMFormat), - Reference: vulnerability.Reference{ID: "CVE-FAKE-bad-range-post-jep223"}, - }, - }, - } -} - -func (mp *mockProvider) GetByCPE(p cpe.CPE) ([]vulnerability.Vulnerability, error) { - return mp.data["nvd:cpe"][p.Attributes.Product], nil -} - -func (mp *mockProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) { - return []vulnerability.Vulnerability{}, nil -} - -func (mp *mockProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]vulnerability.Vulnerability, error) { - return mp.data[l][p.Name], nil -} diff --git a/grype/db/v5/namespace/cpe/namespace.go b/grype/db/v5/namespace/cpe/namespace.go index baa4ff892be6..1045be0ae1a5 100644 --- a/grype/db/v5/namespace/cpe/namespace.go +++ b/grype/db/v5/namespace/cpe/namespace.go @@ -29,13 +29,16 @@ func FromString(namespaceStr string) (*Namespace, error) { } components := strings.Split(namespaceStr, ":") + return FromComponents(components) +} +func FromComponents(components []string) (*Namespace, error) { if len(components) != 2 { - return nil, fmt.Errorf("unable to create CPE namespace from %s: incorrect number of components", namespaceStr) + return nil, fmt.Errorf("unable to create CPE namespace from %s: incorrect number of components", strings.Join(components, ":")) } if components[1] != ID { - return nil, fmt.Errorf("unable to create CPE namespace from %s: type %s is incorrect", namespaceStr, components[1]) + return nil, fmt.Errorf("unable to create CPE namespace from %s: type %s is incorrect", strings.Join(components, ":"), components[1]) } return NewNamespace(components[0]), nil diff --git a/grype/db/v5/namespace/distro/namespace.go b/grype/db/v5/namespace/distro/namespace.go index 9bfefa3e939a..083174afed79 100644 --- a/grype/db/v5/namespace/distro/namespace.go +++ b/grype/db/v5/namespace/distro/namespace.go @@ -34,13 +34,16 @@ func FromString(namespaceStr string) (*Namespace, error) { } components := strings.Split(namespaceStr, ":") + return FromComponents(components) +} +func FromComponents(components []string) (*Namespace, error) { if len(components) != 4 { - return nil, fmt.Errorf("unable to create distro namespace from %s: incorrect number of components", namespaceStr) + return nil, fmt.Errorf("unable to create distro namespace from %s: incorrect number of components", strings.Join(components, ":")) } if components[1] != ID { - return nil, fmt.Errorf("unable to create distro namespace from %s: type %s is incorrect", namespaceStr, components[1]) + return nil, fmt.Errorf("unable to create distro namespace from %s: type %s is incorrect", strings.Join(components, ":"), components[1]) } return NewNamespace(components[0], distro.Type(components[2]), components[3]), nil diff --git a/grype/db/v5/namespace/from_string.go b/grype/db/v5/namespace/from_string.go index bf68a38643dc..b71dd11e31ec 100644 --- a/grype/db/v5/namespace/from_string.go +++ b/grype/db/v5/namespace/from_string.go @@ -17,17 +17,17 @@ func FromString(namespaceStr string) (Namespace, error) { components := strings.Split(namespaceStr, ":") - if len(components) < 1 { + if len(components) < 2 { return nil, fmt.Errorf("unable to create namespace from %s: incorrect number of components", namespaceStr) } switch components[1] { case cpe.ID: - return cpe.FromString(namespaceStr) + return cpe.FromComponents(components) case distro.ID: - return distro.FromString(namespaceStr) + return distro.FromComponents(components) case language.ID: - return language.FromString(namespaceStr) + return language.FromComponents(components) default: return nil, fmt.Errorf("unable to create namespace from %s: unknown type %s", namespaceStr, components[1]) } diff --git a/grype/db/v5/namespace/index.go b/grype/db/v5/namespace/index.go index 5e321e1072e5..5c207cd4f0df 100644 --- a/grype/db/v5/namespace/index.go +++ b/grype/db/v5/namespace/index.go @@ -86,7 +86,7 @@ func (i *Index) NamespacesForDistro(d *grypeDistro.Distro) []*distro.Namespace { return nil } - dTy := distroTypeString(d.Type) + dTy := DistroTypeString(d.Type) if d.IsRolling() { distroKey := fmt.Sprintf("%s:%s", dTy, "rolling") @@ -151,7 +151,7 @@ func (i *Index) getAlpineMajorMinorNamespace(d *grypeDistro.Distro, versionSegme } func (i *Index) findClosestNamespace(d *grypeDistro.Distro, versionSegments []int) ([]*distro.Namespace, bool) { - ty := distroTypeString(d.Type) + ty := DistroTypeString(d.Type) // look for exact match distroKey := fmt.Sprintf("%s:%s", ty, d.FullVersion()) @@ -239,7 +239,7 @@ func (i *Index) CPENamespaces() []*cpe.Namespace { return i.cpe } -func distroTypeString(ty grypeDistro.Type) string { +func DistroTypeString(ty grypeDistro.Type) string { switch ty { case grypeDistro.CentOS, grypeDistro.RedHat, grypeDistro.Fedora, grypeDistro.RockyLinux, grypeDistro.AlmaLinux, grypeDistro.Gentoo: return strings.ToLower(string(grypeDistro.RedHat)) diff --git a/grype/db/v5/namespace/language/namespace.go b/grype/db/v5/namespace/language/namespace.go index 2a1814eb131c..f438bbcb5dcc 100644 --- a/grype/db/v5/namespace/language/namespace.go +++ b/grype/db/v5/namespace/language/namespace.go @@ -35,13 +35,16 @@ func FromString(namespaceStr string) (*Namespace, error) { } components := strings.Split(namespaceStr, ":") + return FromComponents(components) +} +func FromComponents(components []string) (*Namespace, error) { if len(components) != 3 && len(components) != 4 { - return nil, fmt.Errorf("unable to create language namespace from %s: incorrect number of components", namespaceStr) + return nil, fmt.Errorf("unable to create language namespace from %s: incorrect number of components", strings.Join(components, ":")) } if components[1] != ID { - return nil, fmt.Errorf("unable to create language namespace from %s: type %s is incorrect", namespaceStr, components[1]) + return nil, fmt.Errorf("unable to create language namespace from %s: type %s is incorrect", strings.Join(components, ":"), components[1]) } packageType := "" diff --git a/grype/db/v5/provider.go b/grype/db/v5/provider.go deleted file mode 100644 index 8f17c8458beb..000000000000 --- a/grype/db/v5/provider.go +++ /dev/null @@ -1,33 +0,0 @@ -package v5 - -import ( - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/grype/vulnerability" - "github.com/anchore/syft/syft/cpe" - syftPkg "github.com/anchore/syft/syft/pkg" -) - -type VulnerabilityProvider interface { - Get(id, namespace string) ([]vulnerability.Vulnerability, error) - ProviderByDistro - ProviderByLanguage - ProviderByCPE -} - -type ProviderByDistro interface { - GetByDistro(*distro.Distro, pkg.Package) ([]vulnerability.Vulnerability, error) -} - -type ProviderByLanguage interface { - GetByLanguage(syftPkg.Language, pkg.Package) ([]vulnerability.Vulnerability, error) -} - -type ProviderByCPE interface { - GetByCPE(cpe.CPE) ([]vulnerability.Vulnerability, error) -} - -type VulnerabilityMetadataProvider interface { - vulnerability.MetadataProvider - GetMetadata(id, namespace string) (*vulnerability.Metadata, error) -} diff --git a/grype/db/v5/provider_store.go b/grype/db/v5/provider_store.go index dd66da85aae5..734576769140 100644 --- a/grype/db/v5/provider_store.go +++ b/grype/db/v5/provider_store.go @@ -1,14 +1,11 @@ package v5 import ( - "io" - "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/vulnerability" ) type ProviderStore struct { - VulnerabilityProvider - VulnerabilityMetadataProvider + vulnerability.Provider match.ExclusionProvider - io.Closer } diff --git a/grype/db/v5/search/criteria.go b/grype/db/v5/search/criteria.go deleted file mode 100644 index fba23c8787b0..000000000000 --- a/grype/db/v5/search/criteria.go +++ /dev/null @@ -1,55 +0,0 @@ -package search - -import ( - "errors" - - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/internal/log" -) - -var ( - ByCPE Criteria = "by-cpe" - ByLanguage Criteria = "by-language" - ByDistro Criteria = "by-distro" - CommonCriteria = []Criteria{ - ByLanguage, - } -) - -type Criteria string - -func ByCriteria(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package, upstreamMatcher match.MatcherType, criteria ...Criteria) ([]match.Match, error) { - matches := make([]match.Match, 0) - for _, c := range criteria { - switch c { - case ByCPE: - m, err := ByPackageCPE(store, d, p, upstreamMatcher) - if errors.Is(err, ErrEmptyCPEMatch) { - log.Warnf("attempted CPE search on %s, which has no CPEs. Consider re-running with --add-cpes-if-none", p.Name) - continue - } else if err != nil { - log.Warnf("could not match by package CPE (package=%+v): %v", p, err) - continue - } - matches = append(matches, m...) - case ByLanguage: - m, err := ByPackageLanguage(store, d, p, upstreamMatcher) - if err != nil { - log.Warnf("could not match by package language (package=%+v): %v", p, err) - continue - } - matches = append(matches, m...) - case ByDistro: - m, err := ByPackageDistro(store, d, p, upstreamMatcher) - if err != nil { - log.Warnf("could not match by package distro (package=%+v): %v", p, err) - continue - } - matches = append(matches, m...) - } - } - return matches, nil -} diff --git a/grype/db/v5/search/language.go b/grype/db/v5/search/language.go deleted file mode 100644 index 2ab453abc0b4..000000000000 --- a/grype/db/v5/search/language.go +++ /dev/null @@ -1,76 +0,0 @@ -package search - -import ( - "errors" - "fmt" - - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/grype/version" - "github.com/anchore/grype/internal/log" -) - -func ByPackageLanguage(store v5.ProviderByLanguage, d *distro.Distro, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) { - if isUnknownVersion(p.Version) { - log.WithFields("package", p.Name).Trace("skipping package with unknown version") - - return nil, nil - } - - verObj, err := version.NewVersionFromPkg(p) - if err != nil { - if errors.Is(err, version.ErrUnsupportedVersion) { - log.WithFields("error", err).Tracef("skipping package '%s@%s'", p.Name, p.Version) - return nil, nil - } - return nil, fmt.Errorf("matcher failed to parse version pkg=%q ver=%q: %w", p.Name, p.Version, err) - } - - allPkgVulns, err := store.GetByLanguage(p.Language, p) - if err != nil { - return nil, fmt.Errorf("matcher failed to fetch language=%q pkg=%q: %w", p.Language, p.Name, err) - } - - applicableVulns, err := onlyQualifiedPackages(d, p, allPkgVulns) - if err != nil { - return nil, fmt.Errorf("unable to filter language-related vulnerabilities: %w", err) - } - - // TODO: Port this over to a qualifier and remove - applicableVulns, err = onlyVulnerableVersions(verObj, applicableVulns) - if err != nil { - return nil, fmt.Errorf("unable to filter language-related vulnerabilities: %w", err) - } - - var matches []match.Match - for _, vuln := range applicableVulns { - matches = append(matches, match.Match{ - - Vulnerability: vuln, - Package: p, - Details: []match.Detail{ - { - Type: match.ExactDirectMatch, - Confidence: 1.0, // TODO: this is hard coded for now - Matcher: upstreamMatcher, - SearchedBy: map[string]interface{}{ - "language": string(p.Language), - "namespace": vuln.Namespace, - "package": map[string]string{ - "name": p.Name, - "version": p.Version, - }, - }, - Found: map[string]interface{}{ - "vulnerabilityID": vuln.ID, - "versionConstraint": vuln.Constraint.String(), - }, - }, - }, - }) - } - - return matches, err -} diff --git a/grype/db/v5/search/only_qualified_packages.go b/grype/db/v5/search/only_qualified_packages.go deleted file mode 100644 index 9fbd2c3f4f37..000000000000 --- a/grype/db/v5/search/only_qualified_packages.go +++ /dev/null @@ -1,38 +0,0 @@ -package search - -import ( - "fmt" - - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/grype/vulnerability" -) - -func onlyQualifiedPackages(d *distro.Distro, p pkg.Package, allVulns []vulnerability.Vulnerability) ([]vulnerability.Vulnerability, error) { - var vulns []vulnerability.Vulnerability - - for _, vuln := range allVulns { - isVulnerable := true - - for _, q := range vuln.PackageQualifiers { - v, err := q.Satisfied(d, p) - - if err != nil { - return nil, fmt.Errorf("failed to check package qualifier=%q for distro=%q package=%q: %w", q, d, p, err) - } - - isVulnerable = v - if !isVulnerable { - break - } - } - - if !isVulnerable { - continue - } - - vulns = append(vulns, vuln) - } - - return vulns, nil -} diff --git a/grype/db/v5/search/only_vulnerable_versions.go b/grype/db/v5/search/only_vulnerable_versions.go deleted file mode 100644 index 482429d4f2a1..000000000000 --- a/grype/db/v5/search/only_vulnerable_versions.go +++ /dev/null @@ -1,34 +0,0 @@ -package search - -import ( - "errors" - "fmt" - - "github.com/anchore/grype/grype/version" - "github.com/anchore/grype/grype/vulnerability" - "github.com/anchore/grype/internal/log" -) - -func onlyVulnerableVersions(verObj *version.Version, allVulns []vulnerability.Vulnerability) ([]vulnerability.Vulnerability, error) { - var vulns []vulnerability.Vulnerability - - for _, vuln := range allVulns { - isPackageVulnerable, err := vuln.Constraint.Satisfied(verObj) - if err != nil { - var e *version.NonFatalConstraintError - if errors.As(err, &e) { - log.Warn(e) - } else { - return nil, fmt.Errorf("failed to check constraint=%q version=%q: %w", vuln.Constraint, verObj, err) - } - } - - if !isPackageVulnerable { - continue - } - - vulns = append(vulns, vuln) - } - - return vulns, nil -} diff --git a/grype/db/v5/vulnerability.go b/grype/db/v5/vulnerability.go index 8e4b59bb62be..49ac9e014d39 100644 --- a/grype/db/v5/vulnerability.go +++ b/grype/db/v5/vulnerability.go @@ -9,6 +9,7 @@ import ( "github.com/anchore/grype/grype/pkg/qualifier" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/cpe" ) @@ -147,6 +148,16 @@ func NewVulnerability(vuln Vulnerability) (*vulnerability.Vulnerability, error) }) } + var cpes []cpe.CPE + for _, cp := range vuln.CPEs { + c, err := cpe.New(cp, "") + if err != nil { + log.WithFields("err", err, "cpe", cp).Debug("failed to parse CPE") + continue + } + cpes = append(cpes, c) + } + return &vulnerability.Vulnerability{ PackageName: vuln.PackageName, Constraint: constraint, @@ -154,7 +165,7 @@ func NewVulnerability(vuln Vulnerability) (*vulnerability.Vulnerability, error) ID: vuln.ID, Namespace: vuln.Namespace, }, - CPEs: make([]cpe.CPE, 0), + CPEs: cpes, PackageQualifiers: pkgQualifiers, Fix: vulnerability.Fix{ Versions: vuln.Fix.Versions, diff --git a/grype/db/v5/vulnerability_metadata_provider.go b/grype/db/v5/vulnerability_metadata_provider.go deleted file mode 100644 index c58510de20f1..000000000000 --- a/grype/db/v5/vulnerability_metadata_provider.go +++ /dev/null @@ -1,32 +0,0 @@ -package v5 - -import ( - "fmt" - - "github.com/anchore/grype/grype/vulnerability" -) - -var _ VulnerabilityMetadataProvider = (*vulnerabilityMetadataProvider)(nil) - -type vulnerabilityMetadataProvider struct { - reader VulnerabilityMetadataStoreReader -} - -func NewVulnerabilityMetadataProvider(reader VulnerabilityMetadataStoreReader) VulnerabilityMetadataProvider { - return &vulnerabilityMetadataProvider{ - reader: reader, - } -} - -func (pr *vulnerabilityMetadataProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) { - return pr.GetMetadata(ref.ID, ref.Namespace) -} - -func (pr *vulnerabilityMetadataProvider) GetMetadata(id, namespace string) (*vulnerability.Metadata, error) { - metadata, err := pr.reader.GetVulnerabilityMetadata(id, namespace) - if err != nil { - return nil, fmt.Errorf("metadata provider failed to fetch id='%s' recordsource='%s': %w", id, namespace, err) - } - - return NewMetadata(metadata) -} diff --git a/grype/db/v5/vulnerability_provider.go b/grype/db/v5/vulnerability_provider.go index 851aa3a152db..52ec1ccee073 100644 --- a/grype/db/v5/vulnerability_provider.go +++ b/grype/db/v5/vulnerability_provider.go @@ -9,6 +9,7 @@ import ( "github.com/anchore/grype/grype/db/v5/namespace" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/cpe" @@ -17,10 +18,10 @@ import ( type vulnerabilityProvider struct { namespaceIndex *namespace.Index - reader VulnerabilityStoreReader + reader StoreReader } -func NewVulnerabilityProvider(reader VulnerabilityStoreReader) (VulnerabilityProvider, error) { +func NewVulnerabilityProvider(reader StoreReader) (vulnerability.Provider, error) { namespaces, err := reader.GetVulnerabilityNamespaces() if err != nil { return nil, fmt.Errorf("unable to get namespaces from store: %w", err) @@ -37,6 +38,83 @@ func NewVulnerabilityProvider(reader VulnerabilityStoreReader) (VulnerabilityPro }, nil } +func (pr *vulnerabilityProvider) Close() error { + return pr.reader.Close() +} + +//nolint:gocognit +func (pr *vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) { + if err := search.ValidateCriteria(criteria); err != nil { + return nil, err + } + + searchCrit, unapplied := splitSearchCriteria(criteria) + + var err error + var vulns []vulnerability.Vulnerability + switch { + case searchCrit.id != "": + if len(searchCrit.distros) > 0 { + for _, d := range searchCrit.distros { + namespaces := pr.namespaceIndex.NamespacesForDistro(d) + if len(namespaces) > 0 { + for _, n := range namespaces { + nsVulns, err := pr.Get(searchCrit.id, n.String()) + vulns = append(vulns, nsVulns...) + if err != nil { + return vulns, err + } + } + } + } + return vulns, nil + } + return pr.Get(searchCrit.id, "") + case len(searchCrit.distros) > 0: + for _, d := range searchCrit.distros { + newVulns, newErr := pr.getByDistro(d, searchCrit.packageName) + vulns = append(vulns, newVulns...) + if newErr != nil { + return vulns, newErr + } + } + case searchCrit.language != nil: + vulns, err = pr.getByLanguage(*searchCrit.language, searchCrit.packageName) + case searchCrit.cpe != nil: + vulns, err = pr.getByCPE(*searchCrit.cpe) + default: + return nil, fmt.Errorf("unable to find vulnerabilities without distro, language, or CPE: %+v", criteria) + } + + // apply criteria + for i := 0; i < len(vulns); i++ { + for _, c := range unapplied { + if c == nil { + continue + } + matches, err := c.MatchesVulnerability(vulns[i]) + if err != nil { + return nil, err + } + if !matches { + log.WithFields("vulnerability", vulns[i].ID, "criteria", c).Trace("dropping vulnerability due to criteria mismatch") + vulns = append(vulns[0:i], vulns[i+1:]...) + i-- + break + } + } + } + return vulns, err +} + +func (pr *vulnerabilityProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) { + meta, err := pr.reader.GetVulnerabilityMetadata(ref.ID, ref.Namespace) + if err != nil { + return nil, err + } + return NewMetadata(meta) +} + func (pr *vulnerabilityProvider) Get(id, namespace string) ([]vulnerability.Vulnerability, error) { // note: getting a vulnerability record by id doesn't necessarily return a single record // since records are duplicated by the set of fixes they have. @@ -58,7 +136,7 @@ func (pr *vulnerabilityProvider) Get(id, namespace string) ([]vulnerability.Vuln return results, nil } -func (pr *vulnerabilityProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) { +func (pr *vulnerabilityProvider) getByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) { if d == nil { return nil, nil } @@ -97,7 +175,7 @@ func (pr *vulnerabilityProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([ return vulnerabilities, nil } -func (pr *vulnerabilityProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]vulnerability.Vulnerability, error) { +func (pr *vulnerabilityProvider) getByLanguage(l syftPkg.Language, p pkg.Package) ([]vulnerability.Vulnerability, error) { var vulnerabilities []vulnerability.Vulnerability namespaces := pr.namespaceIndex.NamespacesForLanguage(l) @@ -109,7 +187,8 @@ func (pr *vulnerabilityProvider) GetByLanguage(l syftPkg.Language, p pkg.Package vulnerabilities = make([]vulnerability.Vulnerability, 0) for _, n := range namespaces { - for _, packageName := range n.Resolver().Resolve(p) { + // for _, packageName := range n.Resolver().Resolve(p) { + if packageName := p.Name; packageName != "" { nsStr := n.String() allPkgVulns, err := pr.reader.SearchForVulnerabilities(nsStr, packageName) @@ -132,7 +211,7 @@ func (pr *vulnerabilityProvider) GetByLanguage(l syftPkg.Language, p pkg.Package return vulnerabilities, nil } -func (pr *vulnerabilityProvider) GetByCPE(requestCPE cpe.CPE) ([]vulnerability.Vulnerability, error) { +func (pr *vulnerabilityProvider) getByCPE(requestCPE cpe.CPE) ([]vulnerability.Vulnerability, error) { vulns := make([]vulnerability.Vulnerability, 0) namespaces := pr.namespaceIndex.CPENamespaces() @@ -182,3 +261,46 @@ func (pr *vulnerabilityProvider) GetByCPE(requestCPE cpe.CPE) ([]vulnerability.V return vulns, nil } + +// searchCriteria are the possible values used directly against the store +type searchCriteria struct { + distros []*distro.Distro + language *syftPkg.Language + cpe *cpe.CPE + packageName pkg.Package + id string +} + +// splitSearchCriteria takes all criteria objects and combines the values into a single +// criteria object to use for vulnerability querying, and a set of criteria that did not apply +// which will be applied after initial database results have been returned +func splitSearchCriteria(allCriteria []vulnerability.Criteria) (searchCriteria, []vulnerability.Criteria) { + searchCrit := searchCriteria{} + var unapplied []vulnerability.Criteria + for _, c := range allCriteria { + applied := false + switch c := c.(type) { + case *search.PackageNameCriteria: + searchCrit.packageName.Name = c.PackageName + applied = true + case *search.LanguageCriteria: + searchCrit.language = &c.Language + applied = true + case *search.DistroCriteria: + for _, d := range c.Distros { + searchCrit.distros = append(searchCrit.distros, &d) + } + applied = true + case *search.IDCriteria: + searchCrit.id = c.ID + applied = true + case *search.CPECriteria: + searchCrit.cpe = &c.CPE + applied = true + } + if !applied { + unapplied = append(unapplied, c) + } + } + return searchCrit, unapplied +} diff --git a/grype/db/v5/vulnerability_provider_mocks_test.go b/grype/db/v5/vulnerability_provider_mocks_test.go index 693fbf13b84a..8edeb602df33 100644 --- a/grype/db/v5/vulnerability_provider_mocks_test.go +++ b/grype/db/v5/vulnerability_provider_mocks_test.go @@ -1,18 +1,18 @@ package v5 -type mockStore struct { +type mockStoreReader struct { data map[string]map[string][]Vulnerability } -func newMockStore() *mockStore { - d := mockStore{ +func newMockStoreReader() StoreReader { + d := mockStoreReader{ data: make(map[string]map[string][]Vulnerability), } d.stub() return &d } -func (d *mockStore) stub() { +func (d *mockStoreReader) stub() { d.data["debian:distro:debian:8"] = map[string][]Vulnerability{ "neutron": { { @@ -96,7 +96,7 @@ func (d *mockStore) stub() { } } -func (d *mockStore) GetVulnerability(namespace, id string) ([]Vulnerability, error) { +func (d *mockStoreReader) GetVulnerability(namespace, id string) ([]Vulnerability, error) { var results []Vulnerability for _, vulns := range d.data[namespace] { for _, vuln := range vulns { @@ -108,15 +108,15 @@ func (d *mockStore) GetVulnerability(namespace, id string) ([]Vulnerability, err return results, nil } -func (d *mockStore) SearchForVulnerabilities(namespace, name string) ([]Vulnerability, error) { +func (d *mockStoreReader) SearchForVulnerabilities(namespace, name string) ([]Vulnerability, error) { return d.data[namespace][name], nil } -func (d *mockStore) GetAllVulnerabilities() (*[]Vulnerability, error) { +func (d *mockStoreReader) GetAllVulnerabilities() (*[]Vulnerability, error) { return nil, nil } -func (d *mockStore) GetVulnerabilityNamespaces() ([]string, error) { +func (d *mockStoreReader) GetVulnerabilityNamespaces() ([]string, error) { keys := make([]string, 0, len(d.data)) for k := range d.data { keys = append(keys, k) @@ -124,3 +124,27 @@ func (d *mockStore) GetVulnerabilityNamespaces() ([]string, error) { return keys, nil } + +func (d *mockStoreReader) GetID() (*ID, error) { + panic("not implemented") +} + +func (d *mockStoreReader) DiffStore(_ StoreReader) (*[]Diff, error) { + panic("not implemented") +} + +func (d *mockStoreReader) GetVulnerabilityMetadata(_, _ string) (*VulnerabilityMetadata, error) { + panic("not implemented") +} + +func (d *mockStoreReader) GetAllVulnerabilityMetadata() (*[]VulnerabilityMetadata, error) { + panic("not implemented") +} + +func (d *mockStoreReader) GetVulnerabilityMatchExclusion(_ string) ([]VulnerabilityMatchExclusion, error) { + panic("not implemented") +} + +func (d *mockStoreReader) Close() error { + panic("not implemented") +} diff --git a/grype/db/v5/vulnerability_provider_test.go b/grype/db/v5/vulnerability_provider_test.go index e4347d4b5a5e..82e52a0227fc 100644 --- a/grype/db/v5/vulnerability_provider_test.go +++ b/grype/db/v5/vulnerability_provider_test.go @@ -5,19 +5,19 @@ import ( "github.com/go-test/deep" "github.com/google/uuid" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/pkg/qualifier" + "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/cpe" ) func Test_GetByDistro(t *testing.T) { - provider, err := NewVulnerabilityProvider(newMockStore()) + provider, err := NewVulnerabilityProvider(newMockStoreReader()) require.NoError(t, err) d, err := distro.New(distro.Debian, "8", "") @@ -28,7 +28,7 @@ func Test_GetByDistro(t *testing.T) { Name: "neutron", } - actual, err := provider.GetByDistro(d, p) + actual, err := provider.FindVulnerabilities(search.ByDistro(*d), search.ByPackageName(p.Name)) require.NoError(t, err) expected := []vulnerability.Vulnerability{ @@ -40,7 +40,7 @@ func Test_GetByDistro(t *testing.T) { Namespace: "debian:distro:debian:8", }, PackageQualifiers: []qualifier.Qualifier{}, - CPEs: []cpe.CPE{}, + CPEs: nil, Advisories: []vulnerability.Advisory{}, }, { @@ -51,12 +51,12 @@ func Test_GetByDistro(t *testing.T) { Namespace: "debian:distro:debian:8", }, PackageQualifiers: []qualifier.Qualifier{}, - CPEs: []cpe.CPE{}, + CPEs: nil, Advisories: []vulnerability.Advisory{}, }, } - assert.Len(t, actual, len(expected)) + require.Len(t, actual, len(expected)) for idx, vuln := range actual { for _, d := range deep.Equal(expected[idx], vuln) { @@ -65,8 +65,8 @@ func Test_GetByDistro(t *testing.T) { } } -func Test_GetByDistro_nilDistro(t *testing.T) { - provider, err := NewVulnerabilityProvider(newMockStore()) +func Test_GetByDistro_emptyDistro(t *testing.T) { + provider, err := NewVulnerabilityProvider(newMockStoreReader()) require.NoError(t, err) p := pkg.Package{ @@ -74,10 +74,10 @@ func Test_GetByDistro_nilDistro(t *testing.T) { Name: "neutron", } - vulnerabilities, err := provider.GetByDistro(nil, p) + vulnerabilities, err := provider.FindVulnerabilities(search.ByDistro(distro.Distro{}), search.ByPackageName(p.Name)) - assert.Empty(t, vulnerabilities) - assert.NoError(t, err) + require.Empty(t, vulnerabilities) + require.NoError(t, err) } func Test_GetByCPE(t *testing.T) { @@ -168,18 +168,17 @@ func Test_GetByCPE(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - - provider, err := NewVulnerabilityProvider(newMockStore()) + provider, err := NewVulnerabilityProvider(newMockStoreReader()) require.NoError(t, err) - actual, err := provider.GetByCPE(test.cpe) + actual, err := provider.FindVulnerabilities(search.ByCPE(test.cpe)) if err != nil && !test.err { t.Fatalf("expected no err, got: %+v", err) } else if err == nil && test.err { t.Fatalf("expected an err, got none") } - assert.Len(t, actual, len(test.expected)) + require.Len(t, actual, len(test.expected)) for idx, vuln := range actual { for _, d := range deep.Equal(test.expected[idx], vuln) { @@ -192,22 +191,26 @@ func Test_GetByCPE(t *testing.T) { } func Test_Get(t *testing.T) { - provider, err := NewVulnerabilityProvider(newMockStore()) + provider, err := NewVulnerabilityProvider(newMockStoreReader()) + require.NoError(t, err) + + d, err := distro.New(distro.Debian, "8", "") require.NoError(t, err) - actual, err := provider.Get("CVE-2014-fake-1", "debian:distro:debian:8") + // with distro + actual, err := provider.FindVulnerabilities(search.ByDistro(*d), search.ByID("CVE-2014-fake-1")) require.NoError(t, err) expected := []vulnerability.Vulnerability{ { - Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat), - PackageName: "neutron", Reference: vulnerability.Reference{ ID: "CVE-2014-fake-1", Namespace: "debian:distro:debian:8", }, + PackageName: "neutron", + Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat), PackageQualifiers: []qualifier.Qualifier{}, - CPEs: []cpe.CPE{}, + CPEs: nil, Advisories: []vulnerability.Advisory{}, }, } @@ -220,8 +223,18 @@ func Test_Get(t *testing.T) { } } + // without distro + actual, err = provider.FindVulnerabilities(search.ByID("CVE-2014-fake-1")) + require.NoError(t, err) + + for idx, vuln := range actual { + for _, d := range deep.Equal(expected[idx], vuln) { + t.Errorf("diff: %+v", d) + } + } + // prove we survive a bad request - actual, err = provider.Get("CVE-2014-fake-3", "debian:distro:debian:8") + actual, err = provider.FindVulnerabilities(search.ByDistro(*d), search.ByID("CVE-2014-fake-3")) require.NoError(t, err) - assert.Empty(t, actual) + require.Empty(t, actual) } diff --git a/grype/db/v6/affected_package_store_test.go b/grype/db/v6/affected_package_store_test.go index bfb4969ee85e..c0f163ae7308 100644 --- a/grype/db/v6/affected_package_store_test.go +++ b/grype/db/v6/affected_package_store_test.go @@ -1,7 +1,6 @@ package v6 import ( - "fmt" "testing" "time" @@ -230,7 +229,7 @@ func TestAffectedPackageStore_AddAffectedPackages(t *testing.T) { // the IDs should have been set, and there is only one, so we know the correct values c.ID = 1 - c.PackageID = idRef(1) + c.PackageID = ptr(ID(1)) if d := cmp.Diff([]Cpe{c}, result.Package.CPEs); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) @@ -331,9 +330,9 @@ func TestAffectedPackageStore_AddAffectedPackages(t *testing.T) { expPkg := *pkg1.Package expPkg.ID = 1 cpe1.ID = 1 - cpe1.PackageID = idRef(1) + cpe1.PackageID = ptr(ID(1)) cpe2.ID = 2 - cpe2.PackageID = idRef(1) + cpe2.PackageID = ptr(ID(1)) expPkg.CPEs = []Cpe{cpe1, cpe2} expected := []Package{ @@ -512,9 +511,8 @@ func TestAffectedPackageStore_GetAffectedPackages_ByCPE(t *testing.T) { return } if d := cmp.Diff(tt.expected, result, cmpopts.EquateEmpty()); d != "" { - t.Errorf(fmt.Sprintf("unexpected result: %s", d)) + t.Errorf("unexpected result: %s", d) } - }) } } @@ -872,7 +870,7 @@ func TestAffectedPackageStore_GetAffectedPackages(t *testing.T) { return } if d := cmp.Diff(expected, result); d != "" { - t.Errorf(fmt.Sprintf("unexpected result: %s", d)) + t.Errorf("unexpected result: %s", d) } }) } @@ -1376,11 +1374,6 @@ func expectErrIs(t *testing.T, expected error) require.ErrorAssertionFunc { } } -func idRef(i int64) *ID { - v := ID(i) - return &v -} - func pkgFromName(name string) *PackageSpecifier { return &PackageSpecifier{Name: name} } diff --git a/grype/db/v6/blob_store.go b/grype/db/v6/blob_store.go index 4a4c752e0d43..f124a0f18b5c 100644 --- a/grype/db/v6/blob_store.go +++ b/grype/db/v6/blob_store.go @@ -91,7 +91,9 @@ func (s *blobStore) attachBlobValue(bs ...blobable) error { b := bs[i] id := b.getBlobID() - if id == 0 { + + // skip fetching this blob if there is no blobID, or if we already have this blob + if id == 0 || b.getBlobValue() != nil { continue } diff --git a/grype/db/v6/cvss.go b/grype/db/v6/cvss.go new file mode 100644 index 000000000000..992a8f915a54 --- /dev/null +++ b/grype/db/v6/cvss.go @@ -0,0 +1,37 @@ +package v6 + +import ( + "github.com/anchore/grype/grype/vulnerability" +) + +func toCvss(severities ...Severity) []vulnerability.Cvss { + //nolint:prealloc + var out []vulnerability.Cvss + for _, sev := range severities { + switch sev.Scheme { + case SeveritySchemeCVSS: + default: + // not a CVSS score + continue + } + score, ok := sev.Value.(CVSSSeverity) + if !ok { + // not a CVSS score + continue + } + out = append(out, vulnerability.Cvss{ + Source: sev.Source, + Type: string(sev.Scheme), + Version: score.Version, + Vector: score.Vector, + Metrics: vulnerability.CvssMetrics{ + // FIXME: where do these metrics come from? + //BaseScore: score.Metrics.BaseScore, + //ExploitabilityScore: score.Metrics.ExploitabilityScore, + //ImpactScore: score.Metrics.ImpactScore, + }, + //VendorMetadata: score.VendorMetadata, + }) + } + return out +} diff --git a/grype/db/v6/db.go b/grype/db/v6/db.go index 47ed8994db6a..255c1825dbf8 100644 --- a/grype/db/v6/db.go +++ b/grype/db/v6/db.go @@ -38,6 +38,8 @@ type Reader interface { VulnerabilityStoreReader AffectedPackageStoreReader AffectedCPEStoreReader + getDB() *gorm.DB + attachBlobValue(...blobable) error } type Writer interface { diff --git a/grype/db/v6/fillers.go b/grype/db/v6/fillers.go new file mode 100644 index 000000000000..9998e1335e3b --- /dev/null +++ b/grype/db/v6/fillers.go @@ -0,0 +1,105 @@ +package v6 + +import ( + "errors" +) + +// fillAffectedPackageHandles lazy loads all properties on the list of AffectedPackageHandles +func fillAffectedPackageHandles(reader Reader, handles []*AffectedPackageHandle) error { + return errors.Join( + reader.attachBlobValue(toBlobables(handles)...), + fillRefs(reader, handles, affectedPackageHandleOperatingSystemRef, operatingSystemID), + fillRefs(reader, handles, affectedPackageHandlePackageRef, packageID), + fillVulnerabilityHandles(reader, handles, affectedPackageHandleVulnerabilityHandleRef), + ) +} + +func affectedPackageHandleOperatingSystemRef(t *AffectedPackageHandle) idRef[OperatingSystem] { + return idRef[OperatingSystem]{ + id: t.OperatingSystemID, + ref: &t.OperatingSystem, + } +} + +func affectedPackageHandlePackageRef(t *AffectedPackageHandle) idRef[Package] { + return idRef[Package]{ + id: &t.PackageID, + ref: &t.Package, + } +} + +func affectedPackageHandleVulnerabilityHandleRef(t *AffectedPackageHandle) idRef[VulnerabilityHandle] { + return idRef[VulnerabilityHandle]{ + id: &t.VulnerabilityID, + ref: &t.Vulnerability, + } +} + +// fillAffectedCPEHandles lazy loads all properties on the list of AffectedCPEHandles +func fillAffectedCPEHandles(reader Reader, handles []*AffectedCPEHandle) error { + return errors.Join( + reader.attachBlobValue(toBlobables(handles)...), + fillRefs(reader, handles, affectedCPEHandleCpeRef, cpeHandleID), + fillVulnerabilityHandles(reader, handles, affectedCPEHandleVulnerabilityHandleRef), + ) +} + +func affectedCPEHandleCpeRef(t *AffectedCPEHandle) idRef[Cpe] { + return idRef[Cpe]{ + id: &t.CpeID, + ref: &t.CPE, + } +} + +func affectedCPEHandleVulnerabilityHandleRef(t *AffectedCPEHandle) idRef[VulnerabilityHandle] { + return idRef[VulnerabilityHandle]{ + id: &t.VulnerabilityID, + ref: &t.Vulnerability, + } +} + +// fillVulnerabilityHandles lazy loads vulnerability handle properties +func fillVulnerabilityHandles[T any](reader Reader, handles []*T, vulnHandleRef refProvider[T, VulnerabilityHandle]) error { + // fill vulnerabilities + if err := fillRefs(reader, handles, vulnHandleRef, vulnerabilityHandleID); err != nil { + return err + } + var providerRefs []ref[string, Provider] + vulnHandles := make([]*VulnerabilityHandle, len(handles)) + for i := range handles { + vulnHandles[i] = *vulnHandleRef(handles[i]).ref + providerRefs = append(providerRefs, ref[string, Provider]{ + id: &vulnHandles[i].ProviderID, + ref: &vulnHandles[i].Provider, + }) + } + // then get references to them to fill the properties + return errors.Join( + reader.attachBlobValue(toBlobables(vulnHandles)...), + reader.fillProviders(providerRefs), + ) +} + +func vulnerabilityHandleID(h *VulnerabilityHandle) ID { + return h.ID +} + +func cpeHandleID(h *Cpe) ID { + return h.ID +} + +func operatingSystemID(h *OperatingSystem) ID { + return h.ID +} + +func packageID(h *Package) ID { + return h.ID +} + +func toBlobables[T blobable](handles []T) []blobable { + out := make([]blobable, len(handles)) + for i := range handles { + out[i] = handles[i] + } + return out +} diff --git a/grype/db/v6/log_dropped.go b/grype/db/v6/log_dropped.go new file mode 100644 index 000000000000..704dda441aff --- /dev/null +++ b/grype/db/v6/log_dropped.go @@ -0,0 +1,13 @@ +package v6 + +import "github.com/anchore/grype/internal/log" + +// logDroppedVulnerability is a hook called when vulnerabilities are dropped from consideration in a vulnerability Provider, +// this offers a convenient location to set a breakpoint +func logDroppedVulnerability(vulnerabilityID string, reason any, context ...any) { + log.WithFields( + "vulnerability", vulnerabilityID, + "reason", reason, + "context", context, + ).Trace("dropped vulnerability") +} diff --git a/grype/db/v6/models.go b/grype/db/v6/models.go index 72b7af008c47..bb0aaf0ba621 100644 --- a/grype/db/v6/models.go +++ b/grype/db/v6/models.go @@ -163,6 +163,9 @@ type VulnerabilityHandle struct { } func (v VulnerabilityHandle) getBlobValue() any { + if v.BlobValue == nil { + return nil // must return untyped nil or getBlobValue() == nil will always be false + } return v.BlobValue } @@ -254,6 +257,9 @@ type AffectedPackageHandle struct { } func (aph AffectedPackageHandle) getBlobValue() any { + if aph.BlobValue == nil { + return nil // must return untyped nil or getBlobValue() == nil will always be false + } return aph.BlobValue } @@ -540,7 +546,7 @@ type AffectedCPEHandle struct { VulnerabilityID ID `gorm:"column:vulnerability_id;not null"` Vulnerability *VulnerabilityHandle `gorm:"foreignKey:VulnerabilityID"` - CpeID ID `gorm:"column:cpe_id"` + CpeID ID `gorm:"column:cpe_id;index"` CPE *Cpe `gorm:"foreignKey:CpeID"` BlobID ID `gorm:"column:blob_id"` @@ -552,6 +558,9 @@ func (v AffectedCPEHandle) getBlobID() ID { } func (v AffectedCPEHandle) getBlobValue() any { + if v.BlobValue == nil { + return nil // must return untyped nil or getBlobValue() == nil will always be false + } return v.BlobValue } diff --git a/grype/db/v6/provider_store.go b/grype/db/v6/provider_store.go index 7487b67cb499..5df66a23a217 100644 --- a/grype/db/v6/provider_store.go +++ b/grype/db/v6/provider_store.go @@ -12,6 +12,7 @@ import ( type ProviderStoreReader interface { GetProvider(name string) (*Provider, error) AllProviders() ([]Provider, error) + fillProviders(handles []ref[string, Provider]) error } type providerStore struct { @@ -51,3 +52,24 @@ func (s *providerStore) AllProviders() ([]Provider, error) { return providers, nil } + +func (s *providerStore) fillProviders(handles []ref[string, Provider]) error { + providers, err := s.AllProviders() + if err != nil { + return err + } + + providerMap := make(map[string]*Provider) + for _, provider := range providers { + providerMap[provider.ID] = &provider + } + + for _, handle := range handles { + if handle.id == nil { + continue + } + *handle.ref = providerMap[*handle.id] + } + + return nil +} diff --git a/grype/db/v6/refs.go b/grype/db/v6/refs.go new file mode 100644 index 000000000000..228c1479965f --- /dev/null +++ b/grype/db/v6/refs.go @@ -0,0 +1,75 @@ +package v6 + +import ( + "slices" +) + +type ref[ID, T any] struct { + id *ID + ref **T +} + +type idRef[T any] ref[ID, T] + +type refProvider[T, R any] func(*T) idRef[R] + +type idProvider[T any] func(*T) ID + +func fillRefs[T, R any](reader Reader, handles []*T, getRef refProvider[T, R], refID idProvider[R]) error { + if len(handles) == 0 { + return nil + } + + // collect all ref locations and IDs + var refs []idRef[R] + var ids []ID + for i := range handles { + ref := getRef(handles[i]) + if ref.id == nil { + continue + } + refs = append(refs, ref) + id := *ref.id + if slices.Contains(ids, id) { + continue + } + ids = append(ids, id) + } + + // load a map with all id -> ref results + var values []R + tx := reader.getDB().Where("id IN (?)", ids) + err := tx.Find(&values).Error + if err != nil { + return err + } + refsByID := map[ID]*R{} + for i := range values { + v := &values[i] + id := refID(v) + refsByID[id] = v + } + + // assign matching refs back to the object graph + for _, ref := range refs { + if ref.id == nil { + continue + } + incomingRef := refsByID[*ref.id] + *ref.ref = incomingRef + } + + return nil +} + +// ptrs returns a slice of pointers to each element in the provided slice +func ptrs[T any](values []T) []*T { + if len(values) == 0 { + return nil + } + out := make([]*T, len(values)) + for i := range values { + out[i] = &values[i] + } + return out +} diff --git a/grype/db/v6/store.go b/grype/db/v6/store.go index d441a4f56004..6f7eaccff6c2 100644 --- a/grype/db/v6/store.go +++ b/grype/db/v6/store.go @@ -18,7 +18,16 @@ type store struct { blobStore *blobStore db *gorm.DB config Config - readOnly bool + empty bool + writable bool +} + +func (s *store) getDB() *gorm.DB { + return s.db +} + +func (s *store) attachBlobValue(values ...blobable) error { + return s.blobStore.attachBlobValue(values...) } func InitialData() []any { @@ -56,14 +65,16 @@ func newStore(cfg Config, empty, writable bool) (*store, error) { blobStore: bs, db: db, config: cfg, - readOnly: !empty && !writable, + empty: empty, + writable: writable, }, nil } // Close closes the store and finalizes the blobs when the DB is open for writing. If open for reading, it does nothing. func (s *store) Close() error { log.Debug("closing store") - if s.readOnly { + if !s.writable || !s.empty { + // if not empty, this writable execution created indexes return nil } diff --git a/grype/db/v6/store_test.go b/grype/db/v6/store_test.go index 7fdd7da088a8..a3b95de0909b 100644 --- a/grype/db/v6/store_test.go +++ b/grype/db/v6/store_test.go @@ -11,7 +11,8 @@ func TestStoreClose(t *testing.T) { t.Run("readonly mode does nothing", func(t *testing.T) { s := setupTestStore(t) - s.readOnly = true + s.empty = false + s.writable = false err := s.Close() require.NoError(t, err) diff --git a/grype/db/v6/vulnerability.go b/grype/db/v6/vulnerability.go new file mode 100644 index 000000000000..c35980d022bb --- /dev/null +++ b/grype/db/v6/vulnerability.go @@ -0,0 +1,225 @@ +package v6 + +import ( + "fmt" + "strings" + + "github.com/anchore/grype/grype/pkg/qualifier" + "github.com/anchore/grype/grype/pkg/qualifier/platformcpe" + "github.com/anchore/grype/grype/pkg/qualifier/rpmmodularity" + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/log" + "github.com/anchore/syft/syft/cpe" +) + +func newVulnerabilityFromAffectedPackageHandle(affected AffectedPackageHandle, affectedRange AffectedRange) (*vulnerability.Vulnerability, error) { + packageName := "" + if affected.Package != nil { + packageName = affected.Package.Name + } + + if affected.Vulnerability == nil || affected.Vulnerability.BlobValue == nil || affected.BlobValue == nil { + return nil, fmt.Errorf("nil data when attempting to create vulnerability from AffectedPackageHandle") + } + + return newVulnerabilityFromParts(packageName, affected.Vulnerability, affected.BlobValue, affectedRange, &affected, nil) +} + +func newVulnerabilityFromAffectedCPEHandle(affected AffectedCPEHandle, affectedRange AffectedRange) (*vulnerability.Vulnerability, error) { + if affected.Vulnerability == nil || affected.Vulnerability.BlobValue == nil || affected.BlobValue == nil { + return nil, fmt.Errorf("nil data when attempting to create vulnerability from AffectedCPEHandle") + } + return newVulnerabilityFromParts(affected.CPE.Product, affected.Vulnerability, affected.BlobValue, affectedRange, nil, &affected) +} + +func newVulnerabilityFromParts(packageName string, vuln *VulnerabilityHandle, affected *AffectedPackageBlob, affectedRange AffectedRange, affectedPackageHandle *AffectedPackageHandle, affectedCpeHandle *AffectedCPEHandle) (*vulnerability.Vulnerability, error) { + if vuln.BlobValue == nil { + return nil, fmt.Errorf("vuln has no blob value: %+v", vuln) + } + + v5namespace := MimicV5Namespace(vuln, affectedPackageHandle) + + var relatedVulnerabilities []vulnerability.Reference + for _, alias := range vuln.BlobValue.Aliases { + relatedVulnerabilities = append(relatedVulnerabilities, vulnerability.Reference{ + ID: alias, + Namespace: v5namespace, + }) + } + var packageQualifiers []qualifier.Qualifier + if affected != nil { + for _, cve := range affected.CVEs { + relatedVulnerabilities = append(relatedVulnerabilities, vulnerability.Reference{ + ID: cve, + Namespace: v5namespace, + }) + } + packageQualifiers = toPackageQualifiers(affected.Qualifiers) + } + versionFormat := version.ParseFormat(affectedRange.Version.Type) + constraint, err := version.GetConstraint(affectedRange.Version.Constraint, versionFormat) + if err != nil { + log.WithFields("error", err, "constraint", affectedRange.Version.Constraint).Debug("unable to parse constraint") + return nil, nil + } + + return &vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: vuln.Name, + Namespace: v5namespace, + Internal: vuln, // just hold a reference to the vulnHandle for later use + }, + PackageName: packageName, + PackageQualifiers: packageQualifiers, + Constraint: constraint, + CPEs: toCPEs(affectedPackageHandle, affectedCpeHandle), + RelatedVulnerabilities: relatedVulnerabilities, + Fix: toFix(affectedRange.Fix), + Advisories: toAdvisories(affectedRange.Fix), + }, nil +} + +// MimicV5Namespace returns the namespace for a given affected package based on what schema v5 did. +// +//nolint:funlen +func MimicV5Namespace(vuln *VulnerabilityHandle, affected *AffectedPackageHandle) string { + if affected == nil { // for CPE matches + return "nvd:cpe" + } + switch vuln.Provider.ID { + case "nvd": + return "nvd:cpe" + case "github": + language := affected.Package.Ecosystem + // normalize from purl type, github ecosystem types, and vunnel mappings + switch strings.ToLower(language) { + case "golang", "go-module": + language = "go" + case "composer", "php-composer": + language = "php" + case "cargo", "rust-crate": + language = "rust" + case "dart-pub", "pub": + language = "dart" + case "nuget": + language = "dotnet" + case "maven": + language = "java" + case "swifturl": + language = "swift" + case "npm", "node": + language = "javascript" + case "pypi", "pip": + language = "python" + case "rubygems", "gem": + language = "ruby" + } + return fmt.Sprintf("github:language:%s", language) + } + if affected.OperatingSystem != nil { + // distro family fixes + family := affected.OperatingSystem.Name + ver := affected.OperatingSystem.Version() + switch affected.OperatingSystem.Name { + case "amazon": + family = "amazonlinux" + case "mariner": + switch ver { + case "1.0", "2.0": + family = "mariner" + default: + family = "azurelinux" + } + case "oracle": + family = "oraclelinux" + } + + // provider fixes + pr := vuln.Provider.ID + if pr == "rhel" { + pr = "redhat" + } + + // version fixes + switch vuln.Provider.ID { + case "rhel", "oracle": + // ensure we only keep the major version + ver = strings.Split(ver, ".")[0] + } + + return fmt.Sprintf("%s:distro:%s:%s", pr, family, ver) + } + return vuln.Provider.ID +} + +func toPackageQualifiers(qualifiers *AffectedPackageQualifiers) []qualifier.Qualifier { + if qualifiers == nil { + return nil + } + var out []qualifier.Qualifier + for _, c := range qualifiers.PlatformCPEs { + out = append(out, platformcpe.New(c)) + } + if qualifiers.RpmModularity == "" { + out = append(out, rpmmodularity.New(qualifiers.RpmModularity)) + } + return out +} + +func toFix(fix *Fix) vulnerability.Fix { + if fix == nil || fix.Version == "" { + return vulnerability.Fix{} + } + return vulnerability.Fix{ + Versions: []string{fix.Version}, + State: vulnerability.FixState(fix.State), + } +} + +func toAdvisories(fix *Fix) []vulnerability.Advisory { + if fix == nil || fix.Detail == nil { + return nil + } + + var advisories []vulnerability.Advisory + for _, r := range fix.Detail.References { + if r.URL == "" { + continue + } + advisories = append(advisories, vulnerability.Advisory{ + Link: r.URL, + }) + } + return advisories +} + +func toCPEs(affectedPackageHandle *AffectedPackageHandle, affectedCPEHandle *AffectedCPEHandle) []cpe.CPE { + var out []cpe.CPE + var cpes []Cpe + if affectedPackageHandle != nil { + cpes = affectedPackageHandle.Package.CPEs + } + if affectedCPEHandle != nil && affectedCPEHandle.CPE != nil { + cpes = append(cpes, *affectedCPEHandle.CPE) + } + for _, c := range cpes { + out = append(out, cpe.CPE{ + Attributes: cpe.Attributes{ + Part: c.Part, + Vendor: c.Vendor, + Product: c.Product, + Version: cpe.Any, + Update: cpe.Any, + Edition: c.Edition, + SWEdition: c.SoftwareEdition, + TargetSW: c.TargetSoftware, + TargetHW: c.TargetHardware, + Other: c.Other, + Language: c.Language, + }, + Source: "", + }) + } + return out +} diff --git a/grype/db/v6/vulnerability_provider.go b/grype/db/v6/vulnerability_provider.go new file mode 100644 index 000000000000..35dc5ecffaee --- /dev/null +++ b/grype/db/v6/vulnerability_provider.go @@ -0,0 +1,384 @@ +package v6 + +import ( + "fmt" + "io" + + "github.com/iancoleman/strcase" + + "github.com/anchore/grype/grype/search" + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/log" + "github.com/anchore/syft/syft/cpe" +) + +func NewVulnerabilityProvider(rdr Reader) vulnerability.Provider { + return &vulnerabilityProvider{ + reader: rdr, + } +} + +type vulnerabilityProvider struct { + reader Reader +} + +var _ interface { + vulnerability.Provider +} = (*vulnerabilityProvider)(nil) + +func (s vulnerabilityProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) { + vuln, ok := ref.Internal.(*VulnerabilityHandle) + if !ok { + return nil, nil + } + return &vulnerability.Metadata{ + ID: vuln.Name, + DataSource: vuln.Provider.ID, + Namespace: ref.Namespace, + Severity: toSeverityString(vuln), + URLs: toURLs(vuln), + Description: vuln.BlobValue.Description, + Cvss: toCvss(vuln.BlobValue.Severities...), + }, nil +} + +func (s vulnerabilityProvider) Close() error { + return s.reader.(io.Closer).Close() +} + +//nolint:funlen,gocognit,gocyclo +func (s vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) { + if err := search.ValidateCriteria(criteria); err != nil { + return nil, err + } + + var err error + + var out []vulnerability.Vulnerability + for _, criteriaSet := range search.CriteriaIterator(criteria) { + var vulnSpecs VulnerabilitySpecifiers + var osSpecs OSSpecifiers + var pkgSpec *PackageSpecifier + var cpeSpec *cpe.Attributes + + for i := 0; i < len(criteriaSet); i++ { + applied := false + switch c := criteriaSet[i].(type) { + case *search.PackageNameCriteria: + if pkgSpec == nil { + pkgSpec = &PackageSpecifier{} + } + pkgSpec.Name = c.PackageName + applied = true + case *search.LanguageCriteria: + if pkgSpec == nil { + pkgSpec = &PackageSpecifier{} + } + pkgSpec.Ecosystem = string(c.Language) + applied = true + case *search.IDCriteria: + vulnSpecs = append(vulnSpecs, VulnerabilitySpecifier{ + Name: c.ID, + }) + applied = true + case *search.CPECriteria: + if cpeSpec == nil { + cpeSpec = &cpe.Attributes{} + } + *cpeSpec = c.CPE.Attributes + if cpeSpec.Product == cpe.Any || cpeSpec.Vendor == cpe.Any { + return nil, fmt.Errorf("must specify vendor and product to search by CPE; got: %s", c.CPE.Attributes.BindToFmtString()) + } + if pkgSpec == nil { + pkgSpec = &PackageSpecifier{} + } + pkgSpec.CPE = &c.CPE.Attributes + applied = true + case *search.DistroCriteria: + for _, d := range c.Distros { + osSpecs = append(osSpecs, &OSSpecifier{ + Name: d.Name(), + MajorVersion: d.MajorVersion(), + MinorVersion: d.MinorVersion(), + }) + } + applied = true + } + + // remove fully applied criteria from later checks + if applied { + criteriaSet = append(criteriaSet[0:i], criteriaSet[i+1:]...) + i-- + } + } + + versionMatcher, remainingCriteria := splitConstraintMatcher(criteriaSet...) + + var affectedPackages []AffectedPackageHandle + var affectedCPEs []AffectedCPEHandle + + if pkgSpec != nil || len(vulnSpecs) > 0 { + affectedPackages, err = s.reader.GetAffectedPackages(pkgSpec, &GetAffectedPackageOptions{ + OSs: osSpecs, + Vulnerabilities: vulnSpecs, + PreloadBlob: true, + }) + if err != nil { + return nil, err + } + + affectedPackages = filterAffectedPackageVersions(versionMatcher, affectedPackages) + + // after filtering, read vulnerability data + if err = fillAffectedPackageHandles(s.reader, ptrs(affectedPackages)); err != nil { + return nil, err + } + } + + if cpeSpec != nil { + affectedCPEs, err = s.reader.GetAffectedCPEs(cpeSpec, &GetAffectedCPEOptions{ + Vulnerabilities: vulnSpecs, + PreloadBlob: true, + }) + if err != nil { + return nil, err + } + + affectedCPEs = filterAffectedCPEVersions(versionMatcher, affectedCPEs) + + // after filtering, read vulnerability data + if err = fillAffectedCPEHandles(s.reader, ptrs(affectedCPEs)); err != nil { + return nil, err + } + } + + // fill complete vulnerabilities for this set -- these should have already had all properties lazy loaded + vulns, err := toVulnerabilities(affectedPackages, affectedCPEs) + if err != nil { + return nil, err + } + + // filter vulnerabilities by any remaining criteria such as ByQualifiedPackages + vulns, err = s.filterVulnerabilities(vulns, remainingCriteria...) + if err != nil { + return nil, err + } + + out = append(out, vulns...) + } + + return out, nil +} + +func (s vulnerabilityProvider) filterVulnerabilities(vulns []vulnerability.Vulnerability, criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) { + isMatch := func(v vulnerability.Vulnerability) (bool, error) { + for _, c := range criteria { + if _, ok := c.(search.VersionConstraintMatcher); ok { + continue // already run + } + matches, err := c.MatchesVulnerability(v) + if !matches || err != nil { + logDroppedVulnerability(v.Reference.ID, err, c) + return false, err + } + } + return true, nil + } + for i := 0; i < len(vulns); i++ { + matches, err := isMatch(vulns[i]) + if err != nil { + return nil, err + } + if !matches { + vulns = append(vulns[0:i], vulns[i+1:]...) + i-- + } + } + return vulns, nil +} + +// toVulnerabilities takes fully-filled handles and returns all vulnerabilities from them +func toVulnerabilities(packageHandles []AffectedPackageHandle, cpeHandles []AffectedCPEHandle) ([]vulnerability.Vulnerability, error) { + var out []vulnerability.Vulnerability + + for _, packageHandle := range packageHandles { + if packageHandle.BlobValue == nil { + log.Debugf("unable to find blobValue for %+v", packageHandle) + continue + } + for _, rng := range packageHandle.BlobValue.Ranges { + v, err := newVulnerabilityFromAffectedPackageHandle(packageHandle, rng) + if err != nil { + return nil, err + } + if v == nil { + continue + } + out = append(out, *v) + } + } + + for _, c := range cpeHandles { + if c.BlobValue == nil { + log.Debugf("unable to find blobValue for %+v", c) + continue + } + for _, rng := range c.BlobValue.Ranges { + v, err := newVulnerabilityFromAffectedCPEHandle(c, rng) + if err != nil { + return nil, err + } + if v == nil { + continue + } + out = append(out, *v) + } + } + + return out, nil +} + +// splitConstraintMatcher returns a search.VersionConstraintMatcher from all search.VersionConstraintMatcher(s) in the criteria +func splitConstraintMatcher(criteria ...vulnerability.Criteria) (search.VersionConstraintMatcher, []vulnerability.Criteria) { + var remaining []vulnerability.Criteria + var matcher search.VersionConstraintMatcher + for _, c := range criteria { + if nextMatcher, ok := c.(search.VersionConstraintMatcher); ok { + if matcher == nil { + matcher = nextMatcher + } else { + matcher = search.MultiConstraintMatcher(matcher, nextMatcher) + } + } else { + remaining = append(remaining, c) + } + } + return matcher, remaining +} + +func filterAffectedPackageVersions(constraintMatcher search.VersionConstraintMatcher, packages []AffectedPackageHandle) []AffectedPackageHandle { + // no constraint matcher, just return all packages + if constraintMatcher == nil { + return packages + } + for packageIdx := 0; packageIdx < len(packages); packageIdx++ { + handle := packages[packageIdx] + filterAffectedPackageRanges(constraintMatcher, handle.BlobValue) + if len(handle.BlobValue.Ranges) > 0 { + continue // keep this handle + } + + id := "" + if handle.Vulnerability != nil { + id = handle.Vulnerability.Name + } else if len(handle.BlobValue.CVEs) > 0 { + id = handle.BlobValue.CVEs[0] + } + logDroppedVulnerability(id, "constraints", handle, constraintMatcher) + + // if we haven't matched a constraint, remove the package + packages = append(packages[0:packageIdx], packages[packageIdx+1:]...) + packageIdx-- + } + return packages +} + +func filterAffectedCPEVersions(constraintMatcher search.VersionConstraintMatcher, handles []AffectedCPEHandle) []AffectedCPEHandle { + // no constraint matcher, just return all packages + if constraintMatcher == nil { + return handles + } + var out []AffectedCPEHandle + for _, handle := range handles { + filterAffectedPackageRanges(constraintMatcher, handle.BlobValue) + if len(handle.BlobValue.Ranges) > 0 { + out = append(out, handle) + continue // keep this handle + } + + id := "" + if handle.Vulnerability != nil { + id = handle.Vulnerability.Name + } else if len(handle.BlobValue.CVEs) > 0 { + id = handle.BlobValue.CVEs[0] + } + logDroppedVulnerability(id, "constraints", handle, constraintMatcher) + } + return out +} + +// filterAffectedPackageRanges returns true if all ranges removed +func filterAffectedPackageRanges(matcher search.VersionConstraintMatcher, b *AffectedPackageBlob) { + var out []AffectedRange + for _, r := range b.Ranges { + v := r.Version + format := version.ParseFormat(v.Type) + constraint, err := version.GetConstraint(v.Constraint, format) + if err != nil || constraint == nil { + log.WithFields("error", err, "constraint", v.Constraint, "format", v.Type).Debug("unable to parse constraint") + continue + } + matches, err := matcher.MatchesConstraint(constraint) + if err != nil { + log.WithFields("error", err, "constraint", v.Constraint, "format", v.Type).Debug("match constraint error") + } + if matches { + out = append(out, r) + continue + } + } + b.Ranges = out +} + +func toSeverityString(vuln *VulnerabilityHandle) string { + return strcase.ToCamel(getSeverity(vuln).String()) +} + +func getSeverity(vuln *VulnerabilityHandle) vulnerability.Severity { + // TODO the severity is stored differently than v5, we need to figure out the correct way to determine + // what to present to the user + if vuln.BlobValue == nil { + return vulnerability.UnknownSeverity + } + if len(vuln.BlobValue.Severities) > 0 { + return extractSeverity(vuln.BlobValue.Severities[0].Value) + } + return vulnerability.UnknownSeverity +} + +func extractSeverity(severity any) vulnerability.Severity { + switch sev := severity.(type) { + case CVSSSeverity: + return normalizedScoreToSeverityValue(sev.Score / 10.) + default: + return vulnerability.UnknownSeverity + } +} + +func normalizedScoreToSeverityValue(score float64) vulnerability.Severity { + if score > .9 { + return vulnerability.CriticalSeverity + } + if score > .7 { + return vulnerability.HighSeverity + } + if score > .4 { + return vulnerability.MediumSeverity + } + if score > .2 { + return vulnerability.LowSeverity + } + if score >= .0 { + return vulnerability.NegligibleSeverity + } + return vulnerability.UnknownSeverity +} + +func toURLs(vuln *VulnerabilityHandle) []string { + var out []string + for _, v := range vuln.BlobValue.References { + out = append(out, v.URL) + } + return out +} diff --git a/grype/db/v6/vulnerability_provider_mocks_test.go b/grype/db/v6/vulnerability_provider_mocks_test.go new file mode 100644 index 000000000000..d1b1b2753996 --- /dev/null +++ b/grype/db/v6/vulnerability_provider_mocks_test.go @@ -0,0 +1,253 @@ +package v6 + +import ( + "encoding/hex" + "testing" + "time" + + "github.com/stretchr/testify/require" + + v5 "github.com/anchore/grype/grype/db/v5" + "github.com/anchore/grype/grype/db/v5/namespace" + distroNs "github.com/anchore/grype/grype/db/v5/namespace/distro" + "github.com/anchore/grype/grype/db/v5/namespace/language" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/log" + "github.com/anchore/syft/syft/cpe" +) + +func testVulnerabilityProvider(t *testing.T) vulnerability.Provider { + t.Helper() + tmp := t.TempDir() + w, err := NewWriter(Config{ + DBDirPath: tmp, + }) + defer log.CloseAndLogError(w, tmp) + require.NoError(t, err) + + aDayAgo := time.Now().Add(-1 * 24 * time.Hour) + aWeekAgo := time.Now().Add(-7 * 24 * time.Hour) + twoWeeksAgo := time.Now().Add(-14 * 24 * time.Hour) + + prov := &Provider{ + ID: "debian", + Version: "1", + Processor: "debian-processor", + DateCaptured: &aDayAgo, + InputDigest: hex.EncodeToString([]byte("debian")), + } + + v5vulns := []v5.Vulnerability{ + // neutron + { + PackageName: "neutron", + Namespace: "debian:distro:debian:8", + VersionConstraint: "< 2014.1.3-6", + ID: "CVE-2014-fake-1", + VersionFormat: "deb", + }, + { + PackageName: "neutron", + Namespace: "debian:distro:debian:8", + VersionConstraint: "< 2013.0.2-1", + ID: "CVE-2013-fake-2", + VersionFormat: "deb", + }, + // poison the well! this is not a valid entry, but we want the matching process to survive and find other good results... + { + PackageName: "neutron", + Namespace: "debian:distro:debian:8", + VersionConstraint: "< 70.3.0-rc0", // intentionally bad value + ID: "CVE-2014-fake-3", + VersionFormat: "apk", + }, + + // activerecord + { + PackageName: "activerecord", + Namespace: "nvd:cpe", + VersionConstraint: "< 3.7.6", + ID: "CVE-2014-fake-3", + VersionFormat: "unknown", + CPEs: []string{ + "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", + }, + }, + { + PackageName: "activerecord", + Namespace: "nvd:cpe", + VersionConstraint: "< 3.7.4", + ID: "CVE-2014-fake-4", + VersionFormat: "unknown", + CPEs: []string{ + "cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", + }, + }, + { + PackageName: "activerecord", + Namespace: "nvd:cpe", + VersionConstraint: "= 4.0.1", + ID: "CVE-2014-fake-5", + VersionFormat: "unknown", + CPEs: []string{ + "cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*", // shouldn't match on this + }, + }, + { + PackageName: "activerecord", + Namespace: "nvd:cpe", + VersionConstraint: "< 98SP3", + ID: "CVE-2014-fake-6", + VersionFormat: "unknown", + CPEs: []string{ + "cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*", // shouldn't match on this + }, + }, + // poison the well! this is not a valid entry, but we want the matching process to survive and find other good results... + { + PackageName: "activerecord", + Namespace: "nvd:cpe", + VersionConstraint: "< 70.3.0-rc0", // intentionally bad value + ID: "CVE-2014-fake-7", + VersionFormat: "apk", + CPEs: []string{ + "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", + }, + }, + } + + for _, v := range v5vulns { + var os *OperatingSystem + + switch v.Namespace { + case "nvd:cpe": + case "debian:distro:debian:8": + os = &OperatingSystem{ + Name: "debian", + MajorVersion: "8", + } + } + + vuln := &VulnerabilityHandle{ + ID: 0, + Name: v.ID, + Status: "", + PublishedDate: &twoWeeksAgo, + ModifiedDate: &aWeekAgo, + WithdrawnDate: nil, + ProviderID: prov.ID, + Provider: prov, + BlobID: 0, + BlobValue: &VulnerabilityBlob{ + ID: v.ID, + Assigners: []string{v.ID + "-assigner-1", v.ID + "-assigner-2"}, + Description: v.ID + "-description", + References: []Reference{ + { + //URL: "http://somewhere/" + v.ID, + Tags: []string{v.ID + "-tag-1", v.ID + "-tag-2"}, + }, + }, + //Aliases: []string{"GHSA-" + v.ID}, + Severities: []Severity{ + { + Scheme: SeveritySchemeCVSS, + Value: "high", + Source: "", + Rank: 0, + }, + }, + }, + } + + err = w.AddVulnerabilities(vuln) + require.NoError(t, err) + + var cpes []Cpe + for _, c := range v.CPEs { + cp, err := cpe.New(c, "") + require.NoError(t, err) + cpes = append(cpes, Cpe{ + Part: cp.Attributes.Part, + Vendor: cp.Attributes.Vendor, + Product: cp.Attributes.Product, + Edition: cp.Attributes.Edition, + Language: cp.Attributes.Language, + SoftwareEdition: cp.Attributes.SWEdition, + TargetHardware: cp.Attributes.TargetHW, + TargetSoftware: cp.Attributes.TargetSW, + Other: cp.Attributes.Other, + }) + } + + packageType := "" + + ns, err := namespace.FromString(v.Namespace) + require.NoError(t, err) + + d, _ := ns.(*distroNs.Namespace) + if d != nil { + packageType = string(d.DistroType()) + } + lang, _ := ns.(*language.Namespace) + if lang != nil { + packageType = string(lang.Language()) + } + + pkg := &Package{ + ID: 0, + Ecosystem: packageType, + Name: v.PackageName, + //CPEs: cpes, + } + + ap := &AffectedPackageHandle{ + ID: 0, + VulnerabilityID: 0, + Vulnerability: vuln, + OperatingSystemID: nil, + OperatingSystem: os, + PackageID: 0, + Package: pkg, + BlobID: 0, + BlobValue: &AffectedPackageBlob{ + CVEs: nil, + Qualifiers: nil, + Ranges: []AffectedRange{ + { + Fix: nil, + Version: AffectedVersion{ + Type: v.VersionFormat, + Constraint: v.VersionConstraint, + }, + }, + }, + }, + } + + err = w.AddAffectedPackages(ap) + require.NoError(t, err) + + for _, c := range cpes { + ac := &AffectedCPEHandle{ + Vulnerability: vuln, + CPE: &c, + BlobValue: &AffectedPackageBlob{ + Ranges: []AffectedRange{ + { + Version: AffectedVersion{ + Type: v.VersionFormat, + Constraint: v.VersionConstraint, + }, + }, + }, + }, + } + + err = w.AddAffectedCPEs(ac) + require.NoError(t, err) + } + } + + return NewVulnerabilityProvider(w) +} diff --git a/grype/db/v6/vulnerability_provider_test.go b/grype/db/v6/vulnerability_provider_test.go new file mode 100644 index 000000000000..2a928a640d77 --- /dev/null +++ b/grype/db/v6/vulnerability_provider_test.go @@ -0,0 +1,255 @@ +package v6 + +import ( + "testing" + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/anchore/grype/grype/distro" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/pkg/qualifier" + "github.com/anchore/grype/grype/search" + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/syft/syft/cpe" +) + +func Test_FindVulnerabilitiesByDistro(t *testing.T) { + provider := testVulnerabilityProvider(t) + + d, err := distro.New(distro.Debian, "8", "") + require.NoError(t, err) + + p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "neutron", + } + + actual, err := provider.FindVulnerabilities(search.ByDistro(*d), search.ByPackageName(p.Name)) + require.NoError(t, err) + + expected := []vulnerability.Vulnerability{ + { + PackageName: "neutron", + Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat), + Reference: vulnerability.Reference{ + ID: "CVE-2014-fake-1", + Namespace: "debian:distro:debian:8", + }, + PackageQualifiers: []qualifier.Qualifier{}, + CPEs: nil, + Advisories: []vulnerability.Advisory{}, + }, + { + PackageName: "neutron", + Constraint: version.MustGetConstraint("< 2013.0.2-1", version.DebFormat), + Reference: vulnerability.Reference{ + ID: "CVE-2013-fake-2", + Namespace: "debian:distro:debian:8", + }, + PackageQualifiers: []qualifier.Qualifier{}, + CPEs: nil, + Advisories: []vulnerability.Advisory{}, + }, + } + + require.Len(t, actual, len(expected)) + + for idx, vuln := range actual { + if d := cmp.Diff(expected[idx], vuln, cmpOpts()...); d != "" { + t.Errorf("diff: %+v", d) + } + } +} + +func Test_FindVulnerabilitiesByEmptyDistro(t *testing.T) { + provider := testVulnerabilityProvider(t) + + p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "neutron", + } + + vulnerabilities, err := provider.FindVulnerabilities(search.ByDistro(distro.Distro{}), search.ByPackageName(p.Name)) + + require.Empty(t, vulnerabilities) + require.NoError(t, err) +} + +func Test_FindVulnerabilitiesByCPE(t *testing.T) { + + tests := []struct { + name string + cpe cpe.CPE + expected []vulnerability.Vulnerability + err bool + }{ + { + name: "match from name and target SW", + cpe: cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*", ""), + expected: []vulnerability.Vulnerability{ + { + PackageName: "activerecord", + Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat), + Reference: vulnerability.Reference{ + ID: "CVE-2014-fake-4", + Namespace: "nvd:cpe", + }, + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""), + }, + PackageQualifiers: []qualifier.Qualifier{}, + Advisories: []vulnerability.Advisory{}, + }, + }, + }, + { + name: "match with normalization", + cpe: cpe.Must("cpe:2.3:*:ActiVERecord:ACTiveRecord:*:*:*:*:*:ruby:*:*", ""), + expected: []vulnerability.Vulnerability{ + { + PackageName: "activerecord", + Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat), + Reference: vulnerability.Reference{ + ID: "CVE-2014-fake-4", + Namespace: "nvd:cpe", + }, + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""), + }, + PackageQualifiers: []qualifier.Qualifier{}, + Advisories: []vulnerability.Advisory{}, + }, + }, + }, + { + name: "match from vendor & name", + cpe: cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:*:*:*", ""), + expected: []vulnerability.Vulnerability{ + { + PackageName: "activerecord", + Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat), + Reference: vulnerability.Reference{ + ID: "CVE-2014-fake-3", + Namespace: "nvd:cpe", + }, + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", ""), + }, + PackageQualifiers: []qualifier.Qualifier{}, + Advisories: []vulnerability.Advisory{}, + }, + { + PackageName: "activerecord", + Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat), + Reference: vulnerability.Reference{ + ID: "CVE-2014-fake-4", + Namespace: "nvd:cpe", + }, + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""), + }, + PackageQualifiers: []qualifier.Qualifier{}, + Advisories: []vulnerability.Advisory{}, + }, + }, + }, + + { + name: "dont allow any name", + cpe: cpe.Must("cpe:2.3:*:couldntgetthisrightcouldyou:*:*:*:*:*:*:*:*:*", ""), + err: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + provider := testVulnerabilityProvider(t) + + actual, err := provider.FindVulnerabilities(search.ByCPE(test.cpe)) + if err != nil && !test.err { + t.Fatalf("expected no err, got: %+v", err) + } else if err == nil && test.err { + t.Fatalf("expected an err, got none") + } + + require.Len(t, actual, len(test.expected)) + + for idx, vuln := range actual { + if d := cmp.Diff(test.expected[idx], vuln, cmpOpts()...); d != "" { + t.Errorf("diff: %+v", d) + } + } + }) + } + +} + +func Test_FindVulnerabilitiesByByID(t *testing.T) { + provider := testVulnerabilityProvider(t) + + d, err := distro.New(distro.Debian, "8", "") + require.NoError(t, err) + + // with distro + actual, err := provider.FindVulnerabilities(search.ByDistro(*d), search.ByID("CVE-2014-fake-1")) + require.NoError(t, err) + + expected := []vulnerability.Vulnerability{ + { + Reference: vulnerability.Reference{ + ID: "CVE-2014-fake-1", + Namespace: "debian:distro:debian:8", + }, + PackageName: "neutron", + Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat), + PackageQualifiers: []qualifier.Qualifier{}, + CPEs: nil, + Advisories: []vulnerability.Advisory{}, + }, + } + + require.Len(t, actual, len(expected)) + + for idx, vuln := range actual { + if d := cmp.Diff(expected[idx], vuln, cmpOpts()...); d != "" { + t.Errorf("diff: %+v", d) + } + } + + // without distro + actual, err = provider.FindVulnerabilities(search.ByID("CVE-2014-fake-1")) + require.NoError(t, err) + + for idx, vuln := range actual { + if d := cmp.Diff(expected[idx], vuln, cmpOpts()...); d != "" { + t.Errorf("diff: %+v", d) + } + } + + // prove we survive a bad request + actual, err = provider.FindVulnerabilities(search.ByDistro(*d), search.ByID("CVE-2014-fake-3")) + require.NoError(t, err) + require.Empty(t, actual) +} + +func cmpOpts() []cmp.Option { + return []cmp.Option{ + // globally ignore unexported -- these are unexported structs we cannot reference here to use cmpopts.IgnoreUnexported + cmp.FilterPath(func(p cmp.Path) bool { + sf, ok := p.Index(-1).(cmp.StructField) + if !ok { + return false + } + r, _ := utf8.DecodeRuneInString(sf.Name()) + return !unicode.IsUpper(r) + }, cmp.Ignore()), + cmpopts.EquateEmpty(), + cmpopts.IgnoreFields(vulnerability.Reference{}, "Internal"), + } +} diff --git a/grype/db/v6/vulnerability_test.go b/grype/db/v6/vulnerability_test.go new file mode 100644 index 000000000000..c5322278cc2d --- /dev/null +++ b/grype/db/v6/vulnerability_test.go @@ -0,0 +1,418 @@ +package v6 + +import ( + "strings" + "testing" + "unicode" + + "github.com/stretchr/testify/assert" +) + +func TestV5Namespace(t *testing.T) { + // provider input should be derived from the Providers table: + // +------------+---------+---------------+----------------------------------+------------------------+ + // | id | version | processor | date_captured | input_digest | + // +------------+---------+---------------+----------------------------------+------------------------+ + // | nvd | 2 | vunnel@0.29.0 | 2025-01-08 01:32:55.179881+00:00 | xxh64:0a160d2b53dd0208 | + // | alpine | 1 | vunnel@0.29.0 | 2025-01-08 01:31:28.824872+00:00 | xxh64:30c5b7b8efa0c087 | + // | amazon | 1 | vunnel@0.29.0 | 2025-01-08 01:31:28.837469+00:00 | xxh64:7d90b3fa66b183bc | + // | chainguard | 1 | vunnel@0.29.0 | 2025-01-08 01:31:26.969865+00:00 | xxh64:25a82fa97ac9e077 | + // | debian | 1 | vunnel@0.29.0 | 2025-01-08 01:31:50.718966+00:00 | xxh64:4b1834b9e4e68987 | + // | github | 1 | vunnel@0.29.0 | 2025-01-08 01:31:27.450124+00:00 | xxh64:a3ee6b48d37a0124 | + // | mariner | 1 | vunnel@0.29.0 | 2025-01-08 01:32:35.005761+00:00 | xxh64:cb4f5861a1fda0af | + // | oracle | 1 | vunnel@0.29.0 | 2025-01-08 01:32:33.696274+00:00 | xxh64:72c0a15731e96ab3 | + // | rhel | 1 | vunnel@0.29.0 | 2025-01-08 01:32:32.192345+00:00 | xxh64:abf5d2fd5a26c194 | + // | sles | 1 | vunnel@0.29.0 | 2025-01-08 01:32:42.988937+00:00 | xxh64:8f558f8f28a04489 | + // | ubuntu | 3 | vunnel@0.29.0 | 2025-01-08 01:33:25.795537+00:00 | xxh64:97ef8421c0093620 | + // | wolfi | 1 | vunnel@0.29.0 | 2025-01-08 01:32:58.571417+00:00 | xxh64:f294f3474d35b1a9 | + // +------------+---------+---------------+----------------------------------+------------------------+ + + // the expected results should mimic what is found as v5 namespace values: + // +--------------------------------------+ + // | namespace | + // +--------------------------------------+ + // | nvd:cpe | + // | github:language:javascript | + // | ubuntu:distro:ubuntu:14.04 | + // | ubuntu:distro:ubuntu:16.04 | + // | ubuntu:distro:ubuntu:18.04 | + // | ubuntu:distro:ubuntu:20.04 | + // | ubuntu:distro:ubuntu:22.04 | + // | ubuntu:distro:ubuntu:22.10 | + // | ubuntu:distro:ubuntu:23.04 | + // | ubuntu:distro:ubuntu:23.10 | + // | ubuntu:distro:ubuntu:24.10 | + // | debian:distro:debian:8 | + // | debian:distro:debian:9 | + // | ubuntu:distro:ubuntu:12.04 | + // | ubuntu:distro:ubuntu:15.04 | + // | sles:distro:sles:15 | + // | sles:distro:sles:15.1 | + // | sles:distro:sles:15.2 | + // | sles:distro:sles:15.3 | + // | sles:distro:sles:15.4 | + // | sles:distro:sles:15.5 | + // | sles:distro:sles:15.6 | + // | amazon:distro:amazonlinux:2 | + // | debian:distro:debian:10 | + // | debian:distro:debian:11 | + // | debian:distro:debian:12 | + // | debian:distro:debian:unstable | + // | oracle:distro:oraclelinux:6 | + // | oracle:distro:oraclelinux:7 | + // | oracle:distro:oraclelinux:8 | + // | oracle:distro:oraclelinux:9 | + // | redhat:distro:redhat:6 | + // | redhat:distro:redhat:7 | + // | redhat:distro:redhat:8 | + // | redhat:distro:redhat:9 | + // | ubuntu:distro:ubuntu:12.10 | + // | ubuntu:distro:ubuntu:13.04 | + // | ubuntu:distro:ubuntu:14.10 | + // | ubuntu:distro:ubuntu:15.10 | + // | ubuntu:distro:ubuntu:16.10 | + // | ubuntu:distro:ubuntu:17.04 | + // | ubuntu:distro:ubuntu:17.10 | + // | ubuntu:distro:ubuntu:18.10 | + // | ubuntu:distro:ubuntu:19.04 | + // | ubuntu:distro:ubuntu:19.10 | + // | ubuntu:distro:ubuntu:20.10 | + // | ubuntu:distro:ubuntu:21.04 | + // | ubuntu:distro:ubuntu:21.10 | + // | ubuntu:distro:ubuntu:24.04 | + // | github:language:php | + // | debian:distro:debian:13 | + // | debian:distro:debian:7 | + // | redhat:distro:redhat:5 | + // | sles:distro:sles:11.1 | + // | sles:distro:sles:11.3 | + // | sles:distro:sles:11.4 | + // | sles:distro:sles:11.2 | + // | sles:distro:sles:12 | + // | sles:distro:sles:12.1 | + // | sles:distro:sles:12.2 | + // | sles:distro:sles:12.3 | + // | sles:distro:sles:12.4 | + // | sles:distro:sles:12.5 | + // | chainguard:distro:chainguard:rolling | + // | wolfi:distro:wolfi:rolling | + // | github:language:go | + // | alpine:distro:alpine:3.20 | + // | alpine:distro:alpine:3.21 | + // | alpine:distro:alpine:edge | + // | github:language:rust | + // | github:language:python | + // | sles:distro:sles:11 | + // | oracle:distro:oraclelinux:5 | + // | github:language:ruby | + // | github:language:dotnet | + // | alpine:distro:alpine:3.12 | + // | alpine:distro:alpine:3.13 | + // | alpine:distro:alpine:3.14 | + // | alpine:distro:alpine:3.15 | + // | alpine:distro:alpine:3.16 | + // | alpine:distro:alpine:3.17 | + // | alpine:distro:alpine:3.18 | + // | alpine:distro:alpine:3.19 | + // | mariner:distro:mariner:2.0 | + // | github:language:java | + // | github:language:dart | + // | amazon:distro:amazonlinux:2023 | + // | alpine:distro:alpine:3.10 | + // | alpine:distro:alpine:3.11 | + // | alpine:distro:alpine:3.4 | + // | alpine:distro:alpine:3.5 | + // | alpine:distro:alpine:3.7 | + // | alpine:distro:alpine:3.8 | + // | alpine:distro:alpine:3.9 | + // | mariner:distro:azurelinux:3.0 | + // | mariner:distro:mariner:1.0 | + // | alpine:distro:alpine:3.3 | + // | alpine:distro:alpine:3.6 | + // | amazon:distro:amazonlinux:2022 | + // | alpine:distro:alpine:3.2 | + // | github:language:swift | + // +--------------------------------------+ + + type testCase struct { + name string + provider string // from Providers.id + ecosystem string // only used when provider is "github" + osName string // only used for OS-based providers + osVersion string // only used for OS-based providers + expected string + } + + tests := []testCase{ + // NVD + { + name: "nvd provider", + provider: "nvd", + expected: "nvd:cpe", + }, + + // GitHub ecosystem tests + { + name: "github golang direct", + provider: "github", + ecosystem: "golang", + expected: "github:language:go", + }, + { + name: "github go-module ecosystem", + provider: "github", + ecosystem: "go-module", + expected: "github:language:go", + }, + { + name: "github composer ecosystem", + provider: "github", + ecosystem: "composer", + expected: "github:language:php", + }, + { + name: "github php-composer ecosystem", + provider: "github", + ecosystem: "php-composer", + expected: "github:language:php", + }, + { + name: "github cargo ecosystem", + provider: "github", + ecosystem: "cargo", + expected: "github:language:rust", + }, + { + name: "github rust-crate ecosystem", + provider: "github", + ecosystem: "rust-crate", + expected: "github:language:rust", + }, + { + name: "github pub ecosystem", + provider: "github", + ecosystem: "pub", + expected: "github:language:dart", + }, + { + name: "github dart-pub ecosystem", + provider: "github", + ecosystem: "dart-pub", + expected: "github:language:dart", + }, + { + name: "github nuget ecosystem", + provider: "github", + ecosystem: "nuget", + expected: "github:language:dotnet", + }, + { + name: "github maven ecosystem", + provider: "github", + ecosystem: "maven", + expected: "github:language:java", + }, + { + name: "github swifturl ecosystem", + provider: "github", + ecosystem: "swifturl", + expected: "github:language:swift", + }, + { + name: "github npm ecosystem", + provider: "github", + ecosystem: "npm", + expected: "github:language:javascript", + }, + { + name: "github node ecosystem", + provider: "github", + ecosystem: "node", + expected: "github:language:javascript", + }, + { + name: "github pypi ecosystem", + provider: "github", + ecosystem: "pypi", + expected: "github:language:python", + }, + { + name: "github pip ecosystem", + provider: "github", + ecosystem: "pip", + expected: "github:language:python", + }, + { + name: "github rubygems ecosystem", + provider: "github", + ecosystem: "rubygems", + expected: "github:language:ruby", + }, + { + name: "github gem ecosystem", + provider: "github", + ecosystem: "gem", + expected: "github:language:ruby", + }, + + // OS Distribution tests + { + name: "ubuntu distribution", + provider: "ubuntu", + osName: "ubuntu", + osVersion: "22.04", + expected: "ubuntu:distro:ubuntu:22.04", + }, + { + name: "redhat distribution", + provider: "rhel", + osName: "redhat", + osVersion: "8", + expected: "redhat:distro:redhat:8", + }, + { + name: "debian distribution", + provider: "debian", + osName: "debian", + osVersion: "11", + expected: "debian:distro:debian:11", + }, + { + name: "sles distribution", + provider: "sles", + osName: "sles", + osVersion: "15.5", + expected: "sles:distro:sles:15.5", + }, + { + name: "alpine distribution", + provider: "alpine", + osName: "alpine", + osVersion: "3.18", + expected: "alpine:distro:alpine:3.18", + }, + { + name: "chainguard distribution", + provider: "chainguard", + osName: "chainguard", + osVersion: "rolling", + expected: "chainguard:distro:chainguard:rolling", + }, + { + name: "wolfi distribution", + provider: "wolfi", + osName: "wolfi", + osVersion: "rolling", + expected: "wolfi:distro:wolfi:rolling", + }, + { + name: "amazon linux distribution", + provider: "amazon", + osName: "amazon", + osVersion: "2023", + expected: "amazon:distro:amazonlinux:2023", + }, + { + name: "mariner regular version", + provider: "mariner", + osName: "mariner", + osVersion: "2.0", + expected: "mariner:distro:mariner:2.0", + }, + { + name: "mariner azure version", + provider: "mariner", + osName: "mariner", + osVersion: "3.0", + expected: "mariner:distro:azurelinux:3.0", + }, + { + name: "oracle linux distribution", + provider: "oracle", + osName: "oracle", + osVersion: "8", + expected: "oracle:distro:oraclelinux:8", + }, + + // Version truncation tests + { + name: "rhel with minor version", + provider: "rhel", + osName: "redhat", + osVersion: "8.6", + expected: "redhat:distro:redhat:8", + }, + { + name: "rhel with patch version", + provider: "rhel", + osName: "redhat", + osVersion: "9.2.1", + expected: "redhat:distro:redhat:9", + }, + { + name: "oracle with minor version", + provider: "oracle", + osName: "oracle", + osVersion: "8.7", + expected: "oracle:distro:oraclelinux:8", + }, + { + name: "oracle with patch version", + provider: "oracle", + osName: "oracle", + osVersion: "9.3.1", + expected: "oracle:distro:oraclelinux:9", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vuln := &VulnerabilityHandle{ + Provider: &Provider{ + ID: tt.provider, + }, + } + pkg := &AffectedPackageHandle{} + + if tt.osName != "" { + major, minor, _ := majorMinorPatch(tt.osVersion) + var label string + if major == "" { + label = tt.osVersion + } + pkg.OperatingSystem = &OperatingSystem{ + Name: tt.osName, + MajorVersion: major, + MinorVersion: minor, + LabelVersion: label, + } + } + + if tt.provider == "github" { + pkg.Package = &Package{ + Ecosystem: tt.ecosystem, + } + } + + result := MimicV5Namespace(vuln, pkg) + assert.Equal(t, tt.expected, result) + }) + } +} + +func majorMinorPatch(ver string) (string, string, string) { + if !unicode.IsDigit(rune(ver[0])) { + return "", "", "" + } + parts := strings.Split(ver, ".") + if len(parts) == 0 { + return "", "", "" + } + if len(parts) == 1 { + return parts[0], "", "" + } + if len(parts) == 2 { + return parts[0], parts[1], "" + } + return parts[0], parts[1], parts[2] +} diff --git a/grype/deprecated.go b/grype/deprecated.go index b96a2bdbde2f..fc8ec72b29f7 100644 --- a/grype/deprecated.go +++ b/grype/deprecated.go @@ -1,10 +1,10 @@ package grype import ( - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/matcher" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/syft/syft" @@ -13,7 +13,7 @@ import ( ) // TODO: deprecated, will remove before v1.0.0 -func FindVulnerabilities(store v5.ProviderStore, userImageStr string, scopeOpt source.Scope, registryOptions *image.RegistryOptions) (match.Matches, pkg.Context, []pkg.Package, error) { +func FindVulnerabilities(store vulnerability.Provider, userImageStr string, scopeOpt source.Scope, registryOptions *image.RegistryOptions) (match.Matches, pkg.Context, []pkg.Package, error) { providerConfig := pkg.ProviderConfig{ SyftProviderConfig: pkg.SyftProviderConfig{ RegistryOptions: registryOptions, @@ -33,11 +33,13 @@ func FindVulnerabilities(store v5.ProviderStore, userImageStr string, scopeOpt s } // TODO: deprecated, will remove before v1.0.0 -func FindVulnerabilitiesForPackage(store v5.ProviderStore, d *linux.Release, matchers []matcher.Matcher, packages []pkg.Package) match.Matches { +func FindVulnerabilitiesForPackage(store vulnerability.Provider, d *linux.Release, matchers []match.Matcher, packages []pkg.Package) match.Matches { + exclusionProvider, _ := store.(match.ExclusionProvider) // TODO v5 is an exclusion provider, but v6 is not runner := VulnerabilityMatcher{ - Store: store, - Matchers: matchers, - NormalizeByCVE: false, + VulnerabilityProvider: store, + ExclusionProvider: exclusionProvider, + Matchers: matchers, + NormalizeByCVE: false, } actualResults, _, err := runner.FindMatches(packages, pkg.Context{ diff --git a/grype/distro/distro.go b/grype/distro/distro.go index a79bb866f9c0..17ac8dbb0545 100644 --- a/grype/distro/distro.go +++ b/grype/distro/distro.go @@ -80,6 +80,22 @@ func (d Distro) MajorVersion() string { return fmt.Sprintf("%d", d.Version.Segments()[0]) } +// MinorVersion returns the minor version value from the pseudo-semantically versioned distro version value. +func (d Distro) MinorVersion() string { + if d.Version == nil { + parts := strings.Split(d.RawVersion, ".") + if len(parts) > 1 { + return parts[1] + } + return "" + } + parts := d.Version.Segments() + if len(parts) > 1 { + return fmt.Sprintf("%d", parts[1]) + } + return "" +} + // FullVersion returns the original user version value. func (d Distro) FullVersion() string { return d.RawVersion diff --git a/grype/load_vulnerability_db.go b/grype/load_vulnerability_db.go index fdbddd9502f1..a754e53e28d6 100644 --- a/grype/load_vulnerability_db.go +++ b/grype/load_vulnerability_db.go @@ -1,13 +1,55 @@ package grype import ( - "github.com/anchore/grype/grype/db/legacy/distribution" + "fmt" + + v5dist "github.com/anchore/grype/grype/db/legacy/distribution" v5 "github.com/anchore/grype/grype/db/v5" + v6 "github.com/anchore/grype/grype/db/v6" + v6dist "github.com/anchore/grype/grype/db/v6/distribution" + v6inst "github.com/anchore/grype/grype/db/v6/installation" + "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) -func LoadVulnerabilityDB(cfg distribution.Config, update bool) (*v5.ProviderStore, *distribution.Status, error) { - dbCurator, err := distribution.NewCurator(cfg) +func LoadVulnerabilityDBv6(distCfg v6dist.Config, installCfg v6inst.Config, update bool) (vulnerability.Provider, *v5dist.Status, error) { + client, err := v6dist.NewClient(distCfg) + if err != nil { + return nil, nil, fmt.Errorf("unable to create distribution client: %w", err) + } + c, err := v6inst.NewCurator(installCfg, client) + if err != nil { + return nil, nil, fmt.Errorf("unable to create curator: %w", err) + } + + if update { + _, err := c.Update() + if err != nil { + return nil, nil, fmt.Errorf("unable to update db: %w", err) + } + } + + rdr, err := c.Reader() + if err != nil { + return nil, nil, fmt.Errorf("unable to create db reader: %w", err) + } + + s := c.Status() + status := v5dist.Status{ + Built: s.Built.Time, + SchemaVersion: 6, // FIXME: should be s.SchemaVersion, not an int + Location: s.Path, + Checksum: s.Checksum, + Err: s.Err, + } + + v6store := v6.NewVulnerabilityProvider(rdr) + + return v6store, &status, nil +} + +func LoadVulnerabilityDB(cfg v5dist.Config, update bool) (*v5.ProviderStore, *v5dist.Status, error) { + dbCurator, err := v5dist.NewCurator(cfg) if err != nil { return nil, nil, err } @@ -33,10 +75,8 @@ func LoadVulnerabilityDB(cfg distribution.Config, update bool) (*v5.ProviderStor } s := &v5.ProviderStore{ - VulnerabilityProvider: p, - VulnerabilityMetadataProvider: v5.NewVulnerabilityMetadataProvider(storeReader), - ExclusionProvider: v5.NewMatchExclusionProvider(storeReader), - Closer: storeReader, + Provider: p, + ExclusionProvider: v5.NewMatchExclusionProvider(storeReader), } return s, &status, nil diff --git a/grype/match/explicit_ignores.go b/grype/match/explicit_ignores.go index 0ec272b1a90f..bdbd1d1e5bce 100644 --- a/grype/match/explicit_ignores.go +++ b/grype/match/explicit_ignores.go @@ -70,11 +70,15 @@ func init() { // ApplyExplicitIgnoreRules Filters out matches meeting the criteria defined above and those within the grype database func ApplyExplicitIgnoreRules(provider ExclusionProvider, matches Matches) (Matches, []IgnoredMatch) { + if provider == nil { + return matches, nil + } + var ignoreRules []IgnoreRule ignoreRules = append(ignoreRules, explicitIgnoreRules...) for _, m := range matches.Sorted() { - r, err := provider.GetRules(m.Vulnerability.ID) + r, err := provider.IgnoreRules(m.Vulnerability.ID) if err != nil { log.Warnf("unable to get ignore rules for vuln id=%s", m.Vulnerability.ID) diff --git a/grype/match/explicit_ignores_test.go b/grype/match/explicit_ignores_test.go index abdc9c9dd64a..c699f22ad9ad 100644 --- a/grype/match/explicit_ignores_test.go +++ b/grype/match/explicit_ignores_test.go @@ -25,7 +25,7 @@ func newMockExclusionProvider() *mockExclusionProvider { func (d *mockExclusionProvider) stub() { } -func (d *mockExclusionProvider) GetRules(vulnerabilityID string) ([]IgnoreRule, error) { +func (d *mockExclusionProvider) IgnoreRules(vulnerabilityID string) ([]IgnoreRule, error) { return d.data[vulnerabilityID], nil } diff --git a/grype/match/ignore.go b/grype/match/ignore.go index 1899d5507a77..f70d3ba226ef 100644 --- a/grype/match/ignore.go +++ b/grype/match/ignore.go @@ -8,6 +8,12 @@ import ( "github.com/anchore/grype/internal/log" ) +// IgnoreFilter implementations are used to filter matches, returning all applicable IgnoreRule(s) that applied, +// these could include an IgnoreRule with only a Reason value filled in for synthetically generated rules +type IgnoreFilter interface { + IgnoreMatch(match Match) []IgnoreRule +} + // An IgnoredMatch is a vulnerability Match that has been ignored because one or more IgnoreRules applied to the match. type IgnoredMatch struct { Match @@ -48,16 +54,21 @@ type IgnoreRulePackage struct { // ApplyIgnoreRules returns two collections: the matches that are not being // ignored, and the matches that are being ignored. func ApplyIgnoreRules(matches Matches, rules []IgnoreRule) (Matches, []IgnoredMatch) { + matched, ignored := ApplyIgnoreFilters(matches.Sorted(), rules...) + return NewMatches(matched...), ignored +} + +// ApplyIgnoreFilters applies all the IgnoreFilter(s) to the provided set of matches, +// splitting the results into a set of matched matches and ignored matches +func ApplyIgnoreFilters[T IgnoreFilter](matches []Match, filters ...T) ([]Match, []IgnoredMatch) { + var out []Match var ignoredMatches []IgnoredMatch - remainingMatches := NewMatches() - for _, match := range matches.Sorted() { + for _, match := range matches { var applicableRules []IgnoreRule - for _, rule := range rules { - if shouldIgnore(match, rule) { - applicableRules = append(applicableRules, rule) - } + for _, filter := range filters { + applicableRules = append(applicableRules, filter.IgnoreMatch(match)...) } if len(applicableRules) > 0 { @@ -69,41 +80,56 @@ func ApplyIgnoreRules(matches Matches, rules []IgnoreRule) (Matches, []IgnoredMa continue } - remainingMatches.Add(match) + out = append(out, match) } - return remainingMatches, ignoredMatches + return out, ignoredMatches } -func shouldIgnore(match Match, rule IgnoreRule) bool { +func (r IgnoreRule) IgnoreMatch(match Match) []IgnoreRule { // VEX rules are handled by the vex processor - if rule.VexStatus != "" { - return false + if r.VexStatus != "" { + return nil } - ignoreConditions := getIgnoreConditionsForRule(rule) + ignoreConditions := getIgnoreConditionsForRule(r) if len(ignoreConditions) == 0 { // this rule specifies no criteria, so it doesn't apply to the Match - return false + return nil } for _, condition := range ignoreConditions { if !condition(match) { // as soon as one rule criterion doesn't apply, we know this rule doesn't apply to the Match - return false + return nil } } // all criteria specified in the rule apply to this Match - return true + return []IgnoreRule{r} } // HasConditions returns true if the ignore rule has conditions // that can cause a match to be ignored -func (ir IgnoreRule) HasConditions() bool { - return len(getIgnoreConditionsForRule(ir)) == 0 +func (r IgnoreRule) HasConditions() bool { + return len(getIgnoreConditionsForRule(r)) == 0 } +// ignoreFilters implements match.IgnoreFilter on a slice of objects that implement the same interface +type ignoreFilters[T IgnoreFilter] []T + +func (r ignoreFilters[T]) IgnoreMatch(match Match) []IgnoreRule { + for _, rule := range r { + ignores := rule.IgnoreMatch(match) + if len(ignores) > 0 { + return ignores + } + } + return nil +} + +var _ IgnoreFilter = (*ignoreFilters[IgnoreRule])(nil) + // An ignoreCondition is a function that returns a boolean indicating whether // the given Match should be ignored. type ignoreCondition func(match Match) bool diff --git a/grype/match/ignore_test.go b/grype/match/ignore_test.go index 414f60751e09..c4e92114b722 100644 --- a/grype/match/ignore_test.go +++ b/grype/match/ignore_test.go @@ -902,7 +902,7 @@ func TestShouldIgnore(t *testing.T) { for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { - actual := shouldIgnore(testCase.match, testCase.rule) + actual := len(testCase.rule.IgnoreMatch(testCase.match)) > 0 assert.Equal(t, testCase.expected, actual) }) } diff --git a/grype/match/matcher.go b/grype/match/matcher.go new file mode 100644 index 000000000000..1e8e387faf6b --- /dev/null +++ b/grype/match/matcher.go @@ -0,0 +1,18 @@ +package match + +import ( + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +// Matcher is the interface to implement to provide top-level package-to-match +type Matcher interface { + PackageTypes() []syftPkg.Type + + Type() MatcherType + + // Match is called for every package found, returning any matches and an optional Ignorer which will be applied + // after all matches are found + Match(vp vulnerability.Provider, p pkg.Package) ([]Match, []IgnoredMatch, error) +} diff --git a/grype/match/provider.go b/grype/match/provider.go index 10b124aac6ee..8b6c3b699145 100644 --- a/grype/match/provider.go +++ b/grype/match/provider.go @@ -1,5 +1,5 @@ package match type ExclusionProvider interface { - GetRules(vulnerabilityID string) ([]IgnoreRule, error) + IgnoreRules(vulnerabilityID string) ([]IgnoreRule, error) } diff --git a/grype/match/results.go b/grype/match/results.go new file mode 100644 index 000000000000..f98dbee5757f --- /dev/null +++ b/grype/match/results.go @@ -0,0 +1,56 @@ +package match + +import ( + "fmt" + "sort" + + "github.com/scylladb/go-set/strset" +) + +type CPEPackageParameter struct { + Name string `json:"name"` + Version string `json:"version"` +} + +type CPEParameters struct { + Namespace string `json:"namespace"` + CPEs []string `json:"cpes"` + Package CPEPackageParameter `json:"package"` +} + +func (i *CPEParameters) Merge(other CPEParameters) error { + if i.Namespace != other.Namespace { + return fmt.Errorf("namespaces do not match") + } + + existingCPEs := strset.New(i.CPEs...) + newCPEs := strset.New(other.CPEs...) + mergedCPEs := strset.Union(existingCPEs, newCPEs).List() + sort.Strings(mergedCPEs) + i.CPEs = mergedCPEs + return nil +} + +type CPEResult struct { + VulnerabilityID string `json:"vulnerabilityID"` + VersionConstraint string `json:"versionConstraint"` + CPEs []string `json:"cpes"` +} + +func (h CPEResult) Equals(other CPEResult) bool { + if h.VersionConstraint != other.VersionConstraint { + return false + } + + if len(h.CPEs) != len(other.CPEs) { + return false + } + + for i := range h.CPEs { + if h.CPEs[i] != other.CPEs[i] { + return false + } + } + + return true +} diff --git a/grype/db/v5/matcher/apk/matcher.go b/grype/matcher/apk/matcher.go similarity index 57% rename from grype/db/v5/matcher/apk/matcher.go rename to grype/matcher/apk/matcher.go index bb6e4f412ba3..898c5cd62e59 100644 --- a/grype/db/v5/matcher/apk/matcher.go +++ b/grype/matcher/apk/matcher.go @@ -4,11 +4,11 @@ import ( "errors" "fmt" - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/search" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" @@ -26,45 +26,56 @@ func (m *Matcher) Type() match.MatcherType { return match.ApkMatcher } -func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { - var matches = make([]match.Match, 0) +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + var matches []match.Match // direct matches with package itself - directMatches, err := m.findMatchesForPackage(store, d, p) + directMatches, err := m.findMatchesForPackage(store, p) if err != nil { - return nil, err + return nil, nil, err } matches = append(matches, directMatches...) // indirect matches, via package's origin package - indirectMatches, err := m.findMatchesForOriginPackage(store, d, p) + indirectMatches, err := m.findMatchesForOriginPackage(store, p) if err != nil { - return nil, err + return nil, nil, err } matches = append(matches, indirectMatches...) - return matches, nil + // APK sources are also able to NAK vulnerabilities, so we want to return these as explicit ignores in order + // to allow rules later to use these to ignore "the same" vulnerability found in "the same" locations + naks, err := m.findNaksForPackage(store, p) + + return matches, naks, err } -//nolint:funlen -func (m *Matcher) cpeMatchesWithoutSecDBFixes(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { +//nolint:funlen,gocognit +func (m *Matcher) cpeMatchesWithoutSecDBFixes(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) { // find CPE-indexed vulnerability matches specific to the given package name and version - cpeMatches, err := search.ByPackageCPE(store, d, p, m.Type()) + cpeMatches, err := internal.MatchPackageByCPEs(store, p, m.Type()) if err != nil { - return nil, err + log.WithFields("package", p.Name, "error", err).Debug("failed to find CPE matches for package") + } + if p.Distro == nil { + return cpeMatches, nil } cpeMatchesByID := matchesByID(cpeMatches) // remove cpe matches where there is an entry in the secDB for the particular package-vulnerability pairing, and the // installed package version is >= the fixed in version for the secDB record. - secDBVulnerabilities, err := store.GetByDistro(d, p) + secDBVulnerabilities, err := store.FindVulnerabilities( + search.ByPackageName(p.Name), + search.ByDistro(*p.Distro)) if err != nil { return nil, err } for _, upstreamPkg := range pkg.UpstreamPackages(p) { - secDBVulnerabilitiesForUpstream, err := store.GetByDistro(d, upstreamPkg) + secDBVulnerabilitiesForUpstream, err := store.FindVulnerabilities( + search.ByPackageName(upstreamPkg.Name), + search.ByDistro(*upstreamPkg.Distro)) if err != nil { return nil, err } @@ -158,16 +169,16 @@ func vulnerabilitiesByID(vulns []vulnerability.Vulnerability) map[string][]vulne return results } -func (m *Matcher) findMatchesForPackage(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { +func (m *Matcher) findMatchesForPackage(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) { // find SecDB matches for the given package name and version - secDBMatches, err := search.ByPackageDistro(store, d, p, m.Type()) + secDBMatches, _, err := internal.MatchPackageByDistro(store, p, m.Type()) if err != nil { return nil, err } // TODO: are there other errors that we should handle here that causes this to short circuit - cpeMatches, err := m.cpeMatchesWithoutSecDBFixes(store, d, p) - if err != nil && !errors.Is(err, search.ErrEmptyCPEMatch) { + cpeMatches, err := m.cpeMatchesWithoutSecDBFixes(store, p) + if err != nil && !errors.Is(err, internal.ErrEmptyCPEMatch) { return nil, err } @@ -182,11 +193,11 @@ func (m *Matcher) findMatchesForPackage(store v5.VulnerabilityProvider, d *distr return matches, nil } -func (m *Matcher) findMatchesForOriginPackage(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { +func (m *Matcher) findMatchesForOriginPackage(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) { var matches []match.Match for _, indirectPackage := range pkg.UpstreamPackages(p) { - indirectMatches, err := m.findMatchesForPackage(store, d, indirectPackage) + indirectMatches, err := m.findMatchesForPackage(store, indirectPackage) if err != nil { return nil, fmt.Errorf("failed to find vulnerabilities for apk upstream source package: %w", err) } @@ -199,3 +210,68 @@ func (m *Matcher) findMatchesForOriginPackage(store v5.VulnerabilityProvider, d return matches, nil } + +// NAK entries are those reported as explicitly not vulnerable by the upstream provider, +// for example this entry is present in the v5 database: +// 312891,CVE-2020-7224,openvpn,alpine:distro:alpine:3.10,,< 0,apk,,"[{""id"":""CVE-2020-7224"",""namespace"":""nvd:cpe""}]","[""0""]",fixed, +// which indicates, for the alpine:3.10 distro, package openvpn is not vulnerable to CVE-2020-7224 +// we want to report these NAK entries as match.IgnoredMatch, to allow for later processing to create ignore rules +// based on packages which overlap by location, such as a python binary found in addition to the python APK entry -- +// we want to NAK this vulnerability for BOTH packages +func (m *Matcher) findNaksForPackage(store vulnerability.Provider, p pkg.Package) ([]match.IgnoredMatch, error) { + // TODO: this was only applying to specific distros as originally implemented; this should probably be removed: + if d := p.Distro; d == nil || d.Type != distro.Wolfi && d.Type != distro.Chainguard && d.Type != distro.Alpine { + return nil, nil + } + + // get all the direct naks + naks, err := store.FindVulnerabilities( + search.ByDistro(*p.Distro), + search.ByPackageName(p.Name), + nakConstraint, + ) + if err != nil { + return nil, err + } + + // append all the upstream naks + for _, upstreamPkg := range pkg.UpstreamPackages(p) { + upstreamNaks, err := store.FindVulnerabilities( + search.ByDistro(*upstreamPkg.Distro), + search.ByPackageName(upstreamPkg.Name), + nakConstraint, + ) + if err != nil { + return nil, err + } + + naks = append(naks, upstreamNaks...) + } + + var ignores []match.IgnoredMatch + for _, nak := range naks { + ignores = append(ignores, match.IgnoredMatch{ + Match: match.Match{ + Vulnerability: nak, + Package: p, + Details: nil, // Probably don't need details here + }, + AppliedIgnoreRules: []match.IgnoreRule{ + { + Vulnerability: nak.ID, + Reason: "NAK", + }, + }, + }) + } + + return ignores, nil +} + +var ( + nakVersionString = version.MustGetConstraint("< 0", version.ApkFormat).String() + // nakConstraint checks the exact version string for being an APK version with "< 0" + nakConstraint = search.ByConstraintFunc(func(c version.Constraint) (bool, error) { + return c.String() == nakVersionString, nil + }) +) diff --git a/grype/db/v5/matcher/apk/matcher_test.go b/grype/matcher/apk/matcher_test.go similarity index 51% rename from grype/db/v5/matcher/apk/matcher_test.go rename to grype/matcher/apk/matcher_test.go index 25a2ac5fdf37..09da85d0775b 100644 --- a/grype/db/v5/matcher/apk/matcher_test.go +++ b/grype/matcher/apk/matcher_test.go @@ -9,65 +9,28 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/search" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/grype/vulnerability/mock" "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) -type mockStore struct { - backend map[string]map[string][]v5.Vulnerability -} - -func (s *mockStore) GetVulnerability(namespace, id string) ([]v5.Vulnerability, error) { - // TODO implement me - panic("implement me") -} - -func (s *mockStore) SearchForVulnerabilities(namespace, name string) ([]v5.Vulnerability, error) { - namespaceMap := s.backend[namespace] - if namespaceMap == nil { - return nil, nil - } - return namespaceMap[name], nil -} - -func (s *mockStore) GetAllVulnerabilities() (*[]v5.Vulnerability, error) { - return nil, nil -} - -func (s *mockStore) GetVulnerabilityNamespaces() ([]string, error) { - keys := make([]string, 0, len(s.backend)) - for k := range s.backend { - keys = append(keys, k) - } - - return keys, nil -} - func TestSecDBOnlyMatch(t *testing.T) { - - secDbVuln := v5.Vulnerability{ - // ID doesn't match - this is the key for comparison in the matcher - ID: "CVE-2020-2", - VersionConstraint: "<= 0.9.11", - VersionFormat: "apk", - Namespace: "secdb:distro:alpine:3.12", - } - store := mockStore{ - backend: map[string]map[string][]v5.Vulnerability{ - "secdb:distro:alpine:3.12": { - "libvncserver": []v5.Vulnerability{secDbVuln}, - }, + secDbVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + // ID doesn't match - this is the key for comparison in the matcher + ID: "CVE-2020-2", + Namespace: "secdb:distro:alpine:3.12", }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("<= 0.9.11", version.ApkFormat), } - provider, err := v5.NewVulnerabilityProvider(&store) - require.NoError(t, err) + vp := mock.VulnerabilityProvider(secDbVuln) m := Matcher{} d, err := distro.New(distro.Alpine, "3.12.0", "") @@ -80,18 +43,16 @@ func TestSecDBOnlyMatch(t *testing.T) { Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, + Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""), }, } - vulnFound, err := v5.NewVulnerability(secDbVuln) - assert.NoError(t, err) - expected := []match.Match{ { - Vulnerability: *vulnFound, + Vulnerability: secDbVuln, Package: p, Details: []match.Detail{ { @@ -109,7 +70,7 @@ func TestSecDBOnlyMatch(t *testing.T) { "namespace": "secdb:distro:alpine:3.12", }, Found: map[string]interface{}{ - "versionConstraint": vulnFound.Constraint.String(), + "versionConstraint": secDbVuln.Constraint.String(), "vulnerabilityID": "CVE-2020-2", }, Matcher: match.ApkMatcher, @@ -118,7 +79,7 @@ func TestSecDBOnlyMatch(t *testing.T) { }, } - actual, err := m.Match(provider, d, p) + actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) @@ -126,34 +87,29 @@ func TestSecDBOnlyMatch(t *testing.T) { func TestBothSecdbAndNvdMatches(t *testing.T) { // NVD and Alpine's secDB both have the same CVE ID for the package - nvdVuln := v5.Vulnerability{ - ID: "CVE-2020-1", - VersionConstraint: "<= 0.9.11", - VersionFormat: "unknown", - CPEs: []string{`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`}, - Namespace: "nvd:cpe", + nvdVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-1", + Namespace: "nvd:cpe", + }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("<= 0.9.11", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""), + }, } - secDbVuln := v5.Vulnerability{ - // ID *does* match - this is the key for comparison in the matcher - ID: "CVE-2020-1", - VersionConstraint: "<= 0.9.11", - VersionFormat: "apk", - Namespace: "secdb:distro:alpine:3.12", - } - store := mockStore{ - backend: map[string]map[string][]v5.Vulnerability{ - "nvd:cpe": { - "libvncserver": []v5.Vulnerability{nvdVuln}, - }, - "secdb:distro:alpine:3.12": { - "libvncserver": []v5.Vulnerability{secDbVuln}, - }, + secDbVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + // ID *does* match - this is the key for comparison in the matcher + ID: "CVE-2020-1", + Namespace: "secdb:distro:alpine:3.12", }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("<= 0.9.11", version.ApkFormat), } - provider, err := v5.NewVulnerabilityProvider(&store) - require.NoError(t, err) + vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) m := Matcher{} d, err := distro.New(distro.Alpine, "3.12.0", "") @@ -166,19 +122,16 @@ func TestBothSecdbAndNvdMatches(t *testing.T) { Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, + Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""), }, } - // ensure the SECDB record is preferred over the NVD record - vulnFound, err := v5.NewVulnerability(secDbVuln) - assert.NoError(t, err) - expected := []match.Match{ { - - Vulnerability: *vulnFound, + // ensure the SECDB record is preferred over the NVD record + Vulnerability: secDbVuln, Package: p, Details: []match.Detail{ { @@ -196,7 +149,7 @@ func TestBothSecdbAndNvdMatches(t *testing.T) { "namespace": "secdb:distro:alpine:3.12", }, Found: map[string]interface{}{ - "versionConstraint": vulnFound.Constraint.String(), + "versionConstraint": secDbVuln.Constraint.String(), "vulnerabilityID": "CVE-2020-1", }, Matcher: match.ApkMatcher, @@ -205,7 +158,7 @@ func TestBothSecdbAndNvdMatches(t *testing.T) { }, } - actual, err := m.Match(provider, d, p) + actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) @@ -213,44 +166,37 @@ func TestBothSecdbAndNvdMatches(t *testing.T) { func TestBothSecdbAndNvdMatches_DifferentFixInfo(t *testing.T) { // NVD and Alpine's secDB both have the same CVE ID for the package - nvdVuln := v5.Vulnerability{ - ID: "CVE-2020-1", - VersionConstraint: "< 1.0.0", - VersionFormat: "unknown", - CPEs: []string{`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`}, - Namespace: "nvd:cpe", - Fix: v5.Fix{ + nvdVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-1", + Namespace: "nvd:cpe", + }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("< 1.0.0", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""), + }, + Fix: vulnerability.Fix{ Versions: []string{"1.0.0"}, - State: v5.FixedState, + State: vulnerability.FixStateFixed, }, } - secDbVuln := v5.Vulnerability{ - // ID *does* match - this is the key for comparison in the matcher - ID: "CVE-2020-1", - VersionConstraint: "< 0.9.12", - VersionFormat: "apk", - Namespace: "secdb:distro:alpine:3.12", + secDbVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + // ID *does* match - this is the key for comparison in the matcher + ID: "CVE-2020-1", + Namespace: "secdb:distro:alpine:3.12", + }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("< 0.9.12", version.ApkFormat), // SecDB indicates Alpine have backported a fix to v0.9... - Fix: v5.Fix{ + Fix: vulnerability.Fix{ Versions: []string{"0.9.12"}, - State: v5.FixedState, - }, - } - store := mockStore{ - backend: map[string]map[string][]v5.Vulnerability{ - "nvd:cpe": { - "libvncserver": []v5.Vulnerability{nvdVuln}, - }, - "secdb:distro:alpine:3.12": { - "libvncserver": []v5.Vulnerability{secDbVuln}, - }, + State: vulnerability.FixStateFixed, }, } - - provider, err := v5.NewVulnerabilityProvider(&store) - require.NoError(t, err) - + vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) m := Matcher{} d, err := distro.New(distro.Alpine, "3.12.0", "") if err != nil { @@ -262,23 +208,16 @@ func TestBothSecdbAndNvdMatches_DifferentFixInfo(t *testing.T) { Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, + Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""), }, } - // ensure the SECDB record is preferred over the NVD record - vulnFound, err := v5.NewVulnerability(secDbVuln) - assert.NoError(t, err) - vulnFound.Fix = vulnerability.Fix{ - Versions: secDbVuln.Fix.Versions, - State: vulnerability.FixState(secDbVuln.Fix.State), - } - expected := []match.Match{ { - - Vulnerability: *vulnFound, + // ensure the SECDB record is preferred over the NVD record + Vulnerability: secDbVuln, Package: p, Details: []match.Detail{ { @@ -296,7 +235,7 @@ func TestBothSecdbAndNvdMatches_DifferentFixInfo(t *testing.T) { "namespace": "secdb:distro:alpine:3.12", }, Found: map[string]interface{}{ - "versionConstraint": vulnFound.Constraint.String(), + "versionConstraint": secDbVuln.Constraint.String(), "vulnerabilityID": "CVE-2020-1", }, Matcher: match.ApkMatcher, @@ -305,7 +244,7 @@ func TestBothSecdbAndNvdMatches_DifferentFixInfo(t *testing.T) { }, } - actual, err := m.Match(provider, d, p) + actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) @@ -313,35 +252,30 @@ func TestBothSecdbAndNvdMatches_DifferentFixInfo(t *testing.T) { func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) { // NVD and Alpine's secDB both have the same CVE ID for the package - nvdVuln := v5.Vulnerability{ - ID: "CVE-2020-1", - VersionConstraint: "<= 0.9.11", - VersionFormat: "unknown", + nvdVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-1", + Namespace: "nvd:cpe", + }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("<= 0.9.11", version.UnknownFormat), // Note: the product name is NOT the same as the target package name - CPEs: []string{"cpe:2.3:a:lib_vnc_project-(server):libvncumbrellaproject:*:*:*:*:*:*:*:*"}, - Namespace: "nvd:cpe", + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:a:lib_vnc_project-(server):libvncumbrellaproject:*:*:*:*:*:*:*:*", ""), + }, } - secDbVuln := v5.Vulnerability{ - // ID *does* match - this is the key for comparison in the matcher - ID: "CVE-2020-1", - VersionConstraint: "<= 0.9.11", - VersionFormat: "apk", - Namespace: "secdb:distro:alpine:3.12", - } - store := mockStore{ - backend: map[string]map[string][]v5.Vulnerability{ - "nvd:cpe": { - "libvncumbrellaproject": []v5.Vulnerability{nvdVuln}, - }, - "secdb:distro:alpine:3.12": { - "libvncserver": []v5.Vulnerability{secDbVuln}, - }, + secDbVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + // ID *does* match - this is the key for comparison in the matcher + ID: "CVE-2020-1", + Namespace: "secdb:distro:alpine:3.12", }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("<= 0.9.11", version.ApkFormat), } - provider, err := v5.NewVulnerabilityProvider(&store) - require.NoError(t, err) + vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) m := Matcher{} d, err := distro.New(distro.Alpine, "3.12.0", "") @@ -353,20 +287,17 @@ func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) { Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, + Distro: d, CPEs: []cpe.CPE{ // Note: the product name is NOT the same as the package name cpe.Must("cpe:2.3:a:*:libvncumbrellaproject:0.9.9:*:*:*:*:*:*:*", ""), }, } - // ensure the SECDB record is preferred over the NVD record - vulnFound, err := v5.NewVulnerability(secDbVuln) - assert.NoError(t, err) - expected := []match.Match{ { - - Vulnerability: *vulnFound, + // ensure the SECDB record is preferred over the NVD record + Vulnerability: secDbVuln, Package: p, Details: []match.Detail{ { @@ -384,7 +315,7 @@ func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) { "namespace": "secdb:distro:alpine:3.12", }, Found: map[string]interface{}{ - "versionConstraint": vulnFound.Constraint.String(), + "versionConstraint": secDbVuln.Constraint.String(), "vulnerabilityID": "CVE-2020-1", }, Matcher: match.ApkMatcher, @@ -393,30 +324,25 @@ func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) { }, } - actual, err := m.Match(provider, d, p) + actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestNvdOnlyMatches(t *testing.T) { - nvdVuln := v5.Vulnerability{ - ID: "CVE-2020-1", - VersionConstraint: "<= 0.9.11", - VersionFormat: "unknown", - CPEs: []string{`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`}, - Namespace: "nvd:cpe", - } - store := mockStore{ - backend: map[string]map[string][]v5.Vulnerability{ - "nvd:cpe": { - "libvncserver": []v5.Vulnerability{nvdVuln}, - }, + nvdVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-1", + Namespace: "nvd:cpe", + }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("<= 0.9.11", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""), }, } - - provider, err := v5.NewVulnerabilityProvider(&store) - require.NoError(t, err) + vp := mock.VulnerabilityProvider(nvdVuln) m := Matcher{} d, err := distro.New(distro.Alpine, "3.12.0", "") @@ -428,35 +354,32 @@ func TestNvdOnlyMatches(t *testing.T) { Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, + Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""), }, } - vulnFound, err := v5.NewVulnerability(nvdVuln) - assert.NoError(t, err) - vulnFound.CPEs = []cpe.CPE{cpe.Must(nvdVuln.CPEs[0], "")} - expected := []match.Match{ { - Vulnerability: *vulnFound, + Vulnerability: nvdVuln, Package: p, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: search.CPEParameters{ + SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", - Package: search.CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "libvncserver", Version: "0.9.9", }, }, - Found: search.CPEResult{ - CPEs: []string{vulnFound.CPEs[0].Attributes.BindToFmtString()}, - VersionConstraint: vulnFound.Constraint.String(), + Found: match.CPEResult{ + CPEs: []string{nvdVuln.CPEs[0].Attributes.BindToFmtString()}, + VersionConstraint: nvdVuln.Constraint.String(), VulnerabilityID: "CVE-2020-1", }, Matcher: match.ApkMatcher, @@ -465,34 +388,29 @@ func TestNvdOnlyMatches(t *testing.T) { }, } - actual, err := m.Match(provider, d, p) + actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestNvdOnlyMatches_FixInNvd(t *testing.T) { - nvdVuln := v5.Vulnerability{ - ID: "CVE-2020-1", - VersionConstraint: "< 0.9.11", - VersionFormat: "unknown", - CPEs: []string{`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`}, - Namespace: "nvd:cpe", - Fix: v5.Fix{ - Versions: []string{"0.9.12"}, - State: v5.FixedState, + nvdVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-1", + Namespace: "nvd:cpe", }, - } - store := mockStore{ - backend: map[string]map[string][]v5.Vulnerability{ - "nvd:cpe": { - "libvncserver": []v5.Vulnerability{nvdVuln}, - }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("< 0.9.11", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""), + }, + Fix: vulnerability.Fix{ + Versions: []string{"0.9.12"}, + State: vulnerability.FixStateFixed, }, } - - provider, err := v5.NewVulnerabilityProvider(&store) - require.NoError(t, err) + vp := mock.VulnerabilityProvider(nvdVuln) m := Matcher{} d, err := distro.New(distro.Alpine, "3.12.0", "") @@ -504,36 +422,34 @@ func TestNvdOnlyMatches_FixInNvd(t *testing.T) { Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, + Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""), }, } - vulnFound, err := v5.NewVulnerability(nvdVuln) - assert.NoError(t, err) - vulnFound.CPEs = []cpe.CPE{cpe.Must(nvdVuln.CPEs[0], "")} + vulnFound := nvdVuln // Important: for alpine matcher, fix version can come from secDB but _not_ from // NVD data. vulnFound.Fix = vulnerability.Fix{State: vulnerability.FixStateUnknown} expected := []match.Match{ { - - Vulnerability: *vulnFound, + Vulnerability: vulnFound, Package: p, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: search.CPEParameters{ + SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", - Package: search.CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "libvncserver", Version: "0.9.9", }, }, - Found: search.CPEResult{ + Found: match.CPEResult{ CPEs: []string{vulnFound.CPEs[0].Attributes.BindToFmtString()}, VersionConstraint: vulnFound.Constraint.String(), VulnerabilityID: "CVE-2020-1", @@ -544,37 +460,36 @@ func TestNvdOnlyMatches_FixInNvd(t *testing.T) { }, } - actual, err := m.Match(provider, d, p) + actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestNvdMatchesProperVersionFiltering(t *testing.T) { - nvdVulnMatch := v5.Vulnerability{ - ID: "CVE-2020-1", - VersionConstraint: "<= 0.9.11", - VersionFormat: "unknown", - CPEs: []string{`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`}, - Namespace: "nvd:cpe", - } - nvdVulnNoMatch := v5.Vulnerability{ - ID: "CVE-2020-2", - VersionConstraint: "< 0.9.11", - VersionFormat: "unknown", - CPEs: []string{`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`}, - Namespace: "nvd:cpe", + nvdVulnMatch := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-1", + Namespace: "nvd:cpe", + }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("<= 0.9.11", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""), + }, } - store := mockStore{ - backend: map[string]map[string][]v5.Vulnerability{ - "nvd:cpe": { - "libvncserver": []v5.Vulnerability{nvdVulnMatch, nvdVulnNoMatch}, - }, + nvdVulnNoMatch := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-2", + Namespace: "nvd:cpe", + }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("< 0.9.11", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""), }, } - - provider, err := v5.NewVulnerabilityProvider(&store) - require.NoError(t, err) + vp := mock.VulnerabilityProvider(nvdVulnMatch, nvdVulnNoMatch) m := Matcher{} d, err := distro.New(distro.Alpine, "3.12.0", "") @@ -586,35 +501,31 @@ func TestNvdMatchesProperVersionFiltering(t *testing.T) { Name: "libvncserver", Version: "0.9.11-r10", Type: syftPkg.ApkPkg, + Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.11:*:*:*:*:*:*:*", ""), }, } - vulnFound, err := v5.NewVulnerability(nvdVulnMatch) - assert.NoError(t, err) - vulnFound.CPEs = []cpe.CPE{cpe.Must(nvdVulnMatch.CPEs[0], "")} - expected := []match.Match{ { - - Vulnerability: *vulnFound, + Vulnerability: nvdVulnMatch, Package: p, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: search.CPEParameters{ + SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:*:libvncserver:0.9.11:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", - Package: search.CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "libvncserver", Version: "0.9.11-r10", }, }, - Found: search.CPEResult{ - CPEs: []string{vulnFound.CPEs[0].Attributes.BindToFmtString()}, - VersionConstraint: vulnFound.Constraint.String(), + Found: match.CPEResult{ + CPEs: []string{nvdVulnMatch.CPEs[0].Attributes.BindToFmtString()}, + VersionConstraint: nvdVulnMatch.Constraint.String(), VulnerabilityID: "CVE-2020-1", }, Matcher: match.ApkMatcher, @@ -623,40 +534,35 @@ func TestNvdMatchesProperVersionFiltering(t *testing.T) { }, } - actual, err := m.Match(provider, d, p) + actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestNvdMatchesWithSecDBFix(t *testing.T) { - nvdVuln := v5.Vulnerability{ - ID: "CVE-2020-1", - VersionConstraint: "> 0.9.0, < 0.10.0", // note: this is not normal NVD configuration, but has the desired effect of a "wide net" for vulnerable indication - VersionFormat: "unknown", - CPEs: []string{`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`}, - Namespace: "nvd:cpe", - } - - secDbVuln := v5.Vulnerability{ - ID: "CVE-2020-1", - VersionConstraint: "< 0.9.11", // note: this does NOT include 0.9.11, so NVD and SecDB mismatch here... secDB should trump in this case - VersionFormat: "apk", + nvdVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-1", + Namespace: "nvd:cpe", + }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("> 0.9.0, < 0.10.0", version.UnknownFormat), // note: this is not normal NVD configuration, but has the desired effect of a "wide net" for vulnerable indication + CPEs: []cpe.CPE{ + cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""), + }, } - store := mockStore{ - backend: map[string]map[string][]v5.Vulnerability{ - "nvd:cpe": { - "libvncserver": []v5.Vulnerability{nvdVuln}, - }, - "secdb:distro:alpine:3.12": { - "libvncserver": []v5.Vulnerability{secDbVuln}, - }, + secDbVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-1", + Namespace: "secdb:distro:alpine:3.12", }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("< 0.9.11", version.ApkFormat), // note: this does NOT include 0.9.11, so NVD and SecDB mismatch here... secDB should trump in this case } - provider, err := v5.NewVulnerabilityProvider(&store) - require.NoError(t, err) + vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) m := Matcher{} d, err := distro.New(distro.Alpine, "3.12.0", "") @@ -668,48 +574,43 @@ func TestNvdMatchesWithSecDBFix(t *testing.T) { Name: "libvncserver", Version: "0.9.11", Type: syftPkg.ApkPkg, + Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""), }, } - expected := []match.Match{} + var expected []match.Match - actual, err := m.Match(provider, d, p) + actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestNvdMatchesNoConstraintWithSecDBFix(t *testing.T) { - nvdVuln := v5.Vulnerability{ - ID: "CVE-2020-1", - VersionConstraint: "", // note: empty value indicates that all versions are vulnerable - VersionFormat: "unknown", - CPEs: []string{`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`}, - Namespace: "nvd:cpe", - } - - secDbVuln := v5.Vulnerability{ - ID: "CVE-2020-1", - VersionConstraint: "< 0.9.11", - VersionFormat: "apk", - Namespace: "secdb:distro:alpine:3.12", + nvdVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-1", + Namespace: "nvd:cpe", + }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("", version.UnknownFormat), // note: empty value indicates that all versions are vulnerable + CPEs: []cpe.CPE{ + cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""), + }, } - store := mockStore{ - backend: map[string]map[string][]v5.Vulnerability{ - "nvd:cpe": { - "libvncserver": []v5.Vulnerability{nvdVuln}, - }, - "secdb:distro:alpine:3.12": { - "libvncserver": []v5.Vulnerability{secDbVuln}, - }, + secDbVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-1", + Namespace: "secdb:distro:alpine:3.12", }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("< 0.9.11", version.ApkFormat), } - provider, err := v5.NewVulnerabilityProvider(&store) - require.NoError(t, err) + vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) m := Matcher{} d, err := distro.New(distro.Alpine, "3.12.0", "") @@ -721,45 +622,41 @@ func TestNvdMatchesNoConstraintWithSecDBFix(t *testing.T) { Name: "libvncserver", Version: "0.9.11", Type: syftPkg.ApkPkg, + Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""), }, } - expected := []match.Match{} + var expected []match.Match - actual, err := m.Match(provider, d, p) + actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestNVDMatchCanceledByOriginPackageInSecDB(t *testing.T) { - nvdVuln := v5.Vulnerability{ - ID: "CVE-2015-3211", - VersionFormat: "unknown", - CPEs: []string{"cpe:2.3:a:php-fpm:php-fpm:-:*:*:*:*:*:*:*"}, - Namespace: "nvd:cpe", - } - secDBVuln := v5.Vulnerability{ - ID: "CVE-2015-3211", - VersionConstraint: "< 0", - VersionFormat: "apk", - Namespace: "wolfi:distro:wolfi:rolling", + nvdVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2015-3211", + Namespace: "nvd:cpe", + }, + PackageName: "php-fpm", + Constraint: version.MustGetConstraint("", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:a:php-fpm:php-fpm:-:*:*:*:*:*:*:*", ""), + }, } - store := mockStore{ - backend: map[string]map[string][]v5.Vulnerability{ - "nvd:cpe": { - "php-fpm": []v5.Vulnerability{nvdVuln}, - }, - "wolfi:distro:wolfi:rolling": { - "php-8.3": []v5.Vulnerability{secDBVuln}, - }, + secDBVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2015-3211", + Namespace: "wolfi:distro:wolfi:rolling", }, + PackageName: "php-8.3", + Constraint: version.MustGetConstraint("< 0", version.ApkFormat), } - - provider, err := v5.NewVulnerabilityProvider(&store) - require.NoError(t, err) + vp := mock.VulnerabilityProvider(nvdVuln, secDBVuln) m := Matcher{} d, err := distro.New(distro.Wolfi, "") @@ -768,23 +665,24 @@ func TestNVDMatchCanceledByOriginPackageInSecDB(t *testing.T) { } p := pkg.Package{ ID: pkg.ID(uuid.NewString()), - Name: "php-8.3-fpm", + Name: "php-8.3-fpm", // the package will not match anything Version: "8.3.11-r0", Type: syftPkg.ApkPkg, + Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:php-fpm:php-fpm:8.3.11-r0:*:*:*:*:*:*:*", ""), }, Upstreams: []pkg.UpstreamPackage{ { - Name: "php-8.3", + Name: "php-8.3", // this upstream should match Version: "8.3.11-r0", }, }, } - expected := []match.Match{} + var expected []match.Match - actual, err := m.Match(provider, d, p) + actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) @@ -792,23 +690,16 @@ func TestNVDMatchCanceledByOriginPackageInSecDB(t *testing.T) { func TestDistroMatchBySourceIndirection(t *testing.T) { - secDbVuln := v5.Vulnerability{ - // ID doesn't match - this is the key for comparison in the matcher - ID: "CVE-2020-2", - VersionConstraint: "<= 1.3.3-r0", - VersionFormat: "apk", - Namespace: "secdb:distro:alpine:3.12", - } - store := mockStore{ - backend: map[string]map[string][]v5.Vulnerability{ - "secdb:distro:alpine:3.12": { - "musl": []v5.Vulnerability{secDbVuln}, - }, + secDbVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + // ID doesn't match - this is the key for comparison in the matcher + ID: "CVE-2020-2", + Namespace: "secdb:distro:alpine:3.12", }, + PackageName: "musl", + Constraint: version.MustGetConstraint("<= 1.3.3-r0", version.ApkFormat), } - - provider, err := v5.NewVulnerabilityProvider(&store) - require.NoError(t, err) + vp := mock.VulnerabilityProvider(secDbVuln) m := Matcher{} d, err := distro.New(distro.Alpine, "3.12.0", "") @@ -820,6 +711,7 @@ func TestDistroMatchBySourceIndirection(t *testing.T) { Name: "musl-utils", Version: "1.3.2-r0", Type: syftPkg.ApkPkg, + Distro: d, Upstreams: []pkg.UpstreamPackage{ { Name: "musl", @@ -830,13 +722,10 @@ func TestDistroMatchBySourceIndirection(t *testing.T) { }, } - vulnFound, err := v5.NewVulnerability(secDbVuln) - assert.NoError(t, err) - expected := []match.Match{ { - Vulnerability: *vulnFound, + Vulnerability: secDbVuln, Package: p, Details: []match.Detail{ { @@ -854,7 +743,7 @@ func TestDistroMatchBySourceIndirection(t *testing.T) { "namespace": "secdb:distro:alpine:3.12", }, Found: map[string]interface{}{ - "versionConstraint": vulnFound.Constraint.String(), + "versionConstraint": secDbVuln.Constraint.String(), "vulnerabilityID": "CVE-2020-2", }, Matcher: match.ApkMatcher, @@ -863,7 +752,7 @@ func TestDistroMatchBySourceIndirection(t *testing.T) { }, } - actual, err := m.Match(provider, d, p) + actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) @@ -873,23 +762,16 @@ func TestSecDBMatchesStillCountedWithCpeErrors(t *testing.T) { // this should match the test package // the test package will have no CPE causing an error, // but the error should not cause the secDB matches to fail - secDbVuln := v5.Vulnerability{ - ID: "CVE-2020-2", - VersionConstraint: "<= 1.3.3-r0", - VersionFormat: "apk", - Namespace: "secdb:distro:alpine:3.12", - } - - store := mockStore{ - backend: map[string]map[string][]v5.Vulnerability{ - "secdb:distro:alpine:3.12": { - "musl": []v5.Vulnerability{secDbVuln}, - }, + secDbVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-2", + Namespace: "secdb:distro:alpine:3.12", }, + PackageName: "musl", + Constraint: version.MustGetConstraint("<= 1.3.3-r0", version.ApkFormat), } - provider, err := v5.NewVulnerabilityProvider(&store) - require.NoError(t, err) + vp := mock.VulnerabilityProvider(secDbVuln) m := Matcher{} d, err := distro.New(distro.Alpine, "3.12.0", "") @@ -902,6 +784,7 @@ func TestSecDBMatchesStillCountedWithCpeErrors(t *testing.T) { Name: "musl-utils", Version: "1.3.2-r0", Type: syftPkg.ApkPkg, + Distro: d, Upstreams: []pkg.UpstreamPackage{ { Name: "musl", @@ -910,13 +793,10 @@ func TestSecDBMatchesStillCountedWithCpeErrors(t *testing.T) { CPEs: []cpe.CPE{}, } - vulnFound, err := v5.NewVulnerability(secDbVuln) - assert.NoError(t, err) - expected := []match.Match{ { - Vulnerability: *vulnFound, + Vulnerability: secDbVuln, Package: p, Details: []match.Detail{ { @@ -934,7 +814,7 @@ func TestSecDBMatchesStillCountedWithCpeErrors(t *testing.T) { "namespace": "secdb:distro:alpine:3.12", }, Found: map[string]interface{}{ - "versionConstraint": vulnFound.Constraint.String(), + "versionConstraint": secDbVuln.Constraint.String(), "vulnerabilityID": "CVE-2020-2", }, Matcher: match.ApkMatcher, @@ -943,30 +823,25 @@ func TestSecDBMatchesStillCountedWithCpeErrors(t *testing.T) { }, } - actual, err := m.Match(provider, d, p) + actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestNVDMatchBySourceIndirection(t *testing.T) { - nvdVuln := v5.Vulnerability{ - ID: "CVE-2020-1", - VersionConstraint: "<= 1.3.3-r0", - VersionFormat: "unknown", - CPEs: []string{"cpe:2.3:a:musl:musl:*:*:*:*:*:*:*:*"}, - Namespace: "nvd:cpe", - } - store := mockStore{ - backend: map[string]map[string][]v5.Vulnerability{ - "nvd:cpe": { - "musl": []v5.Vulnerability{nvdVuln}, - }, + nvdVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-1", + Namespace: "nvd:cpe", + }, + PackageName: "musl", + Constraint: version.MustGetConstraint("<= 1.3.3-r0", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:a:musl:musl:*:*:*:*:*:*:*:*", ""), }, } - - provider, err := v5.NewVulnerabilityProvider(&store) - require.NoError(t, err) + vp := mock.VulnerabilityProvider(nvdVuln) m := Matcher{} d, err := distro.New(distro.Alpine, "3.12.0", "") @@ -978,6 +853,7 @@ func TestNVDMatchBySourceIndirection(t *testing.T) { Name: "musl-utils", Version: "1.3.2-r0", Type: syftPkg.ApkPkg, + Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:musl-utils:musl-utils:*:*:*:*:*:*:*:*", ""), cpe.Must("cpe:2.3:a:musl-utils:musl-utils:*:*:*:*:*:*:*:*", ""), @@ -989,29 +865,25 @@ func TestNVDMatchBySourceIndirection(t *testing.T) { }, } - vulnFound, err := v5.NewVulnerability(nvdVuln) - assert.NoError(t, err) - vulnFound.CPEs = []cpe.CPE{cpe.Must(nvdVuln.CPEs[0], "")} - expected := []match.Match{ { - Vulnerability: *vulnFound, + Vulnerability: nvdVuln, Package: p, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: search.CPEParameters{ + SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:musl:musl:1.3.2-r0:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", - Package: search.CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "musl", Version: "1.3.2-r0", }, }, - Found: search.CPEResult{ - CPEs: []string{vulnFound.CPEs[0].Attributes.BindToFmtString()}, - VersionConstraint: vulnFound.Constraint.String(), + Found: match.CPEResult{ + CPEs: []string{nvdVuln.CPEs[0].Attributes.BindToFmtString()}, + VersionConstraint: nvdVuln.Constraint.String(), VulnerabilityID: "CVE-2020-1", }, Matcher: match.ApkMatcher, @@ -1020,7 +892,7 @@ func TestNVDMatchBySourceIndirection(t *testing.T) { }, } - actual, err := m.Match(provider, d, p) + actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) @@ -1037,3 +909,46 @@ func assertMatches(t *testing.T, expected, actual []match.Match) { t.Errorf("mismatch (-want +got):\n%s", diff) } } + +func Test_nakConstraint(t *testing.T) { + tests := []struct { + name string + input vulnerability.Vulnerability + wantErr require.ErrorAssertionFunc + matches bool + }{ + { + name: "matches apk", + input: vulnerability.Vulnerability{ + Constraint: version.MustGetConstraint("< 0", version.ApkFormat), + }, + matches: true, + }, + { + name: "not match due to type", + input: vulnerability.Vulnerability{ + Constraint: version.MustGetConstraint("< 0", version.SemanticFormat), + }, + matches: false, + }, + { + name: "not match", + input: vulnerability.Vulnerability{ + Constraint: version.MustGetConstraint("< 2.0", version.SemanticFormat), + }, + matches: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + matches, err := nakConstraint.MatchesVulnerability(tt.input) + wantErr := require.NoError + if tt.wantErr != nil { + wantErr = tt.wantErr + } + wantErr(t, err) + require.Equal(t, tt.matches, matches) + }) + } +} diff --git a/grype/db/v5/matcher/dotnet/matcher.go b/grype/matcher/dotnet/matcher.go similarity index 55% rename from grype/db/v5/matcher/dotnet/matcher.go rename to grype/matcher/dotnet/matcher.go index 02df52351bd8..d16a29bfbed4 100644 --- a/grype/db/v5/matcher/dotnet/matcher.go +++ b/grype/matcher/dotnet/matcher.go @@ -1,11 +1,10 @@ package dotnet import ( - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/search" - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -31,10 +30,6 @@ func (m *Matcher) Type() match.MatcherType { return match.DotnetMatcher } -func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { - criteria := search.CommonCriteria - if m.cfg.UseCPEs { - criteria = append(criteria, search.ByCPE) - } - return search.ByCriteria(store, d, p, m.Type(), criteria...) +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + return internal.MatchPackageByLanguageAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } diff --git a/grype/db/v5/matcher/dpkg/matcher.go b/grype/matcher/dpkg/matcher.go similarity index 58% rename from grype/db/v5/matcher/dpkg/matcher.go rename to grype/matcher/dpkg/matcher.go index f3848a03eaa4..3470661a8275 100644 --- a/grype/db/v5/matcher/dpkg/matcher.go +++ b/grype/matcher/dpkg/matcher.go @@ -3,11 +3,10 @@ package dpkg import ( "fmt" - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/search" - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -22,29 +21,29 @@ func (m *Matcher) Type() match.MatcherType { return match.DpkgMatcher } -func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { matches := make([]match.Match, 0) - sourceMatches, err := m.matchUpstreamPackages(store, d, p) + sourceMatches, err := m.matchUpstreamPackages(store, p) if err != nil { - return nil, fmt.Errorf("failed to match by source indirection: %w", err) + return nil, nil, fmt.Errorf("failed to match by source indirection: %w", err) } matches = append(matches, sourceMatches...) - exactMatches, err := search.ByPackageDistro(store, d, p, m.Type()) + exactMatches, _, err := internal.MatchPackageByDistro(store, p, m.Type()) if err != nil { - return nil, fmt.Errorf("failed to match by exact package name: %w", err) + return nil, nil, fmt.Errorf("failed to match by exact package name: %w", err) } matches = append(matches, exactMatches...) - return matches, nil + return matches, nil, nil } -func (m *Matcher) matchUpstreamPackages(store v5.ProviderByDistro, d *distro.Distro, p pkg.Package) ([]match.Match, error) { +func (m *Matcher) matchUpstreamPackages(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) { var matches []match.Match for _, indirectPackage := range pkg.UpstreamPackages(p) { - indirectMatches, err := search.ByPackageDistro(store, d, indirectPackage, m.Type()) + indirectMatches, _, err := internal.MatchPackageByDistro(store, indirectPackage, m.Type()) if err != nil { return nil, fmt.Errorf("failed to find vulnerabilities for dpkg upstream source package: %w", err) } diff --git a/grype/matcher/dpkg/matcher_mocks_test.go b/grype/matcher/dpkg/matcher_mocks_test.go new file mode 100644 index 000000000000..daa5b9aa7bb8 --- /dev/null +++ b/grype/matcher/dpkg/matcher_mocks_test.go @@ -0,0 +1,34 @@ +package dpkg + +import ( + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/grype/vulnerability/mock" +) + +func newMockProvider() vulnerability.Provider { + return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ + { + PackageName: "neutron", + Reference: vulnerability.Reference{ID: "CVE-2014-fake-1", Namespace: "secdb:distro:debian:8"}, + Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat), + }, + // expected... + { + PackageName: "neutron-devel", + Constraint: version.MustGetConstraint("< 2014.1.4-5", version.DebFormat), + Reference: vulnerability.Reference{ID: "CVE-2014-fake-2", Namespace: "secdb:distro:debian:8"}, + }, + { + PackageName: "neutron-devel", + Constraint: version.MustGetConstraint("< 2015.0.0-1", version.DebFormat), + Reference: vulnerability.Reference{ID: "CVE-2013-fake-3", Namespace: "secdb:distro:debian:8"}, + }, + // unexpected... + { + PackageName: "neutron-devel", + Constraint: version.MustGetConstraint("< 2014.0.4-1", version.DebFormat), + Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD", Namespace: "secdb:distro:debian:8"}, + }, + }...) +} diff --git a/grype/db/v5/matcher/dpkg/matcher_test.go b/grype/matcher/dpkg/matcher_test.go similarity index 94% rename from grype/db/v5/matcher/dpkg/matcher_test.go rename to grype/matcher/dpkg/matcher_test.go index b04a5c477dec..fbbd80ff009a 100644 --- a/grype/db/v5/matcher/dpkg/matcher_test.go +++ b/grype/matcher/dpkg/matcher_test.go @@ -16,11 +16,18 @@ import ( func TestMatcherDpkg_matchBySourceIndirection(t *testing.T) { matcher := Matcher{} + + d, err := distro.New(distro.Debian, "8", "") + if err != nil { + t.Fatal("could not create distro: ", err) + } + p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "neutron", Version: "2014.1.3-6", Type: syftPkg.DebPkg, + Distro: d, Upstreams: []pkg.UpstreamPackage{ { Name: "neutron-devel", @@ -28,13 +35,8 @@ func TestMatcherDpkg_matchBySourceIndirection(t *testing.T) { }, } - d, err := distro.New(distro.Debian, "8", "") - if err != nil { - t.Fatal("could not create distro: ", err) - } - - store := newMockProvider() - actual, err := matcher.matchUpstreamPackages(store, d, p) + vp := newMockProvider() + actual, err := matcher.matchUpstreamPackages(vp, p) assert.NoError(t, err, "unexpected err from matchUpstreamPackages", err) assert.Len(t, actual, 2, "unexpected indirect matches count") diff --git a/grype/db/v5/matcher/golang/matcher.go b/grype/matcher/golang/matcher.go similarity index 79% rename from grype/db/v5/matcher/golang/matcher.go rename to grype/matcher/golang/matcher.go index 377ce871d114..339bfd38ad92 100644 --- a/grype/db/v5/matcher/golang/matcher.go +++ b/grype/matcher/golang/matcher.go @@ -3,11 +3,10 @@ package golang import ( "strings" - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/search" - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -35,7 +34,7 @@ func (m *Matcher) Type() match.MatcherType { return match.GoModuleMatcher } -func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { matches := make([]match.Match, 0) mainModule := "" @@ -58,15 +57,10 @@ func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg. isNotCorrected = strings.HasPrefix(p.Version, "v0.0.0-") || strings.HasPrefix(p.Version, "(devel)") } if p.Name == mainModule && isNotCorrected { - return matches, nil + return matches, nil, nil } - criteria := search.CommonCriteria - if searchByCPE(p.Name, m.cfg) { - criteria = append(criteria, search.ByCPE) - } - - return search.ByCriteria(store, d, p, m.Type(), criteria...) + return internal.MatchPackageByLanguageAndCPEs(store, p, m.Type(), searchByCPE(p.Name, m.cfg)) } func searchByCPE(name string, cfg MatcherConfig) bool { diff --git a/grype/db/v5/matcher/golang/matcher_test.go b/grype/matcher/golang/matcher_test.go similarity index 75% rename from grype/db/v5/matcher/golang/matcher_test.go rename to grype/matcher/golang/matcher_test.go index 8bbd1960e992..81206078b0cd 100644 --- a/grype/db/v5/matcher/golang/matcher_test.go +++ b/grype/matcher/golang/matcher_test.go @@ -7,10 +7,10 @@ import ( "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/grype/vulnerability/mock" "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -73,13 +73,13 @@ func TestMatcher_DropMainPackageGivenVersionInfo(t *testing.T) { }) store := newMockProvider() - preTest, _ := matcher.Match(store, nil, subjectWithoutMainModule) + preTest, _, _ := matcher.Match(store, subjectWithoutMainModule) assert.Len(t, preTest, 1, "should have matched the package when there is not a main module") - actual, _ := matcher.Match(store, nil, subjectWithMainModule) + actual, _, _ := matcher.Match(store, subjectWithMainModule) assert.Len(t, actual, test.expectedMatchCount, "should match the main module depending on config (i.e. 1 match)") - actual, _ = matcher.Match(store, nil, subjectWithMainModuleAsDevel) + actual, _, _ = matcher.Match(store, subjectWithMainModuleAsDevel) assert.Len(t, actual, 0, "unexpected match count; should never match main module (devel)") }) } @@ -180,7 +180,7 @@ func TestMatcher_SearchForStdlib(t *testing.T) { t.Run(c.name, func(t *testing.T) { matcher := NewGolangMatcher(c.cfg) - actual, _ := matcher.Match(store, nil, c.subject) + actual, _, _ := matcher.Match(store, c.subject) actualCVEs := strset.New() for _, m := range actual { actualCVEs.Add(m.Vulnerability.ID) @@ -192,58 +192,20 @@ func TestMatcher_SearchForStdlib(t *testing.T) { }) } - -} - -func newMockProvider() *mockProvider { - mp := mockProvider{ - data: make(map[syftPkg.Language]map[string][]vulnerability.Vulnerability), - } - - mp.populateData() - - return &mp } -type mockProvider struct { - data map[syftPkg.Language]map[string][]vulnerability.Vulnerability -} - -func (mp *mockProvider) Get(id, namespace string) ([]vulnerability.Vulnerability, error) { - // TODO implement me - panic("implement me") -} - -func (mp *mockProvider) populateData() { - mp.data[syftPkg.Go] = map[string][]vulnerability.Vulnerability{ +func newMockProvider() vulnerability.Provider { + return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ // for TestMatcher_DropMainPackageIfNoVersion - "istio.io/istio": { - { - Constraint: version.MustGetConstraint("< 5.0.7", version.UnknownFormat), - Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD"}, - }, + { + PackageName: "istio.io/istio", + Constraint: version.MustGetConstraint("< 5.0.7", version.UnknownFormat), + Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD", Namespace: "github:language:" + syftPkg.Go.String()}, }, - } - - mp.data["nvd:cpe"] = map[string][]vulnerability.Vulnerability{ - // for TestMatcher_SearchForStdlib - "cpe:2.3:a:golang:go:1.18.3:-:*:*:*:*:*:*": { - { - Constraint: version.MustGetConstraint("< 1.18.6 || = 1.19.0", version.UnknownFormat), - Reference: vulnerability.Reference{ID: "CVE-2022-27664"}, - }, + { + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:golang:go:1.18.3:-:*:*:*:*:*:*", "test")}, + Constraint: version.MustGetConstraint("< 1.18.6 || = 1.19.0", version.UnknownFormat), + Reference: vulnerability.Reference{ID: "CVE-2022-27664", Namespace: "nvd:cpe"}, }, - } -} - -func (mp *mockProvider) GetByCPE(p cpe.CPE) ([]vulnerability.Vulnerability, error) { - return mp.data["nvd:cpe"][p.Attributes.BindToFmtString()], nil -} - -func (mp *mockProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) { - return []vulnerability.Vulnerability{}, nil -} - -func (mp *mockProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]vulnerability.Vulnerability, error) { - return mp.data[l][p.Name], nil + }...) } diff --git a/grype/matcher/internal/common.go b/grype/matcher/internal/common.go new file mode 100644 index 000000000000..29b4b220f0c6 --- /dev/null +++ b/grype/matcher/internal/common.go @@ -0,0 +1,56 @@ +package internal + +import ( + "errors" + + "github.com/anchore/grype/grype/db/v5/pkg/resolver" + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/log" +) + +func PackageNames(p pkg.Package) []string { + names := []string{p.Name} + r, _ := resolver.FromLanguage(p.Language) + if r != nil { + parts := r.Resolve(p) + if len(parts) > 0 { + names = parts + } + } + return names +} + +func MatchPackageByLanguageAndCPEs(store vulnerability.Provider, p pkg.Package, matcher match.MatcherType, includeCPEs bool) ([]match.Match, []match.IgnoredMatch, error) { + var matches []match.Match + var ignored []match.IgnoredMatch + + for _, name := range PackageNames(p) { + nameMatches, nameIgnores, err := MatchPackageByLanguagePackageNameAndCPEs(store, p, name, matcher, includeCPEs) + if err != nil { + return nil, nil, err + } + matches = append(matches, nameMatches...) + ignored = append(ignored, nameIgnores...) + } + + return matches, ignored, nil +} + +func MatchPackageByLanguagePackageNameAndCPEs(store vulnerability.Provider, p pkg.Package, packageName string, matcher match.MatcherType, includeCPEs bool) ([]match.Match, []match.IgnoredMatch, error) { + matches, ignored, err := MatchPackageByLanguagePackageName(store, p, packageName, matcher) + if err != nil { + log.Debugf("could not match by package language (package=%+v): %v", p, err) + } + if includeCPEs { + cpeMatches, err := MatchPackageByCPEs(store, p, matcher) + if errors.Is(err, ErrEmptyCPEMatch) { + log.Debugf("attempted CPE search on %s, which has no CPEs. Consider re-running with --add-cpes-if-none", p.Name) + } else if err != nil { + log.Debugf("could not match by package CPE (package=%+v): %v", p, err) + } + matches = append(matches, cpeMatches...) + } + return matches, ignored, nil +} diff --git a/grype/db/v5/search/cpe.go b/grype/matcher/internal/cpe.go similarity index 74% rename from grype/db/v5/search/cpe.go rename to grype/matcher/internal/cpe.go index e267ad69606a..6808af775e8d 100644 --- a/grype/db/v5/search/cpe.go +++ b/grype/matcher/internal/cpe.go @@ -1,4 +1,4 @@ -package search +package internal import ( "errors" @@ -7,12 +7,10 @@ import ( "strings" "github.com/facebookincubator/nvdtools/wfn" - "github.com/scylladb/go-set/strset" - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" @@ -20,54 +18,6 @@ import ( syftPkg "github.com/anchore/syft/syft/pkg" ) -type CPEPackageParameter struct { - Name string `json:"name"` - Version string `json:"version"` -} - -type CPEParameters struct { - Namespace string `json:"namespace"` - CPEs []string `json:"cpes"` - Package CPEPackageParameter `json:"package"` -} - -func (i *CPEParameters) Merge(other CPEParameters) error { - if i.Namespace != other.Namespace { - return fmt.Errorf("namespaces do not match") - } - - existingCPEs := strset.New(i.CPEs...) - newCPEs := strset.New(other.CPEs...) - mergedCPEs := strset.Union(existingCPEs, newCPEs).List() - sort.Strings(mergedCPEs) - i.CPEs = mergedCPEs - return nil -} - -type CPEResult struct { - VulnerabilityID string `json:"vulnerabilityID"` - VersionConstraint string `json:"versionConstraint"` - CPEs []string `json:"cpes"` -} - -func (h CPEResult) Equals(other CPEResult) bool { - if h.VersionConstraint != other.VersionConstraint { - return false - } - - if len(h.CPEs) != len(other.CPEs) { - return false - } - - for i := range h.CPEs { - if h.CPEs[i] != other.CPEs[i] { - return false - } - } - - return true -} - func alpineCPEComparableVersion(version string) string { // clean the alpine package version so that it compares correctly with the CPE version comparison logic // alpine versions are suffixed with -r{buildindex}; however, if left intact CPE comparison logic will @@ -86,8 +36,8 @@ func alpineCPEComparableVersion(version string) string { var ErrEmptyCPEMatch = errors.New("attempted CPE match against package with no CPEs") -// ByPackageCPE retrieves all vulnerabilities that match the generated CPE -func ByPackageCPE(store v5.ProviderByCPE, d *distro.Distro, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) { +// MatchPackageByCPEs retrieves all vulnerabilities that match any of the provided package's CPEs +func MatchPackageByCPEs(store vulnerability.Provider, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) { // we attempt to merge match details within the same matcher when searching by CPEs, in this way there are fewer duplicated match // objects (and fewer duplicated match details). @@ -95,6 +45,7 @@ func ByPackageCPE(store v5.ProviderByCPE, d *distro.Distro, p pkg.Package, upstr if len(p.CPEs) == 0 { return nil, ErrEmptyCPEMatch } + matchesByFingerprint := make(map[match.Fingerprint]match.Match) for _, c := range p.CPEs { // prefer the CPE version, but if npt specified use the package version @@ -129,28 +80,20 @@ func ByPackageCPE(store v5.ProviderByCPE, d *distro.Distro, p pkg.Package, upstr } // find all vulnerability records in the DB for the given CPE (not including version comparisons) - allPkgVulns, err := store.GetByCPE(c) + vulns, err := store.FindVulnerabilities( + search.ByCPE(c), + onlyVulnerableTargets(p), + onlyQualifiedPackages(p), + onlyVulnerableVersions(verObj), + ) if err != nil { return nil, fmt.Errorf("matcher failed to fetch by CPE pkg=%q: %w", p.Name, err) } - applicableVulns, err := onlyQualifiedPackages(d, p, allPkgVulns) - if err != nil { - return nil, fmt.Errorf("unable to filter cpe-related vulnerabilities: %w", err) - } - - // TODO: Port this over to a qualifier and remove - applicableVulns, err = onlyVulnerableVersions(verObj, applicableVulns) - if err != nil { - return nil, fmt.Errorf("unable to filter cpe-related vulnerabilities: %w", err) - } - - applicableVulns = onlyVulnerableTargets(p, applicableVulns) - // for each vulnerability record found, check the version constraint. If the constraint is satisfied // relative to the current version information from the CPE (or the package) then the given package // is vulnerable. - for _, vuln := range applicableVulns { + for _, vuln := range vulns { addNewMatch(matchesByFingerprint, vuln, p, *verObj, upstreamMatcher, c) } } @@ -182,17 +125,17 @@ func addNewMatch(matchesByFingerprint map[match.Fingerprint]match.Match, vuln vu Type: match.CPEMatch, Confidence: 0.9, // TODO: this is hard coded for now Matcher: upstreamMatcher, - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: vuln.Namespace, CPEs: []string{ searchedByCPE.Attributes.BindToFmtString(), }, - Package: CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: p.Name, Version: p.Version, }, }, - Found: CPEResult{ + Found: match.CPEResult{ VulnerabilityID: vuln.ID, VersionConstraint: vuln.Constraint.String(), CPEs: cpesToString(filterCPEsByVersion(searchVersion, vuln.CPEs)), @@ -204,22 +147,22 @@ func addNewMatch(matchesByFingerprint map[match.Fingerprint]match.Match, vuln vu } func addMatchDetails(existingDetails []match.Detail, newDetails match.Detail) []match.Detail { - newFound, ok := newDetails.Found.(CPEResult) + newFound, ok := newDetails.Found.(match.CPEResult) if !ok { return existingDetails } - newSearchedBy, ok := newDetails.SearchedBy.(CPEParameters) + newSearchedBy, ok := newDetails.SearchedBy.(match.CPEParameters) if !ok { return existingDetails } for idx, detail := range existingDetails { - found, ok := detail.Found.(CPEResult) + found, ok := detail.Found.(match.CPEResult) if !ok { continue } - searchedBy, ok := detail.SearchedBy.(CPEParameters) + searchedBy, ok := detail.SearchedBy.(match.CPEParameters) if !ok { continue } diff --git a/grype/db/v5/search/cpe_test.go b/grype/matcher/internal/cpe_test.go similarity index 82% rename from grype/db/v5/search/cpe_test.go rename to grype/matcher/internal/cpe_test.go index 189a7d3fad11..ee39b79b84a0 100644 --- a/grype/db/v5/search/cpe_test.go +++ b/grype/matcher/internal/cpe_test.go @@ -1,4 +1,4 @@ -package search +package internal import ( "errors" @@ -9,150 +9,100 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - v5 "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/grype/vulnerability/mock" "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) -var _ v5.VulnerabilityStoreReader = (*mockVulnStore)(nil) - -type mockVulnStore struct { - data map[string]map[string][]v5.Vulnerability -} - -func (pr *mockVulnStore) GetVulnerability(namespace, id string) ([]v5.Vulnerability, error) { - //TODO implement me - panic("implement me") -} - -func newMockStore() *mockVulnStore { - pr := mockVulnStore{ - data: make(map[string]map[string][]v5.Vulnerability), - } - pr.stub() - return &pr -} - -func (pr *mockVulnStore) stub() { - pr.data["nvd:cpe"] = map[string][]v5.Vulnerability{ - "activerecord": { - { - PackageName: "activerecord", - VersionConstraint: "< 3.7.6", - VersionFormat: version.SemanticFormat.String(), - ID: "CVE-2017-fake-1", - CPEs: []string{ - "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", - }, +func newCPETestStore() vulnerability.Provider { + return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ + { + Reference: vulnerability.Reference{ + ID: "CVE-2017-fake-1", Namespace: "nvd:cpe", }, - { - PackageName: "activerecord", - VersionConstraint: "< 3.7.4", - VersionFormat: version.SemanticFormat.String(), - ID: "CVE-2017-fake-2", - CPEs: []string{ - "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*", - }, + PackageName: "activerecord", + Constraint: version.MustGetConstraint("< 3.7.6", version.SemanticFormat), + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", "")}, + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-2017-fake-2", Namespace: "nvd:cpe", }, - { - PackageName: "activerecord", - VersionConstraint: "= 4.0.1", - VersionFormat: version.GemFormat.String(), - ID: "CVE-2017-fake-3", - CPEs: []string{ - "cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*", - }, + PackageName: "activerecord", + Constraint: version.MustGetConstraint("< 3.7.4", version.SemanticFormat), + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*", "")}, + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-2017-fake-3", Namespace: "nvd:cpe", }, + PackageName: "activerecord", + Constraint: version.MustGetConstraint("= 4.0.1", version.GemFormat), + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*", "")}, }, - "awesome": { - { - PackageName: "awesome", - VersionConstraint: "< 98SP3", - VersionFormat: version.UnknownFormat.String(), - ID: "CVE-2017-fake-4", - CPEs: []string{ - "cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*", - }, + { + Reference: vulnerability.Reference{ + ID: "CVE-2017-fake-4", Namespace: "nvd:cpe", }, + PackageName: "awesome", + Constraint: version.MustGetConstraint("< 98SP3", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*", ""), + }, }, - "multiple": { - { - PackageName: "multiple", - VersionConstraint: "< 4.0", - VersionFormat: version.UnknownFormat.String(), - ID: "CVE-2017-fake-5", - CPEs: []string{ - "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", - "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", - "cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*", - "cpe:2.3:*:multiple:multiple:3.0:*:*:*:*:*:*:*", - }, + { + Reference: vulnerability.Reference{ + ID: "CVE-2017-fake-5", Namespace: "nvd:cpe", }, + PackageName: "multiple", + Constraint: version.MustGetConstraint("< 4.0", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", ""), + cpe.Must("cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", ""), + cpe.Must("cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*", ""), + cpe.Must("cpe:2.3:*:multiple:multiple:3.0:*:*:*:*:*:*:*", ""), + }, }, - "funfun": { - { - PackageName: "funfun", - VersionConstraint: "= 5.2.1", - VersionFormat: version.UnknownFormat.String(), - ID: "CVE-2017-fake-6", - CPEs: []string{ - "cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*", - "cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*", - }, + { + Reference: vulnerability.Reference{ + ID: "CVE-2017-fake-6", Namespace: "nvd:cpe", }, + PackageName: "funfun", + Constraint: version.MustGetConstraint("= 5.2.1", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*", ""), + cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*", ""), + }, }, - "sw": { - { - PackageName: "sw", - VersionConstraint: "< 1.0", - VersionFormat: version.UnknownFormat.String(), - ID: "CVE-2017-fake-7", - CPEs: []string{ - "cpe:2.3:*:sw:sw:*:*:*:*:*:puppet:*:*", - }, + { + Reference: vulnerability.Reference{ + ID: "CVE-2017-fake-7", Namespace: "nvd:cpe", }, + PackageName: "sw", + Constraint: version.MustGetConstraint("< 1.0", version.UnknownFormat), + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:*:sw:sw:*:*:*:*:*:puppet:*:*", "")}, }, - "handlebars": { - { - PackageName: "handlebars", - VersionConstraint: "< 4.7.7", - VersionFormat: version.UnknownFormat.String(), - ID: "CVE-2021-23369", - CPEs: []string{ - "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*", - }, + { + Reference: vulnerability.Reference{ + ID: "CVE-2021-23369", Namespace: "nvd:cpe", }, + PackageName: "handlebars", + Constraint: version.MustGetConstraint("< 4.7.7", version.UnknownFormat), + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*", "")}, }, - } -} - -func (pr *mockVulnStore) SearchForVulnerabilities(namespace, pkg string) ([]v5.Vulnerability, error) { - return pr.data[namespace][pkg], nil -} - -func (pr *mockVulnStore) GetAllVulnerabilities() (*[]v5.Vulnerability, error) { - return nil, nil -} - -func (pr *mockVulnStore) GetVulnerabilityNamespaces() ([]string, error) { - keys := make([]string, 0, len(pr.data)) - for k := range pr.data { - keys = append(keys, k) - } - - return keys, nil + }...) } func TestFindMatchesByPackageCPE(t *testing.T) { @@ -195,15 +145,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{"cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*"}, - Package: CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "activerecord", Version: "3.7.5", }, }, - Found: CPEResult{ + Found: match.CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"}, VersionConstraint: "< 3.7.6 (semver)", VulnerabilityID: "CVE-2017-fake-1", @@ -246,15 +196,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{"cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*"}, - Package: CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "activerecord", Version: "3.7.5", }, }, - Found: CPEResult{ + Found: match.CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"}, VersionConstraint: "< 3.7.6 (semver)", VulnerabilityID: "CVE-2017-fake-1", @@ -312,17 +262,17 @@ func TestFindMatchesByPackageCPE(t *testing.T) { { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*", }, Namespace: "nvd:cpe", - Package: CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "activerecord", Version: "3.7.3", }, }, - Found: CPEResult{ + Found: match.CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"}, VersionConstraint: "< 3.7.6 (semver)", VulnerabilityID: "CVE-2017-fake-1", @@ -351,15 +301,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:ra:*:ruby:*:*"}, Namespace: "nvd:cpe", - Package: CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "activerecord", Version: "3.7.3", }, }, - Found: CPEResult{ + Found: match.CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*"}, VersionConstraint: "< 3.7.4 (semver)", VulnerabilityID: "CVE-2017-fake-2", @@ -400,15 +350,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", - Package: CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "activerecord", Version: "4.0.1", }, }, - Found: CPEResult{ + Found: match.CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*"}, VersionConstraint: "= 4.0.1 (semver)", VulnerabilityID: "CVE-2017-fake-3", @@ -460,15 +410,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:*:awesome:awesome:98SE1:rando1:*:ra:*:dunno:*:*"}, Namespace: "nvd:cpe", - Package: CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "awesome", Version: "98SE1", }, }, - Found: CPEResult{ + Found: match.CPEResult{ CPEs: []string{"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*"}, VersionConstraint: "< 98SP3 (unknown)", VulnerabilityID: "CVE-2017-fake-4", @@ -510,15 +460,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", - Package: CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "multiple", Version: "1.0", }, }, - Found: CPEResult{ + Found: match.CPEResult{ CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", @@ -574,15 +524,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:*:sw:sw:0.1:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", - Package: CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "sw", Version: "0.1", }, }, - Found: CPEResult{ + Found: match.CPEResult{ CPEs: []string{ "cpe:2.3:*:sw:sw:*:*:*:*:*:puppet:*:*", }, @@ -630,15 +580,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*"}, Namespace: "nvd:cpe", - Package: CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "funfun", Version: "5.2.1", }, }, - Found: CPEResult{ + Found: match.CPEResult{ CPEs: []string{ "cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*", "cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*", @@ -681,15 +631,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", - Package: CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "handlebars", Version: "0.1", }, }, - Found: CPEResult{ + Found: match.CPEResult{ CPEs: []string{ "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*", }, @@ -731,15 +681,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", - Package: CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "handlebars", Version: "0.1", }, }, - Found: CPEResult{ + Found: match.CPEResult{ CPEs: []string{ "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*", }, @@ -781,15 +731,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", - Package: CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "handlebars", Version: "0.1", }, }, - Found: CPEResult{ + Found: match.CPEResult{ CPEs: []string{ "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*", }, @@ -844,15 +794,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", - Package: CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "handlebars", Version: "0.1", }, }, - Found: CPEResult{ + Found: match.CPEResult{ CPEs: []string{ "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*", }, @@ -869,17 +819,19 @@ func TestFindMatchesByPackageCPE(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - p, err := v5.NewVulnerabilityProvider(newMockStore()) - require.NoError(t, err) - actual, err := ByPackageCPE(p, nil, test.p, matcher) + actual, err := MatchPackageByCPEs(newCPETestStore(), test.p, matcher) if test.wantErr == nil { test.wantErr = require.NoError } test.wantErr(t, err) assertMatchesUsingIDsForVulnerabilities(t, test.expected, actual) for idx, e := range test.expected { - if d := cmp.Diff(e.Details, actual[idx].Details); d != "" { - t.Errorf("unexpected match details (-want +got):\n%s", d) + if idx < len(actual) { + if d := cmp.Diff(e.Details, actual[idx].Details); d != "" { + t.Errorf("unexpected match details (-want +got):\n%s", d) + } + } else { + t.Errorf("expected match details (-want +got)\n%+v:\n", e.Details) } } }) @@ -946,13 +898,13 @@ func TestAddMatchDetails(t *testing.T) { name: "append new entry -- found not equal", existing: []match.Detail{ { - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -961,13 +913,13 @@ func TestAddMatchDetails(t *testing.T) { }, }, new: match.Detail{ - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "totally-different-search", }, }, - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "totally-different-match", @@ -976,13 +928,13 @@ func TestAddMatchDetails(t *testing.T) { }, expected: []match.Detail{ { - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -990,13 +942,13 @@ func TestAddMatchDetails(t *testing.T) { }, }, { - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "totally-different-search", }, }, - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "totally-different-match", @@ -1009,13 +961,13 @@ func TestAddMatchDetails(t *testing.T) { name: "append new entry -- searchedBy merge fails", existing: []match.Detail{ { - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -1024,13 +976,13 @@ func TestAddMatchDetails(t *testing.T) { }, }, new: match.Detail{ - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "totally-different", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -1039,13 +991,13 @@ func TestAddMatchDetails(t *testing.T) { }, expected: []match.Detail{ { - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -1053,13 +1005,13 @@ func TestAddMatchDetails(t *testing.T) { }, }, { - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "totally-different", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -1072,13 +1024,13 @@ func TestAddMatchDetails(t *testing.T) { name: "merge with exiting entry", existing: []match.Detail{ { - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -1087,13 +1039,13 @@ func TestAddMatchDetails(t *testing.T) { }, }, new: match.Detail{ - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "totally-different-search", }, }, - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -1102,14 +1054,14 @@ func TestAddMatchDetails(t *testing.T) { }, expected: []match.Detail{ { - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", "totally-different-search", }, }, - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -1122,13 +1074,13 @@ func TestAddMatchDetails(t *testing.T) { name: "no addition - bad new searchedBy type", existing: []match.Detail{ { - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -1138,7 +1090,7 @@ func TestAddMatchDetails(t *testing.T) { }, new: match.Detail{ SearchedBy: "something else!", - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -1147,13 +1099,13 @@ func TestAddMatchDetails(t *testing.T) { }, expected: []match.Detail{ { - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -1166,13 +1118,13 @@ func TestAddMatchDetails(t *testing.T) { name: "no addition - bad new found type", existing: []match.Detail{ { - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -1181,7 +1133,7 @@ func TestAddMatchDetails(t *testing.T) { }, }, new: match.Detail{ - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", @@ -1191,13 +1143,13 @@ func TestAddMatchDetails(t *testing.T) { }, expected: []match.Detail{ { - SearchedBy: CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: CPEResult{ + Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -1218,19 +1170,19 @@ func TestAddMatchDetails(t *testing.T) { func TestCPESearchHit_Equals(t *testing.T) { tests := []struct { name string - current CPEResult - other CPEResult + current match.CPEResult + other match.CPEResult expected bool }{ { name: "different version constraint", - current: CPEResult{ + current: match.CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", }, }, - other: CPEResult{ + other: match.CPEResult{ VersionConstraint: "different-constraint", CPEs: []string{ "a-cpe", @@ -1240,13 +1192,13 @@ func TestCPESearchHit_Equals(t *testing.T) { }, { name: "different number of CPEs", - current: CPEResult{ + current: match.CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", }, }, - other: CPEResult{ + other: match.CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", @@ -1257,13 +1209,13 @@ func TestCPESearchHit_Equals(t *testing.T) { }, { name: "different CPE value", - current: CPEResult{ + current: match.CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", }, }, - other: CPEResult{ + other: match.CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "b-cpe", @@ -1273,13 +1225,13 @@ func TestCPESearchHit_Equals(t *testing.T) { }, { name: "matches", - current: CPEResult{ + current: match.CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", }, }, - other: CPEResult{ + other: match.CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", diff --git a/grype/db/v5/search/distro.go b/grype/matcher/internal/distro.go similarity index 58% rename from grype/db/v5/search/distro.go rename to grype/matcher/internal/distro.go index 575143320b64..dc9ffa0dee5f 100644 --- a/grype/db/v5/search/distro.go +++ b/grype/matcher/internal/distro.go @@ -1,55 +1,49 @@ -package search +package internal import ( "errors" "fmt" "strings" - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) -func ByPackageDistro(store v5.ProviderByDistro, d *distro.Distro, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) { - if d == nil { - return nil, nil +func MatchPackageByDistro(store vulnerability.Provider, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, []match.IgnoredMatch, error) { + if p.Distro == nil { + return nil, nil, nil } if isUnknownVersion(p.Version) { log.WithFields("package", p.Name).Trace("skipping package with unknown version") - return nil, nil + return nil, nil, nil } verObj, err := version.NewVersionFromPkg(p) if err != nil { if errors.Is(err, version.ErrUnsupportedVersion) { log.WithFields("error", err).Tracef("skipping package '%s@%s'", p.Name, p.Version) - return nil, nil + return nil, nil, nil } - return nil, fmt.Errorf("matcher failed to parse version pkg=%q ver=%q: %w", p.Name, p.Version, err) + return nil, nil, fmt.Errorf("matcher failed to parse version pkg=%q ver=%q: %w", p.Name, p.Version, err) } - allPkgVulns, err := store.GetByDistro(d, p) - if err != nil { - return nil, fmt.Errorf("matcher failed to fetch distro=%q pkg=%q: %w", d, p.Name, err) - } - - applicableVulns, err := onlyQualifiedPackages(d, p, allPkgVulns) - if err != nil { - return nil, fmt.Errorf("unable to filter distro-related vulnerabilities: %w", err) - } - - // TODO: Port this over to a qualifier and remove - applicableVulns, err = onlyVulnerableVersions(verObj, applicableVulns) + var matches []match.Match + vulns, err := store.FindVulnerabilities( + search.ByPackageName(p.Name), + search.ByDistro(*p.Distro), + onlyQualifiedPackages(p), + onlyVulnerableVersions(verObj), + ) if err != nil { - return nil, fmt.Errorf("unable to filter distro-related vulnerabilities: %w", err) + return nil, nil, fmt.Errorf("matcher failed to fetch distro=%q pkg=%q: %w", p.Distro, p.Name, err) } - var matches []match.Match - for _, vuln := range applicableVulns { + for _, vuln := range vulns { matches = append(matches, match.Match{ Vulnerability: vuln, Package: p, @@ -59,8 +53,8 @@ func ByPackageDistro(store v5.ProviderByDistro, d *distro.Distro, p pkg.Package, Matcher: upstreamMatcher, SearchedBy: map[string]interface{}{ "distro": map[string]string{ - "type": d.Type.String(), - "version": d.RawVersion, + "type": p.Distro.Type.String(), + "version": p.Distro.RawVersion, }, // why include the package information? The given package searched with may be a source package // for another package that is installed on the system. This makes it apparent exactly what @@ -80,8 +74,7 @@ func ByPackageDistro(store v5.ProviderByDistro, d *distro.Distro, p pkg.Package, }, }) } - - return matches, err + return matches, nil, err } func isUnknownVersion(v string) bool { diff --git a/grype/db/v5/search/distro_test.go b/grype/matcher/internal/distro_test.go similarity index 68% rename from grype/db/v5/search/distro_test.go rename to grype/matcher/internal/distro_test.go index 527d8463b36f..cca7f73f0062 100644 --- a/grype/db/v5/search/distro_test.go +++ b/grype/matcher/internal/distro_test.go @@ -1,7 +1,6 @@ -package search +package internal import ( - "strings" "testing" "github.com/google/uuid" @@ -13,50 +12,30 @@ import ( "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/grype/vulnerability/mock" syftPkg "github.com/anchore/syft/syft/pkg" ) -type mockDistroProvider struct { - data map[string]map[string][]vulnerability.Vulnerability -} - -func newMockProviderByDistro() *mockDistroProvider { - pr := mockDistroProvider{ - data: make(map[string]map[string][]vulnerability.Vulnerability), - } - pr.stub() - return &pr -} - -func (pr *mockDistroProvider) stub() { - pr.data["debian:8"] = map[string][]vulnerability.Vulnerability{ - // direct... - "neutron": { - { - Constraint: version.MustGetConstraint("< 2014.1.5-6", version.DebFormat), - Reference: vulnerability.Reference{ - ID: "CVE-2014-fake-1", - Namespace: "debian:8", - }, +func newMockProviderByDistro() vulnerability.Provider { + return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ + { + // direct... + PackageName: "neutron", + Constraint: version.MustGetConstraint("< 2014.1.5-6", version.DebFormat), + Reference: vulnerability.Reference{ + ID: "CVE-2014-fake-1", + Namespace: "secdb:distro:debian:8", }, }, - } - pr.data["sles:12.5"] = map[string][]vulnerability.Vulnerability{ - // direct... - "sles_test_package": { - { - Constraint: version.MustGetConstraint("< 2014.1.5-6", version.RpmFormat), - Reference: vulnerability.Reference{ - ID: "CVE-2014-fake-4", - Namespace: "sles:12.5", - }, + { + PackageName: "sles_test_package", + Constraint: version.MustGetConstraint("< 2014.1.5-6", version.RpmFormat), + Reference: vulnerability.Reference{ + ID: "CVE-2014-fake-4", + Namespace: "secdb:distro:sles:12.5", }, }, - } -} - -func (pr *mockDistroProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) { - return pr.data[strings.ToLower(d.Type.String())+":"+d.FullVersion()][p.Name], nil + }...) } func TestFindMatchesByPackageDistro(t *testing.T) { @@ -76,6 +55,7 @@ func TestFindMatchesByPackageDistro(t *testing.T) { if err != nil { t.Fatal("could not create distro: ", err) } + p.Distro = d expected := []match.Match{ { @@ -99,7 +79,7 @@ func TestFindMatchesByPackageDistro(t *testing.T) { "name": "neutron", "version": "2014.1.3-6", }, - "namespace": "debian:8", + "namespace": "secdb:distro:debian:8", }, Found: map[string]interface{}{ "versionConstraint": "< 2014.1.5-6 (deb)", @@ -112,16 +92,17 @@ func TestFindMatchesByPackageDistro(t *testing.T) { } store := newMockProviderByDistro() - actual, err := ByPackageDistro(store, d, p, match.PythonMatcher) + actual, ignored, err := MatchPackageByDistro(store, p, match.PythonMatcher) require.NoError(t, err) + require.Empty(t, ignored) assertMatchesUsingIDsForVulnerabilities(t, expected, actual) // prove we do not search for unknown versions p.Version = "unknown" - actual, err = ByPackageDistro(store, d, p, match.PythonMatcher) + actual, ignored, err = MatchPackageByDistro(store, p, match.PythonMatcher) require.NoError(t, err) + require.Empty(t, ignored) assert.Empty(t, actual) - } func TestFindMatchesByPackageDistroSles(t *testing.T) { @@ -141,6 +122,7 @@ func TestFindMatchesByPackageDistroSles(t *testing.T) { if err != nil { t.Fatal("could not create distro: ", err) } + p.Distro = d expected := []match.Match{ { @@ -164,7 +146,7 @@ func TestFindMatchesByPackageDistroSles(t *testing.T) { "name": "sles_test_package", "version": "2014.1.3-6", }, - "namespace": "sles:12.5", + "namespace": "secdb:distro:sles:12.5", }, Found: map[string]interface{}{ "versionConstraint": "< 2014.1.5-6 (rpm)", @@ -177,7 +159,8 @@ func TestFindMatchesByPackageDistroSles(t *testing.T) { } store := newMockProviderByDistro() - actual, err := ByPackageDistro(store, d, p, match.PythonMatcher) + actual, ignored, err := MatchPackageByDistro(store, p, match.PythonMatcher) assert.NoError(t, err) + require.Empty(t, ignored) assertMatchesUsingIDsForVulnerabilities(t, expected, actual) } diff --git a/grype/matcher/internal/language.go b/grype/matcher/internal/language.go new file mode 100644 index 000000000000..08fed8ecea0c --- /dev/null +++ b/grype/matcher/internal/language.go @@ -0,0 +1,83 @@ +package internal + +import ( + "errors" + "fmt" + + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/search" + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/log" +) + +func MatchPackageByLanguage(store vulnerability.Provider, p pkg.Package, matcherType match.MatcherType) ([]match.Match, []match.IgnoredMatch, error) { + var matches []match.Match + var ignored []match.IgnoredMatch + + for _, name := range PackageNames(p) { + nameMatches, nameIgnores, err := MatchPackageByLanguagePackageName(store, p, name, matcherType) + if err != nil { + return nil, nil, err + } + matches = append(matches, nameMatches...) + ignored = append(ignored, nameIgnores...) + } + + return matches, ignored, nil +} + +func MatchPackageByLanguagePackageName(store vulnerability.Provider, p pkg.Package, packageName string, matcherType match.MatcherType) ([]match.Match, []match.IgnoredMatch, error) { + if isUnknownVersion(p.Version) { + log.WithFields("package", p.Name).Trace("skipping package with unknown version") + return nil, nil, nil + } + + verObj, err := version.NewVersionFromPkg(p) + if err != nil { + if errors.Is(err, version.ErrUnsupportedVersion) { + log.WithFields("error", err).Tracef("skipping package '%s@%s'", p.Name, p.Version) + return nil, nil, nil + } + return nil, nil, fmt.Errorf("matcher failed to parse version pkg=%q ver=%q: %w", p.Name, p.Version, err) + } + + var matches []match.Match + vulns, err := store.FindVulnerabilities( + search.ByLanguage(p.Language), + search.ByPackageName(packageName), + onlyQualifiedPackages(p), + onlyVulnerableVersions(verObj), + ) + if err != nil { + return nil, nil, fmt.Errorf("matcher failed to fetch language=%q pkg=%q: %w", p.Language, p.Name, err) + } + + for _, vuln := range vulns { + matches = append(matches, match.Match{ + Vulnerability: vuln, + Package: p, + Details: []match.Detail{ + { + Type: match.ExactDirectMatch, + Confidence: 1.0, // TODO: this is hard coded for now + Matcher: matcherType, + SearchedBy: map[string]interface{}{ + "language": string(p.Language), + "namespace": vuln.Namespace, + "package": map[string]string{ + "name": p.Name, + "version": p.Version, + }, + }, + Found: map[string]interface{}{ + "vulnerabilityID": vuln.ID, + "versionConstraint": vuln.Constraint.String(), + }, + }, + }, + }) + } + return matches, nil, err +} diff --git a/grype/db/v5/search/language_test.go b/grype/matcher/internal/language_test.go similarity index 56% rename from grype/db/v5/search/language_test.go rename to grype/matcher/internal/language_test.go index 6204a49565ff..307ea73db1dd 100644 --- a/grype/db/v5/search/language_test.go +++ b/grype/matcher/internal/language_test.go @@ -1,7 +1,6 @@ -package search +package internal import ( - "fmt" "testing" "github.com/google/uuid" @@ -12,66 +11,47 @@ import ( "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/grype/vulnerability/mock" syftPkg "github.com/anchore/syft/syft/pkg" ) -type mockLanguageProvider struct { - data map[string]map[string][]vulnerability.Vulnerability -} - -func newMockProviderByLanguage() *mockLanguageProvider { - pr := mockLanguageProvider{ - data: make(map[string]map[string][]vulnerability.Vulnerability), - } - pr.stub() - return &pr -} - -func (pr *mockLanguageProvider) stub() { - pr.data["github:gem"] = map[string][]vulnerability.Vulnerability{ - // direct... - "activerecord": { - { - // make sure we find it with semVer constraint - Constraint: version.MustGetConstraint("< 3.7.6", version.SemanticFormat), - Reference: vulnerability.Reference{ - ID: "CVE-2017-fake-1", - Namespace: "github:ruby", - }, +func newMockProviderByLanguage() vulnerability.Provider { + return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ + { + Reference: vulnerability.Reference{ + ID: "CVE-2017-fake-1", + Namespace: "github:language:ruby", }, - { - Constraint: version.MustGetConstraint("< 3.7.4", version.GemFormat), - Reference: vulnerability.Reference{ - ID: "CVE-2017-fake-2", - Namespace: "github:ruby", - }, + PackageName: "activerecord", + // make sure we find it with semVer constraint + Constraint: version.MustGetConstraint("< 3.7.6", version.SemanticFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-2017-fake-2", + Namespace: "github:language:ruby", }, + PackageName: "activerecord", + Constraint: version.MustGetConstraint("< 3.7.4", version.GemFormat), }, - "nokogiri": { - { - // make sure we find it with gem version constraint - Constraint: version.MustGetConstraint("< 1.7.6", version.GemFormat), - Reference: vulnerability.Reference{ - ID: "CVE-2017-fake-1", - Namespace: "github:ruby", - }, + { + Reference: vulnerability.Reference{ + ID: "CVE-2017-fake-1", + Namespace: "github:language:ruby", }, - { - Constraint: version.MustGetConstraint("< 1.7.4", version.SemanticFormat), - Reference: vulnerability.Reference{ - ID: "CVE-2017-fake-2", - Namespace: "github:ruby", - }, + PackageName: "nokogiri", + // make sure we find it with gem version constraint + Constraint: version.MustGetConstraint("< 1.7.6", version.GemFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-2017-fake-2", + Namespace: "github:language:ruby", }, + PackageName: "nokogiri", + Constraint: version.MustGetConstraint("< 1.7.4", version.SemanticFormat), }, - } -} - -func (pr *mockLanguageProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]vulnerability.Vulnerability, error) { - if l != syftPkg.Ruby { - panic(fmt.Errorf("test mock only supports ruby")) - } - return pr.data["github:gem"][p.Name], nil + }...) } func expectedMatch(p pkg.Package, constraint string) []match.Match { @@ -89,7 +69,7 @@ func expectedMatch(p pkg.Package, constraint string) []match.Match { Confidence: 1, SearchedBy: map[string]interface{}{ "language": "ruby", - "namespace": "github:ruby", + "namespace": "github:language:ruby", "package": map[string]string{"name": p.Name, "version": p.Version}, }, Found: map[string]interface{}{ @@ -144,8 +124,9 @@ func TestFindMatchesByPackageLanguage(t *testing.T) { store := newMockProviderByLanguage() for _, c := range cases { t.Run(c.p.Name, func(t *testing.T) { - actual, err := ByPackageLanguage(store, nil, c.p, match.RubyGemMatcher) + actual, ignored, err := MatchPackageByLanguage(store, c.p, match.RubyGemMatcher) require.NoError(t, err) + require.Empty(t, ignored) if c.assertEmpty { assert.Empty(t, actual) return diff --git a/grype/matcher/internal/only_qualified_packages.go b/grype/matcher/internal/only_qualified_packages.go new file mode 100644 index 000000000000..fd7be30072d8 --- /dev/null +++ b/grype/matcher/internal/only_qualified_packages.go @@ -0,0 +1,23 @@ +package internal + +import ( + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/search" + "github.com/anchore/grype/grype/vulnerability" +) + +// onlyQualifiedPackages returns a criteria object that tests vulnerability qualifiers against the provided package +func onlyQualifiedPackages(p pkg.Package) vulnerability.Criteria { + return search.ByFunc(func(vuln vulnerability.Vulnerability) (bool, error) { + for _, qualifier := range vuln.PackageQualifiers { + satisfied, err := qualifier.Satisfied(p) + if err != nil { + return satisfied, err + } + if !satisfied { + return false, nil + } + } + return true, nil // all qualifiers passed + }) +} diff --git a/grype/db/v5/search/only_vulnerable_targets.go b/grype/matcher/internal/only_vulnerable_targets.go similarity index 66% rename from grype/db/v5/search/only_vulnerable_targets.go rename to grype/matcher/internal/only_vulnerable_targets.go index 54cc27b5d736..c6f31a667c32 100644 --- a/grype/db/v5/search/only_vulnerable_targets.go +++ b/grype/matcher/internal/only_vulnerable_targets.go @@ -1,9 +1,10 @@ -package search +package internal import ( "github.com/facebookincubator/nvdtools/wfn" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -33,18 +34,23 @@ func isUnknownTarget(targetSW string) bool { return true } -// Determines if a vulnerability is an accurate match using the vulnerability's cpes' target software -func onlyVulnerableTargets(p pkg.Package, allVulns []vulnerability.Vulnerability) []vulnerability.Vulnerability { - var vulns []vulnerability.Vulnerability +// onlyVulnerableTargets returns a criteria object that tests vulnerability qualifiers against the package vulnerability rules +func onlyVulnerableTargets(p pkg.Package) vulnerability.Criteria { + return search.ByFunc(func(v vulnerability.Vulnerability) (bool, error) { + return isVulnerableTarget(p, v), nil + }) +} +// Determines if a vulnerability is an accurate match using the vulnerability's cpes' target software +func isVulnerableTarget(p pkg.Package, vuln vulnerability.Vulnerability) bool { // Exclude OS package types from this logic, since they could be embedding any type of ecosystem package if isOSPackage(p) { - return allVulns + return true } // Do not filter by target software for any binary type packages since the composition is unknown if p.Type == syftPkg.BinaryPkg { - return allVulns + return true } // There are quite a few cases within java where other ecosystem components (particularly javascript packages) @@ -52,25 +58,16 @@ func onlyVulnerableTargets(p pkg.Package, allVulns []vulnerability.Vulnerability // of valid vulnerabilities that syft has specific logic https://github.com/anchore/syft/blob/main/syft/pkg/cataloger/common/cpe/candidate_by_package_type.go#L48-L75 // to ensure will be surfaced if p.Language == syftPkg.Java { - return allVulns + return true } - for _, vuln := range allVulns { - isPackageVulnerable := len(vuln.CPEs) == 0 - for _, cpe := range vuln.CPEs { - targetSW := cpe.Attributes.TargetSW - mismatchWithUnknownLanguage := syftPkg.LanguageByName(targetSW) != p.Language && isUnknownTarget(targetSW) - if targetSW == wfn.Any || targetSW == wfn.NA || syftPkg.LanguageByName(targetSW) == p.Language || mismatchWithUnknownLanguage { - isPackageVulnerable = true - } + isPackageVulnerable := len(vuln.CPEs) == 0 + for _, cpe := range vuln.CPEs { + targetSW := cpe.Attributes.TargetSW + mismatchWithUnknownLanguage := syftPkg.LanguageByName(targetSW) != p.Language && isUnknownTarget(targetSW) + if targetSW == wfn.Any || targetSW == wfn.NA || syftPkg.LanguageByName(targetSW) == p.Language || mismatchWithUnknownLanguage { + isPackageVulnerable = true } - - if !isPackageVulnerable { - continue - } - - vulns = append(vulns, vuln) } - - return vulns + return isPackageVulnerable } diff --git a/grype/db/v5/search/only_vulnerable_targets_test.go b/grype/matcher/internal/only_vulnerable_targets_test.go similarity index 97% rename from grype/db/v5/search/only_vulnerable_targets_test.go rename to grype/matcher/internal/only_vulnerable_targets_test.go index c115db133100..a210816ea828 100644 --- a/grype/db/v5/search/only_vulnerable_targets_test.go +++ b/grype/matcher/internal/only_vulnerable_targets_test.go @@ -1,4 +1,4 @@ -package search +package internal import ( "testing" diff --git a/grype/matcher/internal/only_vulnerable_versions.go b/grype/matcher/internal/only_vulnerable_versions.go new file mode 100644 index 000000000000..08d5761a856a --- /dev/null +++ b/grype/matcher/internal/only_vulnerable_versions.go @@ -0,0 +1,18 @@ +package internal + +import ( + "github.com/anchore/grype/grype/search" + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" +) + +// onlyVulnerableVersion returns a criteria object that tests affected vulnerability ranges against the provided version +func onlyVulnerableVersions(v *version.Version) vulnerability.Criteria { + if v == nil { + // if no version is provided, match everything + return search.ByFunc(func(_ vulnerability.Vulnerability) (bool, error) { + return true, nil + }) + } + return search.ByVersion(*v) +} diff --git a/grype/db/v5/search/utils_test.go b/grype/matcher/internal/utils_test.go similarity index 97% rename from grype/db/v5/search/utils_test.go rename to grype/matcher/internal/utils_test.go index 113567952dfb..30e5caf77dee 100644 --- a/grype/db/v5/search/utils_test.go +++ b/grype/matcher/internal/utils_test.go @@ -1,4 +1,4 @@ -package search +package internal import ( "testing" diff --git a/grype/db/v5/matcher/java/matcher.go b/grype/matcher/java/matcher.go similarity index 69% rename from grype/db/v5/matcher/java/matcher.go rename to grype/matcher/java/matcher.go index 0291b7208f7b..ee5d3f840505 100644 --- a/grype/db/v5/matcher/java/matcher.go +++ b/grype/matcher/java/matcher.go @@ -7,11 +7,10 @@ import ( "strings" "time" - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/search" - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -51,10 +50,11 @@ func (m *Matcher) Type() match.MatcherType { return match.JavaMatcher } -func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { var matches []match.Match + if m.cfg.SearchMavenUpstream { - upstreamMatches, err := m.matchUpstreamMavenPackages(store, d, p) + upstreamMatches, err := m.matchUpstreamMavenPackages(store, p) if err != nil { if strings.Contains(err.Error(), "no artifact found") { log.Debugf("no upstream maven artifact found for %s", p.Name) @@ -65,20 +65,18 @@ func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg. matches = append(matches, upstreamMatches...) } } - criteria := search.CommonCriteria - if m.cfg.UseCPEs { - criteria = append(criteria, search.ByCPE) - } - criteriaMatches, err := search.ByCriteria(store, d, p, m.Type(), criteria...) + + criteriaMatches, ignores, err := internal.MatchPackageByLanguageAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) if err != nil { - return nil, fmt.Errorf("failed to match by exact package: %w", err) + return nil, nil, fmt.Errorf("failed to match by exact package: %w", err) } matches = append(matches, criteriaMatches...) - return matches, nil + + return matches, ignores, nil } -func (m *Matcher) matchUpstreamMavenPackages(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { +func (m *Matcher) matchUpstreamMavenPackages(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) { var matches []match.Match ctx := context.Background() @@ -90,7 +88,7 @@ func (m *Matcher) matchUpstreamMavenPackages(store v5.VulnerabilityProvider, d * if err != nil { return nil, err } - indirectMatches, err := search.ByPackageLanguage(store, d, *indirectPackage, m.Type()) + indirectMatches, _, err := internal.MatchPackageByLanguage(store, *indirectPackage, m.Type()) if err != nil { return nil, err } diff --git a/grype/db/v5/matcher/java/matcher_integration_test.go b/grype/matcher/java/matcher_integration_test.go similarity index 100% rename from grype/db/v5/matcher/java/matcher_integration_test.go rename to grype/matcher/java/matcher_integration_test.go diff --git a/grype/matcher/java/matcher_mocks_test.go b/grype/matcher/java/matcher_mocks_test.go new file mode 100644 index 000000000000..85a8c88bc28e --- /dev/null +++ b/grype/matcher/java/matcher_mocks_test.go @@ -0,0 +1,46 @@ +package java + +import ( + "context" + + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/grype/vulnerability/mock" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +func newMockProvider() vulnerability.Provider { + return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ + { + PackageName: "org.springframework.spring-webmvc", + Constraint: version.MustGetConstraint(">=5.0.0,<5.1.7", version.UnknownFormat), + Reference: vulnerability.Reference{ID: "CVE-2014-fake-2", Namespace: "github:language:" + syftPkg.Java.String()}, + }, + { + PackageName: "org.springframework.spring-webmvc", + Constraint: version.MustGetConstraint(">=5.0.1,<5.1.7", version.UnknownFormat), + Reference: vulnerability.Reference{ID: "CVE-2013-fake-3", Namespace: "github:language:" + syftPkg.Java.String()}, + }, + // unexpected... + { + PackageName: "org.springframework.spring-webmvc", + Constraint: version.MustGetConstraint(">=5.0.0,<5.0.7", version.UnknownFormat), + Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD", Namespace: "github:language:" + syftPkg.Java.String()}, + }, + }...) +} + +type mockMavenSearcher struct { + pkg pkg.Package +} + +func (m mockMavenSearcher) GetMavenPackageBySha(context.Context, string) (*pkg.Package, error) { + return &m.pkg, nil +} + +func newMockSearcher(pkg pkg.Package) MavenSearcher { + return mockMavenSearcher{ + pkg, + } +} diff --git a/grype/db/v5/matcher/java/matcher_test.go b/grype/matcher/java/matcher_test.go similarity index 96% rename from grype/db/v5/matcher/java/matcher_test.go rename to grype/matcher/java/matcher_test.go index 649c3df229a7..3658c7565544 100644 --- a/grype/db/v5/matcher/java/matcher_test.go +++ b/grype/matcher/java/matcher_test.go @@ -39,7 +39,7 @@ func TestMatcherJava_matchUpstreamMavenPackage(t *testing.T) { MavenSearcher: newMockSearcher(p), } store := newMockProvider() - actual, _ := matcher.matchUpstreamMavenPackages(store, nil, p) + actual, _ := matcher.matchUpstreamMavenPackages(store, p) assert.Len(t, actual, 2, "unexpected matches count") diff --git a/grype/db/v5/matcher/java/maven_search.go b/grype/matcher/java/maven_search.go similarity index 100% rename from grype/db/v5/matcher/java/maven_search.go rename to grype/matcher/java/maven_search.go diff --git a/grype/db/v5/matcher/java/maven_test.go b/grype/matcher/java/maven_test.go similarity index 100% rename from grype/db/v5/matcher/java/maven_test.go rename to grype/matcher/java/maven_test.go diff --git a/grype/db/v5/matcher/javascript/matcher.go b/grype/matcher/javascript/matcher.go similarity index 55% rename from grype/db/v5/matcher/javascript/matcher.go rename to grype/matcher/javascript/matcher.go index 866429cf5542..6e37ab92b59e 100644 --- a/grype/db/v5/matcher/javascript/matcher.go +++ b/grype/matcher/javascript/matcher.go @@ -1,11 +1,10 @@ package javascript import ( - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/search" - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -31,10 +30,6 @@ func (m *Matcher) Type() match.MatcherType { return match.JavascriptMatcher } -func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { - criteria := search.CommonCriteria - if m.cfg.UseCPEs { - criteria = append(criteria, search.ByCPE) - } - return search.ByCriteria(store, d, p, m.Type(), criteria...) +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + return internal.MatchPackageByLanguageAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } diff --git a/grype/db/v5/matcher/matchers.go b/grype/matcher/matchers.go similarity index 50% rename from grype/db/v5/matcher/matchers.go rename to grype/matcher/matchers.go index 966bbb8c29ad..978f31ac5e46 100644 --- a/grype/db/v5/matcher/matchers.go +++ b/grype/matcher/matchers.go @@ -1,19 +1,20 @@ package matcher import ( - "github.com/anchore/grype/grype/db/v5/matcher/apk" - "github.com/anchore/grype/grype/db/v5/matcher/dotnet" - "github.com/anchore/grype/grype/db/v5/matcher/dpkg" - "github.com/anchore/grype/grype/db/v5/matcher/golang" - "github.com/anchore/grype/grype/db/v5/matcher/java" - "github.com/anchore/grype/grype/db/v5/matcher/javascript" - "github.com/anchore/grype/grype/db/v5/matcher/msrc" - "github.com/anchore/grype/grype/db/v5/matcher/portage" - "github.com/anchore/grype/grype/db/v5/matcher/python" - "github.com/anchore/grype/grype/db/v5/matcher/rpm" - "github.com/anchore/grype/grype/db/v5/matcher/ruby" - "github.com/anchore/grype/grype/db/v5/matcher/rust" - "github.com/anchore/grype/grype/db/v5/matcher/stock" + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/apk" + "github.com/anchore/grype/grype/matcher/dotnet" + "github.com/anchore/grype/grype/matcher/dpkg" + "github.com/anchore/grype/grype/matcher/golang" + "github.com/anchore/grype/grype/matcher/java" + "github.com/anchore/grype/grype/matcher/javascript" + "github.com/anchore/grype/grype/matcher/msrc" + "github.com/anchore/grype/grype/matcher/portage" + "github.com/anchore/grype/grype/matcher/python" + "github.com/anchore/grype/grype/matcher/rpm" + "github.com/anchore/grype/grype/matcher/ruby" + "github.com/anchore/grype/grype/matcher/rust" + "github.com/anchore/grype/grype/matcher/stock" ) // Config contains values used by individual matcher structs for advanced configuration @@ -28,8 +29,8 @@ type Config struct { Stock stock.MatcherConfig } -func NewDefaultMatchers(mc Config) []Matcher { - return []Matcher{ +func NewDefaultMatchers(mc Config) []match.Matcher { + return []match.Matcher{ &dpkg.Matcher{}, ruby.NewRubyMatcher(mc.Ruby), python.NewPythonMatcher(mc.Python), diff --git a/grype/db/v5/matcher/msrc/matcher.go b/grype/matcher/msrc/matcher.go similarity index 70% rename from grype/db/v5/matcher/msrc/matcher.go rename to grype/matcher/msrc/matcher.go index 6e84a2e9fe43..edb038b2619e 100644 --- a/grype/db/v5/matcher/msrc/matcher.go +++ b/grype/matcher/msrc/matcher.go @@ -1,11 +1,10 @@ package msrc import ( - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/search" - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -23,9 +22,9 @@ func (m *Matcher) Type() match.MatcherType { return match.MsrcMatcher } -func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { // find KB matches for the MSFT version given in the package and version. // The "distro" holds the information about the Windows version, and its // patch (KB) - return search.ByCriteria(store, d, p, m.Type(), search.ByDistro) + return internal.MatchPackageByDistro(store, p, m.Type()) } diff --git a/grype/matcher/msrc/matcher_test.go b/grype/matcher/msrc/matcher_test.go new file mode 100644 index 000000000000..0696dc807b2d --- /dev/null +++ b/grype/matcher/msrc/matcher_test.go @@ -0,0 +1,117 @@ +package msrc + +import ( + "fmt" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/anchore/grype/grype/distro" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/grype/vulnerability/mock" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +func TestMatches(t *testing.T) { + d, err := distro.New(distro.Windows, "10816", "Windows Server 2016") + require.NoError(t, err) + + // TODO: it would be ideal to test against something that constructs the namespace based on grype-db + // and not break the adaption of grype-db + msrcNamespace := fmt.Sprintf("msrc:distro:windows:%s", d.RawVersion) + + vp := mock.VulnerabilityProvider([]vulnerability.Vulnerability{ + { + Reference: vulnerability.Reference{ + ID: "CVE-2016-3333", + Namespace: msrcNamespace, + }, + PackageName: d.RawVersion, + Constraint: version.MustGetConstraint("3200970 || 878787 || base", version.KBFormat), + }, + { + Reference: vulnerability.Reference{ + // Does not match, version constraints do not apply + ID: "CVE-2020-made-up", + Namespace: msrcNamespace, + }, + PackageName: d.RawVersion, + Constraint: version.MustGetConstraint("778786 || 878787 || base", version.KBFormat), + }, + // Does not match the product ID + { + Reference: vulnerability.Reference{ + ID: "CVE-2020-also-made-up", + Namespace: msrcNamespace, + }, + PackageName: "something-else", + Constraint: version.MustGetConstraint("3200970 || 878787 || base", version.KBFormat), + }, + }...) + + tests := []struct { + name string + pkg pkg.Package + expectedVulnIDs []string + }{ + { + name: "direct KB match", + pkg: pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: d.RawVersion, + Version: "3200970", + Type: syftPkg.KbPkg, + Distro: d, + }, + expectedVulnIDs: []string{ + "CVE-2016-3333", + }, + }, + { + name: "multiple direct KB match", + pkg: pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: d.RawVersion, + Version: "878787", + Type: syftPkg.KbPkg, + Distro: d, + }, + expectedVulnIDs: []string{ + "CVE-2016-3333", + "CVE-2020-made-up", + }, + }, + { + name: "no KBs found", + pkg: pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: d.RawVersion, + // this is the assumed version if no KBs are found + Version: "base", + Type: syftPkg.KbPkg, + Distro: d, + }, + expectedVulnIDs: []string{ + "CVE-2016-3333", + "CVE-2020-made-up", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + m := Matcher{} + matches, _, err := m.Match(vp, test.pkg) + require.NoError(t, err) + var actualVulnIDs []string + for _, a := range matches { + actualVulnIDs = append(actualVulnIDs, a.Vulnerability.ID) + } + require.ElementsMatch(t, test.expectedVulnIDs, actualVulnIDs) + }) + } + +} diff --git a/grype/matcher/portage/matcher.go b/grype/matcher/portage/matcher.go new file mode 100644 index 000000000000..2126cd337c7e --- /dev/null +++ b/grype/matcher/portage/matcher.go @@ -0,0 +1,24 @@ +package portage + +import ( + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/internal" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +type Matcher struct { +} + +func (m *Matcher) PackageTypes() []syftPkg.Type { + return []syftPkg.Type{syftPkg.PortagePkg} +} + +func (m *Matcher) Type() match.MatcherType { + return match.PortageMatcher +} + +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + return internal.MatchPackageByDistro(store, p, m.Type()) +} diff --git a/grype/matcher/portage/matcher_mocks_test.go b/grype/matcher/portage/matcher_mocks_test.go new file mode 100644 index 000000000000..612729dc1071 --- /dev/null +++ b/grype/matcher/portage/matcher_mocks_test.go @@ -0,0 +1,23 @@ +package portage + +import ( + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/grype/vulnerability/mock" +) + +func newMockProvider() vulnerability.Provider { + return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ + // direct... + { + PackageName: "app-misc/neutron", + Constraint: version.MustGetConstraint("< 2014.1.3", version.PortageFormat), + Reference: vulnerability.Reference{ID: "CVE-2014-fake-1", Namespace: "secdb:distro:gentoo:"}, + }, + { + PackageName: "app-misc/neutron", + Constraint: version.MustGetConstraint("< 2014.1.4", version.PortageFormat), + Reference: vulnerability.Reference{ID: "CVE-2014-fake-2", Namespace: "secdb:distro:gentoo:"}, + }, + }...) +} diff --git a/grype/db/v5/matcher/portage/matcher_test.go b/grype/matcher/portage/matcher_test.go similarity index 95% rename from grype/db/v5/matcher/portage/matcher_test.go rename to grype/matcher/portage/matcher_test.go index 2c3c769a59f8..9814d9a526bb 100644 --- a/grype/db/v5/matcher/portage/matcher_test.go +++ b/grype/matcher/portage/matcher_test.go @@ -15,20 +15,22 @@ import ( func TestMatcherPortage_Match(t *testing.T) { matcher := Matcher{} + + d, err := distro.New(distro.Gentoo, "", "") + if err != nil { + t.Fatal("could not create distro: ", err) + } + p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "app-misc/neutron", Version: "2014.1.3", Type: syftPkg.PortagePkg, - } - - d, err := distro.New(distro.Gentoo, "", "") - if err != nil { - t.Fatal("could not create distro: ", err) + Distro: d, } store := newMockProvider() - actual, err := matcher.Match(store, d, p) + actual, _, err := matcher.Match(store, p) assert.NoError(t, err, "unexpected err from Match", err) assert.Len(t, actual, 1, "unexpected indirect matches count") diff --git a/grype/db/v5/matcher/python/matcher.go b/grype/matcher/python/matcher.go similarity index 55% rename from grype/db/v5/matcher/python/matcher.go rename to grype/matcher/python/matcher.go index e3b50cf3b138..7b1a3e8ae799 100644 --- a/grype/db/v5/matcher/python/matcher.go +++ b/grype/matcher/python/matcher.go @@ -1,11 +1,10 @@ package python import ( - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/search" - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -31,10 +30,6 @@ func (m *Matcher) Type() match.MatcherType { return match.PythonMatcher } -func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { - criteria := search.CommonCriteria - if m.cfg.UseCPEs { - criteria = append(criteria, search.ByCPE) - } - return search.ByCriteria(store, d, p, m.Type(), criteria...) +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + return internal.MatchPackageByLanguageAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } diff --git a/grype/db/v5/matcher/rpm/matcher.go b/grype/matcher/rpm/matcher.go similarity index 85% rename from grype/db/v5/matcher/rpm/matcher.go rename to grype/matcher/rpm/matcher.go index 9d2848bde161..59971faaa9ef 100644 --- a/grype/db/v5/matcher/rpm/matcher.go +++ b/grype/matcher/rpm/matcher.go @@ -4,11 +4,10 @@ import ( "fmt" "strings" - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/search" - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -24,7 +23,7 @@ func (m *Matcher) Type() match.MatcherType { } //nolint:funlen -func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { matches := make([]match.Match, 0) // let's match with a synthetic package that doesn't exist. We will create a new @@ -72,9 +71,9 @@ func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg. // really assume an epoch of 4 on the other side). This could still lead to // problems since an epoch delimits potentially non-comparable version lineages. - sourceMatches, err := m.matchUpstreamPackages(store, d, p) + sourceMatches, err := m.matchUpstreamPackages(store, p) if err != nil { - return nil, fmt.Errorf("failed to match by source indirection: %w", err) + return nil, nil, fmt.Errorf("failed to match by source indirection: %w", err) } matches = append(matches, sourceMatches...) @@ -94,21 +93,21 @@ func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg. // case). To do this we fill in missing epoch values in the package versions with // an explicit 0. - exactMatches, err := m.matchPackage(store, d, p) + exactMatches, err := m.matchPackage(store, p) if err != nil { - return nil, fmt.Errorf("failed to match by exact package name: %w", err) + return nil, nil, fmt.Errorf("failed to match by exact package name: %w", err) } matches = append(matches, exactMatches...) - return matches, nil + return matches, nil, nil } -func (m *Matcher) matchUpstreamPackages(store v5.ProviderByDistro, d *distro.Distro, p pkg.Package) ([]match.Match, error) { +func (m *Matcher) matchUpstreamPackages(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) { var matches []match.Match for _, indirectPackage := range pkg.UpstreamPackages(p) { - indirectMatches, err := search.ByPackageDistro(store, d, indirectPackage, m.Type()) + indirectMatches, _, err := internal.MatchPackageByDistro(store, indirectPackage, m.Type()) if err != nil { return nil, fmt.Errorf("failed to find vulnerabilities for rpm upstream source package: %w", err) } @@ -122,13 +121,13 @@ func (m *Matcher) matchUpstreamPackages(store v5.ProviderByDistro, d *distro.Dis return matches, nil } -func (m *Matcher) matchPackage(store v5.ProviderByDistro, d *distro.Distro, p pkg.Package) ([]match.Match, error) { +func (m *Matcher) matchPackage(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) { // we want to ensure that the version ALWAYS has an epoch specified... originalPkg := p addEpochIfApplicable(&p) - matches, err := search.ByPackageDistro(store, d, p, m.Type()) + matches, _, err := internal.MatchPackageByDistro(store, p, m.Type()) if err != nil { return nil, fmt.Errorf("failed to find vulnerabilities by dpkg source indirection: %w", err) } diff --git a/grype/matcher/rpm/matcher_mocks_test.go b/grype/matcher/rpm/matcher_mocks_test.go new file mode 100644 index 000000000000..781e580412a5 --- /dev/null +++ b/grype/matcher/rpm/matcher_mocks_test.go @@ -0,0 +1,112 @@ +package rpm + +import ( + "github.com/anchore/grype/grype/pkg/qualifier" + "github.com/anchore/grype/grype/pkg/qualifier/rpmmodularity" + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/grype/vulnerability/mock" +) + +func newMockProvider(packageName, indirectName string, withEpoch bool, withPackageQualifiers bool) vulnerability.Provider { + if withEpoch { + return mock.VulnerabilityProvider(vulnerabilitiesWithEpoch(packageName, indirectName)...) + } else if withPackageQualifiers { + return mock.VulnerabilityProvider(vulnerabilitiesWithPackageQualifiers(packageName)...) + } + return mock.VulnerabilityProvider(vulnerabilitiesDefaults(packageName, indirectName)...) +} + +const namespace = "secdb:distro:centos:8" + +func vulnerabilitiesDefaults(packageName, indirectName string) []vulnerability.Vulnerability { + return []vulnerability.Vulnerability{ + // direct... + { + PackageName: packageName, + Constraint: version.MustGetConstraint("<= 7.1.3-6", version.RpmFormat), + Reference: vulnerability.Reference{ID: "CVE-2014-fake-1", Namespace: namespace}, + }, + // indirect... + // expected... + { + PackageName: indirectName, + Constraint: version.MustGetConstraint("< 7.1.4-5", version.RpmFormat), + Reference: vulnerability.Reference{ID: "CVE-2014-fake-2", Namespace: namespace}, + }, + { + PackageName: indirectName, + Constraint: version.MustGetConstraint("< 8.0.2-0", version.RpmFormat), + Reference: vulnerability.Reference{ID: "CVE-2013-fake-3", Namespace: namespace}, + }, + // unexpected... + { + PackageName: indirectName, + Constraint: version.MustGetConstraint("< 7.0.4-1", version.RpmFormat), + Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD", Namespace: namespace}, + }, + } +} + +func vulnerabilitiesWithEpoch(packageName, indirectName string) []vulnerability.Vulnerability { + return []vulnerability.Vulnerability{ + // direct... + { + PackageName: packageName, + Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat), + Reference: vulnerability.Reference{ID: "CVE-2021-1", Namespace: namespace}, + }, + { + PackageName: packageName, + Constraint: version.MustGetConstraint("<= 0:2.28-419.el8.", version.RpmFormat), + Reference: vulnerability.Reference{ID: "CVE-2021-2", Namespace: namespace}, + }, + // indirect... + { + PackageName: indirectName, + Constraint: version.MustGetConstraint("< 5.28.3-420.el8", version.RpmFormat), + Reference: vulnerability.Reference{ID: "CVE-2021-3", Namespace: namespace}, + }, + // unexpected... + { + PackageName: indirectName, + Constraint: version.MustGetConstraint("< 4:5.26.3-419.el8", version.RpmFormat), + Reference: vulnerability.Reference{ID: "CVE-2021-4", Namespace: namespace}, + }, + } +} + +func vulnerabilitiesWithPackageQualifiers(packageName string) []vulnerability.Vulnerability { + return []vulnerability.Vulnerability{ + // direct... + { + PackageName: packageName, + Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat), + Reference: vulnerability.Reference{ID: "CVE-2021-1", Namespace: namespace}, + PackageQualifiers: []qualifier.Qualifier{ + rpmmodularity.New("containertools:3"), + }, + }, + { + PackageName: packageName, + Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat), + Reference: vulnerability.Reference{ID: "CVE-2021-2", Namespace: namespace}, + PackageQualifiers: []qualifier.Qualifier{ + rpmmodularity.New(""), + }, + }, + { + PackageName: packageName, + Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat), + Reference: vulnerability.Reference{ID: "CVE-2021-3", Namespace: namespace}, + }, + { + PackageName: packageName, + Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat), + Reference: vulnerability.Reference{ID: "CVE-2021-4", Namespace: namespace}, + PackageQualifiers: []qualifier.Qualifier{ + rpmmodularity.New("containertools:4"), + }, + }, + } +} diff --git a/grype/db/v5/matcher/rpm/matcher_test.go b/grype/matcher/rpm/matcher_test.go similarity index 91% rename from grype/db/v5/matcher/rpm/matcher_test.go rename to grype/matcher/rpm/matcher_test.go index 6154770d592b..568ae6f8af6c 100644 --- a/grype/db/v5/matcher/rpm/matcher_test.go +++ b/grype/matcher/rpm/matcher_test.go @@ -7,10 +7,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - v5 "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -22,7 +22,7 @@ func TestMatcherRpm(t *testing.T) { tests := []struct { name string p pkg.Package - setup func() (v5.VulnerabilityProvider, *distro.Distro, Matcher) + setup func() (vulnerability.Provider, *distro.Distro, Matcher) expectedMatches map[string]match.Type wantErr bool }{ @@ -40,7 +40,7 @@ func TestMatcherRpm(t *testing.T) { }, }, }, - setup: func() (v5.VulnerabilityProvider, *distro.Distro, Matcher) { + setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d, err := distro.New(distro.CentOS, "8", "") if err != nil { @@ -71,7 +71,7 @@ func TestMatcherRpm(t *testing.T) { }, }, }, - setup: func() (v5.VulnerabilityProvider, *distro.Distro, Matcher) { + setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d, err := distro.New(distro.CentOS, "8", "") if err != nil { @@ -101,7 +101,7 @@ func TestMatcherRpm(t *testing.T) { }, }, }, - setup: func() (v5.VulnerabilityProvider, *distro.Distro, Matcher) { + setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d, err := distro.New(distro.CentOS, "8", "") if err != nil { @@ -135,7 +135,7 @@ func TestMatcherRpm(t *testing.T) { }, }, }, - setup: func() (v5.VulnerabilityProvider, *distro.Distro, Matcher) { + setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d, err := distro.New(distro.CentOS, "8", "") if err != nil { @@ -160,7 +160,7 @@ func TestMatcherRpm(t *testing.T) { Type: syftPkg.RpmPkg, Metadata: pkg.RpmMetadata{}, }, - setup: func() (v5.VulnerabilityProvider, *distro.Distro, Matcher) { + setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d, err := distro.New(distro.CentOS, "8", "") if err != nil { @@ -184,7 +184,7 @@ func TestMatcherRpm(t *testing.T) { Type: syftPkg.RpmPkg, Metadata: pkg.RpmMetadata{}, }, - setup: func() (v5.VulnerabilityProvider, *distro.Distro, Matcher) { + setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d, err := distro.New(distro.CentOS, "8", "") if err != nil { @@ -208,7 +208,7 @@ func TestMatcherRpm(t *testing.T) { Type: syftPkg.RpmPkg, Metadata: pkg.RpmMetadata{}, }, - setup: func() (v5.VulnerabilityProvider, *distro.Distro, Matcher) { + setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d, err := distro.New(distro.CentOS, "8", "") if err != nil { @@ -232,7 +232,7 @@ func TestMatcherRpm(t *testing.T) { Type: syftPkg.RpmPkg, Metadata: pkg.RpmMetadata{}, }, - setup: func() (v5.VulnerabilityProvider, *distro.Distro, Matcher) { + setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d, err := distro.New(distro.CentOS, "8", "") if err != nil { @@ -256,7 +256,7 @@ func TestMatcherRpm(t *testing.T) { ModularityLabel: strRef("containertools:3:1234:5678"), }, }, - setup: func() (v5.VulnerabilityProvider, *distro.Distro, Matcher) { + setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d, err := distro.New(distro.CentOS, "8", "") if err != nil { @@ -283,7 +283,7 @@ func TestMatcherRpm(t *testing.T) { ModularityLabel: strRef("containertools:1:abc:123"), }, }, - setup: func() (v5.VulnerabilityProvider, *distro.Distro, Matcher) { + setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d, err := distro.New(distro.CentOS, "8", "") if err != nil { @@ -306,7 +306,7 @@ func TestMatcherRpm(t *testing.T) { Version: "0.1", Type: syftPkg.RpmPkg, }, - setup: func() (v5.VulnerabilityProvider, *distro.Distro, Matcher) { + setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d, err := distro.New(distro.CentOS, "8", "") if err != nil { @@ -329,7 +329,10 @@ func TestMatcherRpm(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { store, d, matcher := test.setup() - actual, err := matcher.Match(store, d, test.p) + if test.p.Distro == nil { + test.p.Distro = d + } + actual, _, err := matcher.Match(store, test.p) if err != nil { t.Fatal("could not find match: ", err) } diff --git a/grype/db/v5/matcher/ruby/matcher.go b/grype/matcher/ruby/matcher.go similarity index 54% rename from grype/db/v5/matcher/ruby/matcher.go rename to grype/matcher/ruby/matcher.go index ee7b336932a6..1ffce4285385 100644 --- a/grype/db/v5/matcher/ruby/matcher.go +++ b/grype/matcher/ruby/matcher.go @@ -1,11 +1,10 @@ package ruby import ( - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/search" - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -31,10 +30,6 @@ func (m *Matcher) Type() match.MatcherType { return match.RubyGemMatcher } -func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { - criteria := search.CommonCriteria - if m.cfg.UseCPEs { - criteria = append(criteria, search.ByCPE) - } - return search.ByCriteria(store, d, p, m.Type(), criteria...) +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + return internal.MatchPackageByLanguageAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } diff --git a/grype/db/v5/matcher/rust/matcher.go b/grype/matcher/rust/matcher.go similarity index 54% rename from grype/db/v5/matcher/rust/matcher.go rename to grype/matcher/rust/matcher.go index 59204df49f52..a9b570765e28 100644 --- a/grype/db/v5/matcher/rust/matcher.go +++ b/grype/matcher/rust/matcher.go @@ -1,11 +1,10 @@ package rust import ( - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/search" - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -31,10 +30,6 @@ func (m *Matcher) Type() match.MatcherType { return match.RustMatcher } -func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { - criteria := search.CommonCriteria - if m.cfg.UseCPEs { - criteria = append(criteria, search.ByCPE) - } - return search.ByCriteria(store, d, p, m.Type(), criteria...) +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + return internal.MatchPackageByLanguageAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } diff --git a/grype/matcher/stock/matcher.go b/grype/matcher/stock/matcher.go new file mode 100644 index 000000000000..07641ee89fc1 --- /dev/null +++ b/grype/matcher/stock/matcher.go @@ -0,0 +1,35 @@ +package stock + +import ( + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/internal" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +type Matcher struct { + cfg MatcherConfig +} + +type MatcherConfig struct { + UseCPEs bool +} + +func NewStockMatcher(cfg MatcherConfig) match.Matcher { + return &Matcher{ + cfg: cfg, + } +} + +func (m *Matcher) PackageTypes() []syftPkg.Type { + return nil +} + +func (m *Matcher) Type() match.MatcherType { + return match.StockMatcher +} + +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + return internal.MatchPackageByLanguageAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) +} diff --git a/grype/matcher/stock/matcher_test.go b/grype/matcher/stock/matcher_test.go new file mode 100644 index 000000000000..f57f959776a6 --- /dev/null +++ b/grype/matcher/stock/matcher_test.go @@ -0,0 +1,127 @@ +package stock + +import ( + "testing" + + "github.com/google/uuid" + "github.com/scylladb/go-set/strset" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/grype/vulnerability/mock" + "github.com/anchore/syft/syft/cpe" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +func TestMatcher_JVMPackage(t *testing.T) { + p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "java_se", + Version: "1.8.0_400", + Type: syftPkg.BinaryPkg, + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:a:oracle:java_se:1.8.0:update400:*:*:*:*:*:*", cpe.DeclaredSource), + }, + } + matcher := Matcher{ + cfg: MatcherConfig{ + UseCPEs: true, + }, + } + store := newMockProvider() + actual, _, err := matcher.Match(store, p) + require.NoError(t, err) + + foundCVEs := strset.New() + for _, v := range actual { + foundCVEs.Add(v.Vulnerability.ID) + + require.NotEmpty(t, v.Details) + for _, d := range v.Details { + assert.Equal(t, match.CPEMatch, d.Type, "indirect match not indicated") + assert.Equal(t, matcher.Type(), d.Matcher, "failed to capture matcher type") + } + assert.Equal(t, p.Name, v.Package.Name, "failed to capture original package name") + } + + expected := strset.New( + "CVE-2024-20919-real", + "CVE-2024-20919-bonkers-format", + "CVE-2024-20919-post-jep223", + ) + + for _, id := range expected.List() { + if !foundCVEs.Has(id) { + t.Errorf("missing CVE: %s", id) + } + } + + extra := strset.Difference(foundCVEs, expected) + + for _, id := range extra.List() { + t.Errorf("unexpected CVE: %s", id) + } + + if t.Failed() { + t.Logf("discovered CVES: %d", foundCVEs.Size()) + for _, id := range foundCVEs.List() { + t.Logf(" - %s", id) + } + } +} + +func newMockProvider() vulnerability.Provider { + // derived from vuln data found on CVE-2024-20919 + hit := "< 1.8.0_401 || >= 1.9-ea, < 8.0.401 || >= 9-ea, < 11.0.22 || >= 12-ea, < 17.0.10 || >= 18-ea, < 21.0.2" + + cpes := []cpe.CPE{cpe.Must("cpe:2.3:a:oracle:java_se:*:*:*:*:*:*:*:*", "")} + + return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ + { + // positive cases + PackageName: "java_se", + Constraint: version.MustGetConstraint(hit, version.JVMFormat), + Reference: vulnerability.Reference{ID: "CVE-2024-20919-real", Namespace: "nvd:cpe"}, + CPEs: cpes, + }, + { + // positive cases + PackageName: "java_se", + Constraint: version.MustGetConstraint("< 22.22.22", version.UnknownFormat), + Reference: vulnerability.Reference{ID: "CVE-2024-20919-bonkers-format", Namespace: "nvd:cpe"}, + CPEs: cpes, + }, + { + // negative case + PackageName: "java_se", + Constraint: version.MustGetConstraint("< 1.8.0_399 || >= 1.9-ea, < 8.0.399 || >= 9-ea", version.JVMFormat), + Reference: vulnerability.Reference{ID: "CVE-FAKE-bad-update", Namespace: "nvd:cpe"}, + CPEs: cpes, + }, + { + // positive case + PackageName: "java_se", + Constraint: version.MustGetConstraint("< 8.0.401", version.JVMFormat), + Reference: vulnerability.Reference{ID: "CVE-2024-20919-post-jep223", Namespace: "nvd:cpe"}, + CPEs: cpes, + }, + { + // negative case + PackageName: "java_se", + Constraint: version.MustGetConstraint("< 8.0.399", version.JVMFormat), + Reference: vulnerability.Reference{ID: "CVE-FAKE-bad-range-post-jep223", Namespace: "nvd:cpe"}, + CPEs: cpes, + }, + { + // negative case + PackageName: "java_se", + Constraint: version.MustGetConstraint("< 7.0.0", version.JVMFormat), + Reference: vulnerability.Reference{ID: "CVE-FAKE-bad-range-post-jep223", Namespace: "nvd:cpe"}, + CPEs: cpes, + }, + }...) +} diff --git a/grype/pkg/java_metadata.go b/grype/pkg/java_metadata.go index 88d820145887..328c53bdf2b6 100644 --- a/grype/pkg/java_metadata.go +++ b/grype/pkg/java_metadata.go @@ -3,7 +3,7 @@ package pkg import ( "github.com/scylladb/go-set/strset" - "github.com/anchore/syft/syft/pkg" + syftPkg "github.com/anchore/syft/syft/pkg" ) type JavaMetadata struct { @@ -35,7 +35,7 @@ func IsJvmPackage(p Package) bool { return true } - if p.Type == pkg.BinaryPkg { + if p.Type == syftPkg.BinaryPkg { if HasJvmPackageName(p.Name) { return true } diff --git a/grype/pkg/package.go b/grype/pkg/package.go index bf71d69522e2..5d3a0594f44f 100644 --- a/grype/pkg/package.go +++ b/grype/pkg/package.go @@ -5,13 +5,14 @@ import ( "regexp" "strings" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/stringutil" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/linux" - "github.com/anchore/syft/syft/pkg" + syftPkg "github.com/anchore/syft/syft/pkg" cpes "github.com/anchore/syft/syft/pkg/cataloger/common/cpe" ) @@ -33,16 +34,17 @@ type Package struct { Name string // the package name Version string // the version of the package Locations file.LocationSet // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package) - Language pkg.Language // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc) + Language syftPkg.Language // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc) + Distro *distro.Distro // a specific distro this package originated from Licenses []string - Type pkg.Type // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc) - CPEs []cpe.CPE // all possible Common Platform Enumerators - PURL string // the Package URL (see https://github.com/package-url/purl-spec) + Type syftPkg.Type // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc) + CPEs []cpe.CPE // all possible Common Platform Enumerators + PURL string // the Package URL (see https://github.com/package-url/purl-spec) Upstreams []UpstreamPackage Metadata interface{} // This is NOT 1-for-1 the syft metadata! Only the select data needed for vulnerability matching } -func New(p pkg.Package) Package { +func New(p syftPkg.Package) Package { metadata, upstreams := dataFromPkg(p) licenseObjs := p.Licenses.ToSlice() @@ -70,11 +72,11 @@ func New(p pkg.Package) Package { } } -func FromCollection(catalog *pkg.Collection, config SynthesisConfig) []Package { +func FromCollection(catalog *syftPkg.Collection, config SynthesisConfig) []Package { return FromPackages(catalog.Sorted(), config) } -func FromPackages(syftpkgs []pkg.Package, config SynthesisConfig) []Package { +func FromPackages(syftpkgs []syftPkg.Package, config SynthesisConfig) []Package { var pkgs []Package for _, p := range syftpkgs { if len(p.CPEs) == 0 { @@ -96,7 +98,7 @@ func (p Package) String() string { return fmt.Sprintf("Pkg(type=%s, name=%s, version=%s, upstreams=%d)", p.Type, p.Name, p.Version, len(p.Upstreams)) } -func removePackagesByOverlap(catalog *pkg.Collection, relationships []artifact.Relationship, distro *linux.Release) *pkg.Collection { +func removePackagesByOverlap(catalog *syftPkg.Collection, relationships []artifact.Relationship, distro *linux.Release) *syftPkg.Collection { byOverlap := map[artifact.ID]artifact.Relationship{} for _, r := range relationships { if r.Type == artifact.OwnershipByFileOverlapRelationship { @@ -104,7 +106,7 @@ func removePackagesByOverlap(catalog *pkg.Collection, relationships []artifact.R } } - out := pkg.NewCollection() + out := syftPkg.NewCollection() comprehensiveDistroFeed := distroFeedIsComprehensive(distro) for p := range catalog.Enumerate() { r, ok := byOverlap[p.ID()] @@ -120,7 +122,7 @@ func removePackagesByOverlap(catalog *pkg.Collection, relationships []artifact.R return out } -func excludePackage(comprehensiveDistroFeed bool, p pkg.Package, parent pkg.Package) bool { +func excludePackage(comprehensiveDistroFeed bool, p syftPkg.Package, parent syftPkg.Package) bool { // NOTE: we are not checking the name because we have mismatches like: // python 3.9.2 binary // python3.9 3.9.2-1 deb @@ -139,7 +141,7 @@ func excludePackage(comprehensiveDistroFeed bool, p pkg.Package, parent pkg.Pack } // filter out binary packages, even for non-comprehensive distros - if p.Type != pkg.BinaryPkg { + if p.Type != syftPkg.BinaryPkg { return false } @@ -184,45 +186,45 @@ var comprehensiveDistros = []string{ "ubuntu", } -func isOSPackage(p pkg.Package) bool { +func isOSPackage(p syftPkg.Package) bool { switch p.Type { - case pkg.DebPkg, pkg.RpmPkg, pkg.PortagePkg, pkg.AlpmPkg, pkg.ApkPkg: + case syftPkg.DebPkg, syftPkg.RpmPkg, syftPkg.PortagePkg, syftPkg.AlpmPkg, syftPkg.ApkPkg: return true default: return false } } -func dataFromPkg(p pkg.Package) (interface{}, []UpstreamPackage) { +func dataFromPkg(p syftPkg.Package) (interface{}, []UpstreamPackage) { var metadata interface{} var upstreams []UpstreamPackage switch p.Metadata.(type) { - case pkg.GolangModuleEntry, pkg.GolangBinaryBuildinfoEntry: + case syftPkg.GolangModuleEntry, syftPkg.GolangBinaryBuildinfoEntry: metadata = golangMetadataFromPkg(p) - case pkg.DpkgDBEntry: + case syftPkg.DpkgDBEntry: upstreams = dpkgDataFromPkg(p) - case pkg.RpmArchive, pkg.RpmDBEntry: + case syftPkg.RpmArchive, syftPkg.RpmDBEntry: m, u := rpmDataFromPkg(p) upstreams = u if m != nil { metadata = *m } - case pkg.JavaArchive: + case syftPkg.JavaArchive: if m := javaDataFromPkg(p); m != nil { metadata = *m } - case pkg.ApkDBEntry: + case syftPkg.ApkDBEntry: metadata = apkMetadataFromPkg(p) upstreams = apkDataFromPkg(p) - case pkg.JavaVMInstallation: + case syftPkg.JavaVMInstallation: metadata = javaVMDataFromPkg(p) } return metadata, upstreams } -func javaVMDataFromPkg(p pkg.Package) any { - if value, ok := p.Metadata.(pkg.JavaVMInstallation); ok { +func javaVMDataFromPkg(p syftPkg.Package) any { + if value, ok := p.Metadata.(syftPkg.JavaVMInstallation); ok { return JavaVMInstallationMetadata{ Release: JavaVMReleaseMetadata{ JavaRuntimeVersion: value.Release.JavaRuntimeVersion, @@ -236,8 +238,8 @@ func javaVMDataFromPkg(p pkg.Package) any { return nil } -func apkMetadataFromPkg(p pkg.Package) interface{} { - if m, ok := p.Metadata.(pkg.ApkDBEntry); ok { +func apkMetadataFromPkg(p syftPkg.Package) interface{} { + if m, ok := p.Metadata.(syftPkg.ApkDBEntry); ok { metadata := ApkMetadata{} fileRecords := make([]ApkFileRecord, 0, len(m.Files)) @@ -254,9 +256,9 @@ func apkMetadataFromPkg(p pkg.Package) interface{} { return nil } -func golangMetadataFromPkg(p pkg.Package) interface{} { +func golangMetadataFromPkg(p syftPkg.Package) interface{} { switch value := p.Metadata.(type) { - case pkg.GolangBinaryBuildinfoEntry: + case syftPkg.GolangBinaryBuildinfoEntry: metadata := GolangBinMetadata{} if value.BuildSettings != nil { metadata.BuildSettings = value.BuildSettings @@ -266,7 +268,7 @@ func golangMetadataFromPkg(p pkg.Package) interface{} { metadata.H1Digest = value.H1Digest metadata.MainModule = value.MainModule return metadata - case pkg.GolangModuleEntry: + case syftPkg.GolangModuleEntry: metadata := GolangModMetadata{} metadata.H1Digest = value.H1Digest return metadata @@ -274,8 +276,8 @@ func golangMetadataFromPkg(p pkg.Package) interface{} { return nil } -func dpkgDataFromPkg(p pkg.Package) (upstreams []UpstreamPackage) { - if value, ok := p.Metadata.(pkg.DpkgDBEntry); ok { +func dpkgDataFromPkg(p syftPkg.Package) (upstreams []UpstreamPackage) { + if value, ok := p.Metadata.(syftPkg.DpkgDBEntry); ok { if value.Source != "" { upstreams = append(upstreams, UpstreamPackage{ Name: value.Source, @@ -288,9 +290,9 @@ func dpkgDataFromPkg(p pkg.Package) (upstreams []UpstreamPackage) { return upstreams } -func rpmDataFromPkg(p pkg.Package) (metadata *RpmMetadata, upstreams []UpstreamPackage) { +func rpmDataFromPkg(p syftPkg.Package) (metadata *RpmMetadata, upstreams []UpstreamPackage) { switch m := p.Metadata.(type) { - case pkg.RpmDBEntry: + case syftPkg.RpmDBEntry: if m.SourceRpm != "" { upstreams = handleSourceRPM(p.Name, m.SourceRpm) } @@ -299,7 +301,7 @@ func rpmDataFromPkg(p pkg.Package) (metadata *RpmMetadata, upstreams []UpstreamP Epoch: m.Epoch, ModularityLabel: m.ModularityLabel, } - case pkg.RpmArchive: + case syftPkg.RpmArchive: if m.SourceRpm != "" { upstreams = handleSourceRPM(p.Name, m.SourceRpm) } @@ -337,8 +339,8 @@ func getNameAndELVersion(sourceRpm string) (string, string) { return groupMatches["name"], version } -func javaDataFromPkg(p pkg.Package) (metadata *JavaMetadata) { - if value, ok := p.Metadata.(pkg.JavaArchive); ok { +func javaDataFromPkg(p syftPkg.Package) (metadata *JavaMetadata) { + if value, ok := p.Metadata.(syftPkg.JavaArchive); ok { var artifactID, groupID, name string if value.PomProperties != nil { artifactID = value.PomProperties.ArtifactID @@ -375,8 +377,8 @@ func javaDataFromPkg(p pkg.Package) (metadata *JavaMetadata) { return metadata } -func apkDataFromPkg(p pkg.Package) (upstreams []UpstreamPackage) { - if value, ok := p.Metadata.(pkg.ApkDBEntry); ok { +func apkDataFromPkg(p syftPkg.Package) (upstreams []UpstreamPackage) { + if value, ok := p.Metadata.(syftPkg.ApkDBEntry); ok { if value.OriginPackage != "" { upstreams = append(upstreams, UpstreamPackage{ Name: value.OriginPackage, diff --git a/grype/pkg/qualifier/platformcpe/qualifier.go b/grype/pkg/qualifier/platformcpe/qualifier.go index 6bdcefe8f6d4..ac67fb309438 100644 --- a/grype/pkg/qualifier/platformcpe/qualifier.go +++ b/grype/pkg/qualifier/platformcpe/qualifier.go @@ -41,7 +41,7 @@ func isWordpressPlatformCPE(c cpe.CPE) bool { return c.Attributes.Vendor == "wordpress" && c.Attributes.Product == "wordpress" } -func (p platformCPE) Satisfied(d *distro.Distro, _ pkg.Package) (bool, error) { +func (p platformCPE) Satisfied(pk pkg.Package) (bool, error) { if p.cpe == "" { return true, nil } @@ -60,20 +60,20 @@ func (p platformCPE) Satisfied(d *distro.Distro, _ pkg.Package) (bool, error) { // The remaining checks are on distro, so if the distro is unknown the condition should // be considered to be satisfied and avoid filtering matches - if d == nil { + if pk.Distro == nil { return true, nil } if isWindowsPlatformCPE(c) { - return d.Type == distro.Windows, nil + return pk.Distro.Type == distro.Windows, nil } if isUbuntuPlatformCPE(c) { - return d.Type == distro.Ubuntu, nil + return pk.Distro.Type == distro.Ubuntu, nil } if isDebianPlatformCPE(c) { - return d.Type == distro.Debian, nil + return pk.Distro.Type == distro.Debian, nil } return true, err diff --git a/grype/pkg/qualifier/platformcpe/qualifier_test.go b/grype/pkg/qualifier/platformcpe/qualifier_test.go index bfd9f26cd95c..e29c8f80424a 100644 --- a/grype/pkg/qualifier/platformcpe/qualifier_test.go +++ b/grype/pkg/qualifier/platformcpe/qualifier_test.go @@ -15,7 +15,6 @@ func TestPlatformCPE_Satisfied(t *testing.T) { name string platformCPE qualifier.Qualifier pkg pkg.Package - distro *distro.Distro satisfied bool hasError bool }{ @@ -23,147 +22,160 @@ func TestPlatformCPE_Satisfied(t *testing.T) { name: "no filter on nil distro", platformCPE: New("cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*"), pkg: pkg.Package{}, - distro: nil, satisfied: true, hasError: false, }, { name: "no filter when platform CPE is empty", platformCPE: New(""), - pkg: pkg.Package{}, - distro: &distro.Distro{Type: distro.Windows}, - satisfied: true, - hasError: false, + pkg: pkg.Package{ + Distro: &distro.Distro{Type: distro.Windows}, + }, + satisfied: true, + hasError: false, }, { name: "no filter when platform CPE is invalid", platformCPE: New(";;;"), - pkg: pkg.Package{}, - distro: &distro.Distro{Type: distro.Windows}, - satisfied: true, - hasError: true, + pkg: pkg.Package{ + Distro: &distro.Distro{Type: distro.Windows}, + }, + satisfied: true, + hasError: true, }, // Windows { name: "filter windows platform vuln when distro is not windows", platformCPE: New("cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*"), - pkg: pkg.Package{}, - distro: &distro.Distro{Type: distro.Debian}, - satisfied: false, - hasError: false, + pkg: pkg.Package{ + Distro: &distro.Distro{Type: distro.Debian}, + }, + satisfied: false, + hasError: false, }, { name: "filter windows server platform vuln when distro is not windows", platformCPE: New("cpe:2.3:o:microsoft:windows_server_2022:-:*:*:*:*:*:*:*"), - pkg: pkg.Package{}, - distro: &distro.Distro{Type: distro.Debian}, - satisfied: false, - hasError: false, + pkg: pkg.Package{ + Distro: &distro.Distro{Type: distro.Debian}, + }, + satisfied: false, + hasError: false, }, { name: "no filter windows platform vuln when distro is windows", platformCPE: New("cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*"), - pkg: pkg.Package{}, - distro: &distro.Distro{Type: distro.Windows}, - satisfied: true, - hasError: false, + pkg: pkg.Package{ + Distro: &distro.Distro{Type: distro.Windows}, + }, + satisfied: true, + hasError: false, }, { name: "no filter windows server platform vuln when distro is windows", platformCPE: New("cpe:2.3:o:microsoft:windows_server_2022:-:*:*:*:*:*:*:*"), - pkg: pkg.Package{}, - distro: &distro.Distro{Type: distro.Windows}, - satisfied: true, - hasError: false, + pkg: pkg.Package{ + Distro: &distro.Distro{Type: distro.Windows}, + }, + satisfied: true, + hasError: false, }, // Debian { name: "filter debian platform vuln when distro is not debian", platformCPE: New("cpe:2.3:o:debian:debian_linux:-:*:*:*:*:*:*:*"), - pkg: pkg.Package{}, - distro: &distro.Distro{Type: distro.Ubuntu}, - satisfied: false, - hasError: false, + pkg: pkg.Package{ + Distro: &distro.Distro{Type: distro.Ubuntu}, + }, + satisfied: false, + hasError: false, }, { name: "filter debian platform vuln when distro is not debian (alternate encountered cpe)", platformCPE: New("cpe:2.3:o:debian:linux:-:*:*:*:*:*:*:*"), - pkg: pkg.Package{}, - distro: &distro.Distro{Type: distro.SLES}, - satisfied: false, - hasError: false, + pkg: pkg.Package{ + Distro: &distro.Distro{Type: distro.SLES}, + }, + satisfied: false, + hasError: false, }, { name: "no filter debian platform vuln when distro is debian", platformCPE: New("cpe:2.3:o:debian:debian_linux:-:*:*:*:*:*:*:*"), - pkg: pkg.Package{}, - distro: &distro.Distro{Type: distro.Debian}, - satisfied: true, - hasError: false, + pkg: pkg.Package{ + Distro: &distro.Distro{Type: distro.Debian}, + }, + satisfied: true, + hasError: false, }, { name: "no filter debian platform vuln when distro is debian (alternate encountered cpe)", platformCPE: New("cpe:2.3:o:debian:linux:-:*:*:*:*:*:*:*"), - pkg: pkg.Package{}, - distro: &distro.Distro{Type: distro.Debian}, - satisfied: true, - hasError: false, + pkg: pkg.Package{ + Distro: &distro.Distro{Type: distro.Debian}, + }, + satisfied: true, + hasError: false, }, // Ubuntu { name: "filter ubuntu platform vuln when distro is not ubuntu", platformCPE: New("cpe:2.3:o:canonical:ubuntu_linux:-:*:*:*:*:*:*:*"), - pkg: pkg.Package{}, - distro: &distro.Distro{Type: distro.SLES}, - satisfied: false, - hasError: false, + pkg: pkg.Package{ + Distro: &distro.Distro{Type: distro.SLES}, + }, + satisfied: false, + hasError: false, }, { name: "filter ubuntu platform vuln when distro is not ubuntu (alternate encountered cpe)", platformCPE: New("cpe:2.3:o:ubuntu:vivid:-:*:*:*:*:*:*:*"), - pkg: pkg.Package{}, - distro: &distro.Distro{Type: distro.Alpine}, - satisfied: false, - hasError: false, + pkg: pkg.Package{ + Distro: &distro.Distro{Type: distro.Alpine}, + }, + satisfied: false, + hasError: false, }, { name: "no filter ubuntu platform vuln when distro is ubuntu", platformCPE: New("cpe:2.3:o:canonical:ubuntu_linux:-:*:*:*:*:*:*:*"), - pkg: pkg.Package{}, - distro: &distro.Distro{Type: distro.Ubuntu}, - satisfied: true, - hasError: false, + pkg: pkg.Package{ + Distro: &distro.Distro{Type: distro.Ubuntu}, + }, + satisfied: true, + hasError: false, }, { name: "no filter ubuntu platform vuln when distro is ubuntu (alternate encountered cpe)", platformCPE: New("cpe:2.3:o:ubuntu:vivid:-:*:*:*:*:*:*:*"), - pkg: pkg.Package{}, - distro: &distro.Distro{Type: distro.Ubuntu}, - satisfied: true, - hasError: false, + pkg: pkg.Package{ + Distro: &distro.Distro{Type: distro.Ubuntu}, + }, + satisfied: true, + hasError: false, }, // Wordpress { name: "always filter wordpress platform vulns (no known distro)", platformCPE: New("cpe:2.3:o:wordpress:wordpress:-:*:*:*:*:*:*:*"), pkg: pkg.Package{}, - distro: nil, satisfied: false, hasError: false, }, { name: "always filter wordpress platform vulns (known distro)", platformCPE: New("cpe:2.3:o:ubuntu:vivid:-:*:*:*:*:*:*:*"), - pkg: pkg.Package{}, - distro: &distro.Distro{Type: distro.Alpine}, - satisfied: false, - hasError: false, + pkg: pkg.Package{ + Distro: &distro.Distro{Type: distro.Alpine}, + }, + satisfied: false, + hasError: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - s, err := test.platformCPE.Satisfied(test.distro, test.pkg) + s, err := test.platformCPE.Satisfied(test.pkg) if test.hasError { assert.Error(t, err) diff --git a/grype/pkg/qualifier/qualifier.go b/grype/pkg/qualifier/qualifier.go index 5816c6d02f01..71bf8a947456 100644 --- a/grype/pkg/qualifier/qualifier.go +++ b/grype/pkg/qualifier/qualifier.go @@ -1,10 +1,9 @@ package qualifier import ( - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" ) type Qualifier interface { - Satisfied(d *distro.Distro, p pkg.Package) (bool, error) + Satisfied(p pkg.Package) (bool, error) } diff --git a/grype/pkg/qualifier/rpmmodularity/qualifier.go b/grype/pkg/qualifier/rpmmodularity/qualifier.go index e207152cdb7e..cd13131ff3bc 100644 --- a/grype/pkg/qualifier/rpmmodularity/qualifier.go +++ b/grype/pkg/qualifier/rpmmodularity/qualifier.go @@ -16,7 +16,7 @@ func New(module string) qualifier.Qualifier { return &rpmModularity{module: module} } -func (r rpmModularity) Satisfied(d *distro.Distro, p pkg.Package) (bool, error) { +func (r rpmModularity) Satisfied(p pkg.Package) (bool, error) { if p.Metadata == nil { // If unable to determine package modularity, the constraint should be considered satisfied return true, nil @@ -33,7 +33,7 @@ func (r rpmModularity) Satisfied(d *distro.Distro, p pkg.Package) (bool, error) return true, nil } - if d != nil && d.Type == distro.OracleLinux && *m.ModularityLabel == "" { + if p.Distro != nil && p.Distro.Type == distro.OracleLinux && *m.ModularityLabel == "" { // For oraclelinux, the default stream of an installed appstream package does not currently set // the MODULARITYLABEL property in the rpm metadata; however, in their advisory data they do specify // modularity information, so this ends up in a case where the vuln entries have modularity but the diff --git a/grype/pkg/qualifier/rpmmodularity/qualifier_test.go b/grype/pkg/qualifier/rpmmodularity/qualifier_test.go index 66304b13342a..f8b14f1a1757 100644 --- a/grype/pkg/qualifier/rpmmodularity/qualifier_test.go +++ b/grype/pkg/qualifier/rpmmodularity/qualifier_test.go @@ -17,127 +17,137 @@ func TestRpmModularity_Satisfied(t *testing.T) { name string rpmModularity qualifier.Qualifier pkg pkg.Package - distro *distro.Distro satisfied bool }{ { name: "non rpm metadata", rpmModularity: New("test:1"), pkg: pkg.Package{ + Distro: nil, Metadata: pkg.JavaMetadata{}, }, - distro: nil, satisfied: false, }, { name: "module with package rpm metadata lacking actual metadata 1", rpmModularity: New("test:1"), - pkg: pkg.Package{Metadata: nil}, - distro: nil, - satisfied: true, + pkg: pkg.Package{ + Distro: nil, + Metadata: nil, + }, + satisfied: true, }, { name: "empty module with rpm metadata lacking actual metadata 2", rpmModularity: New(""), pkg: pkg.Package{Metadata: nil}, - distro: nil, satisfied: true, }, { name: "no modularity label with no module", rpmModularity: New(""), - pkg: pkg.Package{Metadata: pkg.RpmMetadata{ - Epoch: nil, - }}, - distro: nil, + pkg: pkg.Package{ + Distro: nil, + Metadata: pkg.RpmMetadata{ + Epoch: nil, + }}, satisfied: true, }, { name: "no modularity label with module", rpmModularity: New("abc"), - pkg: pkg.Package{Metadata: pkg.RpmMetadata{ - Epoch: nil, - }}, - distro: nil, + pkg: pkg.Package{ + Distro: nil, + Metadata: pkg.RpmMetadata{ + Epoch: nil, + }}, satisfied: true, }, { name: "modularity label with no module", rpmModularity: New(""), - pkg: pkg.Package{Metadata: pkg.RpmMetadata{ - ModularityLabel: strRef("x:3:1234567:abcd"), - }}, - distro: nil, + pkg: pkg.Package{ + Distro: nil, + Metadata: pkg.RpmMetadata{ + ModularityLabel: strRef("x:3:1234567:abcd"), + }}, satisfied: false, }, { name: "modularity label in module", rpmModularity: New("x:3"), - pkg: pkg.Package{Metadata: pkg.RpmMetadata{ - ModularityLabel: strRef("x:3:1234567:abcd"), - }}, - distro: nil, + pkg: pkg.Package{ + Distro: nil, + Metadata: pkg.RpmMetadata{ + ModularityLabel: strRef("x:3:1234567:abcd"), + }}, satisfied: true, }, { name: "modularity label not in module", rpmModularity: New("x:3"), - pkg: pkg.Package{Metadata: pkg.RpmMetadata{ - ModularityLabel: strRef("x:1:1234567:abcd"), - }}, - distro: nil, + pkg: pkg.Package{ + Distro: nil, + Metadata: pkg.RpmMetadata{ + ModularityLabel: strRef("x:1:1234567:abcd"), + }}, satisfied: false, }, { name: "modularity label is positively blank", rpmModularity: New(""), - pkg: pkg.Package{Metadata: pkg.RpmMetadata{ - ModularityLabel: strRef(""), - }}, - distro: nil, + pkg: pkg.Package{ + Distro: nil, + Metadata: pkg.RpmMetadata{ + ModularityLabel: strRef(""), + }}, satisfied: true, }, { name: "modularity label is missing (assume we cannot verify that capability)", rpmModularity: New(""), - pkg: pkg.Package{Metadata: pkg.RpmMetadata{ - ModularityLabel: nil, - }}, - distro: nil, + pkg: pkg.Package{ + Distro: nil, + Metadata: pkg.RpmMetadata{ + ModularityLabel: nil, + }}, satisfied: true, }, { name: "default appstream for oraclelinux (treat as missing)", rpmModularity: New("nodejs:16"), - pkg: pkg.Package{Metadata: pkg.RpmMetadata{ - ModularityLabel: strRef(""), - }}, - distro: oracle, + pkg: pkg.Package{ + Distro: oracle, + Metadata: pkg.RpmMetadata{ + ModularityLabel: strRef(""), + }}, satisfied: true, }, { name: "non-default appstream for oraclelinux matches vuln modularity", rpmModularity: New("nodejs:16"), - pkg: pkg.Package{Metadata: pkg.RpmMetadata{ - ModularityLabel: strRef("nodejs:16:blah"), - }}, - distro: oracle, + pkg: pkg.Package{ + Distro: oracle, + Metadata: pkg.RpmMetadata{ + ModularityLabel: strRef("nodejs:16:blah"), + }}, satisfied: true, }, { name: "non-default appstream for oraclelinux does not match vuln modularity", rpmModularity: New("nodejs:17"), - pkg: pkg.Package{Metadata: pkg.RpmMetadata{ - ModularityLabel: strRef("nodejs:16:blah"), - }}, - distro: oracle, + pkg: pkg.Package{ + Distro: oracle, + Metadata: pkg.RpmMetadata{ + ModularityLabel: strRef("nodejs:16:blah"), + }}, satisfied: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - s, err := test.rpmModularity.Satisfied(test.distro, test.pkg) + s, err := test.rpmModularity.Satisfied(test.pkg) assert.NoError(t, err) assert.Equal(t, test.satisfied, s) }) diff --git a/grype/search/cpe.go b/grype/search/cpe.go new file mode 100644 index 000000000000..a0aa6ddc3f3a --- /dev/null +++ b/grype/search/cpe.go @@ -0,0 +1,59 @@ +package search + +import ( + "strings" + + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/syft/syft/cpe" +) + +// ByCPE returns criteria which will search based on any of the provided CPEs +func ByCPE(c cpe.CPE) vulnerability.Criteria { + return &CPECriteria{ + CPE: c, + } +} + +type CPECriteria struct { + CPE cpe.CPE +} + +func (v *CPECriteria) MatchesVulnerability(vuln vulnerability.Vulnerability) (bool, error) { + if containsCPE(vuln.CPEs, v.CPE) { + return true, nil + } + return false, nil +} + +var _ interface { + vulnerability.Criteria +} = (*CPECriteria)(nil) + +// containsCPE returns true if the provided slice contains a matching CPE based on attributes matching +func containsCPE(cpes []cpe.CPE, cpe cpe.CPE) bool { + for _, c := range cpes { + if matchesAttributes(cpe.Attributes, c.Attributes) { + return true + } + } + return false +} + +func matchesAttributes(a1 cpe.Attributes, a2 cpe.Attributes) bool { + if !matchesAttribute(a1.Product, a2.Product) || + !matchesAttribute(a1.Vendor, a2.Vendor) || + !matchesAttribute(a1.Part, a2.Part) || + !matchesAttribute(a1.Language, a2.Language) || + !matchesAttribute(a1.SWEdition, a2.SWEdition) || + !matchesAttribute(a1.TargetSW, a2.TargetSW) || + !matchesAttribute(a1.TargetHW, a2.TargetHW) || + !matchesAttribute(a1.Other, a2.Other) || + !matchesAttribute(a1.Edition, a2.Edition) { + return false + } + return true +} + +func matchesAttribute(a1, a2 string) bool { + return a1 == "" || a2 == "" || strings.EqualFold(a1, a2) +} diff --git a/grype/search/cpe_test.go b/grype/search/cpe_test.go new file mode 100644 index 000000000000..acaa507d7a55 --- /dev/null +++ b/grype/search/cpe_test.go @@ -0,0 +1,50 @@ +package search + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/syft/syft/cpe" +) + +func Test_ByCPE(t *testing.T) { + tests := []struct { + name string + cpe cpe.CPE + input vulnerability.Vulnerability + wantErr require.ErrorAssertionFunc + matches bool + }{ + { + name: "match", + cpe: cpe.Must("cpe:2.3:a:a-vendor:a-product:*:*:*:*:*:*:*:*", ""), + input: vulnerability.Vulnerability{ + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:a-vendor:a-product:*:*:*:*:*:*:*:*", "")}, + }, + matches: true, + }, + { + name: "not match", + cpe: cpe.Must("cpe:2.3:a:a-vendor:b-product:*:*:*:*:*:*:*:*", ""), + input: vulnerability.Vulnerability{ + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:a-vendor:a-product:*:*:*:*:*:*:*:*", "")}, + }, + matches: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + constraint := ByCPE(tt.cpe) + matches, err := constraint.MatchesVulnerability(tt.input) + wantErr := require.NoError + if tt.wantErr != nil { + wantErr = tt.wantErr + } + wantErr(t, err) + require.Equal(t, tt.matches, matches) + }) + } +} diff --git a/grype/search/criteria.go b/grype/search/criteria.go new file mode 100644 index 000000000000..c5c3c53bd8a4 --- /dev/null +++ b/grype/search/criteria.go @@ -0,0 +1,115 @@ +package search + +import ( + "fmt" + "iter" + "reflect" + "slices" + + "github.com/anchore/grype/grype/vulnerability" +) + +// ------- Utilities ------- + +// CriteriaIterator processes all conditions into distinct sets of flattened criteria +func CriteriaIterator(criteria []vulnerability.Criteria) iter.Seq2[int, []vulnerability.Criteria] { + if len(criteria) == 0 { + return func(_ func(int, []vulnerability.Criteria) bool) {} + } + return func(yield func(int, []vulnerability.Criteria) bool) { + idx := 0 + fn := func(criteria []vulnerability.Criteria) bool { + out := yield(idx, criteria) + idx++ + return out + } + _ = processRemaining(nil, criteria, fn) + } +} + +func processRemaining(row, criteria []vulnerability.Criteria, yield func([]vulnerability.Criteria) bool) bool { + if len(criteria) == 0 { + return yield(row) + } + return processRemainingItem(row, criteria[1:], criteria[0], yield) +} + +func processRemainingItem(row, criteria []vulnerability.Criteria, item vulnerability.Criteria, yield func([]vulnerability.Criteria) bool) bool { + switch item := item.(type) { + case and: + // we replace this criteria object with its constituent parts + return processRemaining(row, append(item, criteria...), yield) + case or: + for _, option := range item { + if !processRemainingItem(row, criteria, option, yield) { + return false + } + } + default: + return processRemaining(append(row, item), criteria, yield) + } + return true // continue +} + +var allowedMultipleCriteria = []reflect.Type{reflect.TypeOf(funcCriteria{})} + +// ValidateCriteria asserts that there are no incorrect duplications of criteria +// e.g. multiple ByPackageName() which would result in no matches, while Or(pkgName1, pkgName2) is allowed +func ValidateCriteria(criteria []vulnerability.Criteria) error { + for _, row := range CriteriaIterator(criteria) { // process OR conditions into flattened lists of AND conditions + seenTypes := make(map[reflect.Type]interface{}) + + for _, criterion := range row { + criterionType := reflect.TypeOf(criterion) + + if slices.Contains(allowedMultipleCriteria, criterionType) { + continue + } + + if previous, exists := seenTypes[criterionType]; exists { + return fmt.Errorf("multiple conflicting criteria specified: %+v %+v", previous, criterion) + } + + seenTypes[criterionType] = criterion + } + } + return nil +} + +// orCriteria provides a way to specify multiple criteria to be used, only requiring one to match +type or []vulnerability.Criteria + +func Or(criteria ...vulnerability.Criteria) vulnerability.Criteria { + return or(criteria) +} + +func (c or) MatchesVulnerability(v vulnerability.Vulnerability) (bool, error) { + for _, crit := range c { + matches, err := crit.MatchesVulnerability(v) + if matches || err != nil { + return matches, err + } + } + return false, nil +} + +var _ interface { + vulnerability.Criteria +} = (*or)(nil) + +// andCriteria provides a way to specify multiple criteria to be used, all required +type and []vulnerability.Criteria + +func And(criteria ...vulnerability.Criteria) vulnerability.Criteria { + return and(criteria) +} + +func (c and) MatchesVulnerability(v vulnerability.Vulnerability) (bool, error) { + for _, crit := range c { + matches, err := crit.MatchesVulnerability(v) + if matches || err != nil { + return matches, err + } + } + return false, nil +} diff --git a/grype/search/criteria_test.go b/grype/search/criteria_test.go new file mode 100644 index 000000000000..a3b0a25f128e --- /dev/null +++ b/grype/search/criteria_test.go @@ -0,0 +1,104 @@ +package search + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/grype/grype/distro" + "github.com/anchore/grype/grype/vulnerability" +) + +func Test_CriteriaIterator(t *testing.T) { + name1 := ByPackageName("name1") + name2 := ByPackageName("name2") + name3 := ByPackageName("name3") + + tests := []struct { + name string + in []vulnerability.Criteria + expected [][]vulnerability.Criteria + }{ + { + name: "empty", + in: nil, + expected: nil, + }, + { + name: "one", + in: []vulnerability.Criteria{name1}, + expected: [][]vulnerability.Criteria{{name1}}, + }, + { + name: "name1 or name2", + in: []vulnerability.Criteria{Or(name1, name2)}, + expected: [][]vulnerability.Criteria{{name1}, {name2}}, + }, + { + name: "name1 AND (name2 or name3)", + in: []vulnerability.Criteria{name1, Or(name2, name3)}, + expected: [][]vulnerability.Criteria{{name1, name2}, {name1, name3}}, + }, + { + name: "name1 AND (name2 or name3) AND (name1 or name2 or name3)", + in: []vulnerability.Criteria{name1, Or(name2, name3), Or(name1, name2, name3)}, + expected: [][]vulnerability.Criteria{ + {name1, name2, name1}, {name1, name3, name1}, + {name1, name2, name2}, {name1, name3, name2}, + {name1, name2, name3}, {name1, name3, name3}, + }, + }, + { + name: "(name1 AND name2) OR (name1 AND name3)", + in: []vulnerability.Criteria{Or(And(name1, name2), And(name1, name3))}, + expected: [][]vulnerability.Criteria{ + {name1, name2}, {name1, name3}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var got [][]vulnerability.Criteria + for _, row := range CriteriaIterator(test.in) { + got = append(got, row) + } + require.ElementsMatch(t, test.expected, got) + }) + } +} + +func Test_ValidateCriteria(t *testing.T) { + tests := []struct { + name string + in []vulnerability.Criteria + wantErr require.ErrorAssertionFunc + }{ + { + name: "no error", + in: []vulnerability.Criteria{ByPackageName("steve"), ByDistro(distro.Distro{})}, + wantErr: require.NoError, + }, + { + name: "package name error", + in: []vulnerability.Criteria{ByPackageName("steve"), ByPackageName("bob")}, + wantErr: require.Error, + }, + { + name: "multiple distros error", + in: []vulnerability.Criteria{ByDistro(distro.Distro{}), ByDistro(distro.Distro{})}, + wantErr: require.Error, + }, + { + name: "multiple package name in or condition not error", + in: []vulnerability.Criteria{Or(ByPackageName("steve"), ByPackageName("bob"))}, + wantErr: require.NoError, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := ValidateCriteria(test.in) + test.wantErr(t, err) + }) + } +} diff --git a/grype/search/distro.go b/grype/search/distro.go new file mode 100644 index 000000000000..c34d0d331400 --- /dev/null +++ b/grype/search/distro.go @@ -0,0 +1,79 @@ +package search + +import ( + "strings" + + "github.com/anchore/grype/grype/db/v5/namespace" + distroNs "github.com/anchore/grype/grype/db/v5/namespace/distro" + "github.com/anchore/grype/grype/distro" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/log" +) + +// ByDistro returns criteria which will match vulnerabilities based on any of the provided Distros +func ByDistro(d ...distro.Distro) vulnerability.Criteria { + return &DistroCriteria{ + Distros: d, + } +} + +type DistroCriteria struct { + Distros []distro.Distro +} + +func (c *DistroCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, error) { + ns, err := namespace.FromString(value.Namespace) + if err != nil { + log.Debugf("unable to determine namespace for vulnerability %v: %v", value.Reference.ID, err) + return false, nil + } + dns, ok := ns.(*distroNs.Namespace) + if !ok || dns == nil { + // not a Distro-based vulnerability + return false, nil + } + if len(c.Distros) == 0 { + return true, nil + } + for _, d := range c.Distros { + if matchesDistro(&d, dns) { + return true, nil + } + } + return false, nil +} + +var _ interface { + vulnerability.Criteria + // queryOSSpecifier +} = (*DistroCriteria)(nil) + +// matchesDistro returns true distro types are equal and versions are compatible +func matchesDistro(d *distro.Distro, ns *distroNs.Namespace) bool { + if d == nil || ns == nil { + return false + } + + ty := namespace.DistroTypeString(d.Type) + + distroType := ns.DistroType() + if distroType != d.Type && distroType != distro.Type(ty) { + return false + } + return compatibleVersion(d.FullVersion(), ns.Version()) +} + +// compatibleVersion returns true when the versions are the same or the partial version describes the matching parts +// of the fullVersion +func compatibleVersion(fullVersion string, partialVersion string) bool { + if fullVersion == "" { + return true + } + if fullVersion == partialVersion { + return true + } + if strings.HasPrefix(fullVersion, partialVersion) && len(fullVersion) > len(partialVersion) && fullVersion[len(partialVersion)] == '.' { + return true + } + return false +} diff --git a/grype/search/distro_test.go b/grype/search/distro_test.go new file mode 100644 index 000000000000..5b9a5cafeb76 --- /dev/null +++ b/grype/search/distro_test.go @@ -0,0 +1,57 @@ +package search + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/grype/grype/distro" + "github.com/anchore/grype/grype/vulnerability" +) + +func Test_ByDistro(t *testing.T) { + deb8, err := distro.New(distro.Debian, "8", "") + require.NoError(t, err) + + tests := []struct { + name string + distro distro.Distro + input vulnerability.Vulnerability + wantErr require.ErrorAssertionFunc + matches bool + }{ + { + name: "match", + distro: *deb8, + input: vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + Namespace: "debian:distro:debian:8", + }, + }, + matches: true, + }, + { + name: "not match", + distro: *deb8, + input: vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + Namespace: "debian:distro:ubuntu:8", + }, + }, + matches: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + constraint := ByDistro(tt.distro) + matches, err := constraint.MatchesVulnerability(tt.input) + wantErr := require.NoError + if tt.wantErr != nil { + wantErr = tt.wantErr + } + wantErr(t, err) + require.Equal(t, tt.matches, matches) + }) + } +} diff --git a/grype/search/func.go b/grype/search/func.go new file mode 100644 index 000000000000..ca292b60c4db --- /dev/null +++ b/grype/search/func.go @@ -0,0 +1,19 @@ +package search + +import "github.com/anchore/grype/grype/vulnerability" + +// ByFunc returns criteria which will use the provided function to filter vulnerabilities +func ByFunc(criteriaFunc func(vulnerability.Vulnerability) (bool, error)) vulnerability.Criteria { + return funcCriteria{fn: criteriaFunc} +} + +// funcCriteria implements vulnerability.Criteria by providing a function implementing the same signature as MatchVulnerability +type funcCriteria struct { + fn func(vulnerability.Vulnerability) (bool, error) +} + +func (f funcCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, error) { + return f.fn(value) +} + +var _ vulnerability.Criteria = (*funcCriteria)(nil) diff --git a/grype/search/func_test.go b/grype/search/func_test.go new file mode 100644 index 000000000000..0052709a0179 --- /dev/null +++ b/grype/search/func_test.go @@ -0,0 +1,49 @@ +package search + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/grype/grype/vulnerability" +) + +func Test_ByFunc(t *testing.T) { + tests := []struct { + name string + fn func(vulnerability.Vulnerability) (bool, error) + input vulnerability.Vulnerability + wantErr require.ErrorAssertionFunc + matches bool + }{ + { + name: "match", + fn: func(v vulnerability.Vulnerability) (bool, error) { + return true, nil + }, + input: vulnerability.Vulnerability{}, + matches: true, + }, + { + name: "not match", + fn: func(v vulnerability.Vulnerability) (bool, error) { + return false, nil + }, + input: vulnerability.Vulnerability{}, + matches: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + constraint := ByFunc(tt.fn) + matches, err := constraint.MatchesVulnerability(tt.input) + wantErr := require.NoError + if tt.wantErr != nil { + wantErr = tt.wantErr + } + wantErr(t, err) + require.Equal(t, tt.matches, matches) + }) + } +} diff --git a/grype/search/id.go b/grype/search/id.go new file mode 100644 index 000000000000..f6c2fc96481c --- /dev/null +++ b/grype/search/id.go @@ -0,0 +1,25 @@ +package search + +import ( + "github.com/anchore/grype/grype/vulnerability" +) + +// ByID returns criteria to search by vulnerability ID, such as CVE-2024-9143 +func ByID(id string) vulnerability.Criteria { + return &IDCriteria{ + ID: id, + } +} + +// IDCriteria is able to match vulnerabilities to the assigned ID, such as CVE-2024-1000 or GHSA-g2x7-ar59-85z5 +type IDCriteria struct { + ID string +} + +func (v *IDCriteria) MatchesVulnerability(vuln vulnerability.Vulnerability) (bool, error) { + return vuln.ID == v.ID, nil +} + +var _ interface { + vulnerability.Criteria +} = (*IDCriteria)(nil) diff --git a/grype/search/id_test.go b/grype/search/id_test.go new file mode 100644 index 000000000000..112f4ebf9eec --- /dev/null +++ b/grype/search/id_test.go @@ -0,0 +1,53 @@ +package search + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/grype/grype/vulnerability" +) + +func Test_ByID(t *testing.T) { + tests := []struct { + name string + id string + input vulnerability.Vulnerability + wantErr require.ErrorAssertionFunc + matches bool + }{ + { + name: "match", + id: "CVE-YEAR-1", + input: vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-YEAR-1", + }, + }, + matches: true, + }, + { + name: "not match", + id: "CVE-YEAR-1", + input: vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-YEAR-2", + }, + }, + matches: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + constraint := ByID(tt.id) + matches, err := constraint.MatchesVulnerability(tt.input) + wantErr := require.NoError + if tt.wantErr != nil { + wantErr = tt.wantErr + } + wantErr(t, err) + require.Equal(t, tt.matches, matches) + }) + } +} diff --git a/grype/search/language.go b/grype/search/language.go new file mode 100644 index 000000000000..57506326ec58 --- /dev/null +++ b/grype/search/language.go @@ -0,0 +1,38 @@ +package search + +import ( + "github.com/anchore/grype/grype/db/v5/namespace" + "github.com/anchore/grype/grype/db/v5/namespace/language" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/log" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +// ByLanguage returns criteria which will search based on the package Language +func ByLanguage(lang syftPkg.Language) vulnerability.Criteria { + return &LanguageCriteria{ + Language: lang, + } +} + +type LanguageCriteria struct { + Language syftPkg.Language +} + +func (c *LanguageCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, error) { + ns, err := namespace.FromString(value.Namespace) + if err != nil { + log.Debugf("unable to determine namespace for vulnerability %v: %v", value.Reference.ID, err) + return false, nil + } + lang, ok := ns.(*language.Namespace) + if !ok || lang == nil { + // not a language-based vulnerability + return false, nil + } + return c.Language == lang.Language(), nil +} + +var _ interface { + vulnerability.Criteria +} = (*LanguageCriteria)(nil) diff --git a/grype/search/language_test.go b/grype/search/language_test.go new file mode 100644 index 000000000000..1e3d94a1d43c --- /dev/null +++ b/grype/search/language_test.go @@ -0,0 +1,54 @@ +package search + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/grype/grype/vulnerability" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +func Test_ByLanguage(t *testing.T) { + tests := []struct { + name string + lang syftPkg.Language + input vulnerability.Vulnerability + wantErr require.ErrorAssertionFunc + matches bool + }{ + { + name: "match", + lang: syftPkg.Java, + input: vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + Namespace: "github:language:java", + }, + }, + matches: true, + }, + { + name: "not match", + lang: syftPkg.Java, + input: vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + Namespace: "github:language:javascript", + }, + }, + matches: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + constraint := ByLanguage(tt.lang) + matches, err := constraint.MatchesVulnerability(tt.input) + wantErr := require.NoError + if tt.wantErr != nil { + wantErr = tt.wantErr + } + wantErr(t, err) + require.Equal(t, tt.matches, matches) + }) + } +} diff --git a/grype/search/package_name.go b/grype/search/package_name.go new file mode 100644 index 000000000000..21e5123bf9b9 --- /dev/null +++ b/grype/search/package_name.go @@ -0,0 +1,26 @@ +package search + +import ( + "strings" + + "github.com/anchore/grype/grype/vulnerability" +) + +// ByPackageName returns criteria restricting vulnerabilities to match the package name provided +func ByPackageName(packageName string) vulnerability.Criteria { + return &PackageNameCriteria{ + PackageName: packageName, + } +} + +type PackageNameCriteria struct { + PackageName string +} + +func (v *PackageNameCriteria) MatchesVulnerability(vuln vulnerability.Vulnerability) (bool, error) { + return strings.EqualFold(vuln.PackageName, v.PackageName), nil +} + +var _ interface { + vulnerability.Criteria +} = (*PackageNameCriteria)(nil) diff --git a/grype/search/package_name_test.go b/grype/search/package_name_test.go new file mode 100644 index 000000000000..33d394300c71 --- /dev/null +++ b/grype/search/package_name_test.go @@ -0,0 +1,57 @@ +package search + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/grype/grype/vulnerability" +) + +func Test_ByPackageName(t *testing.T) { + tests := []struct { + name string + packageName string + input vulnerability.Vulnerability + wantErr require.ErrorAssertionFunc + matches bool + }{ + { + name: "match", + packageName: "some-name", + input: vulnerability.Vulnerability{ + PackageName: "some-name", + }, + matches: true, + }, + { + name: "match case insensitive", + packageName: "some-name", + input: vulnerability.Vulnerability{ + PackageName: "SomE-NaMe", + }, + matches: true, + }, + { + name: "not match", + packageName: "some-name", + input: vulnerability.Vulnerability{ + PackageName: "other-name", + }, + matches: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + constraint := ByPackageName(tt.packageName) + matches, err := constraint.MatchesVulnerability(tt.input) + wantErr := require.NoError + if tt.wantErr != nil { + wantErr = tt.wantErr + } + wantErr(t, err) + require.Equal(t, tt.matches, matches) + }) + } +} diff --git a/grype/search/version_constraint.go b/grype/search/version_constraint.go new file mode 100644 index 000000000000..9757a1a4af65 --- /dev/null +++ b/grype/search/version_constraint.go @@ -0,0 +1,79 @@ +package search + +import ( + "errors" + "fmt" + + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/log" +) + +// VersionConstraintMatcher is used for searches which include version.Constraints; this should be used instead of +// post-filtering vulnerabilities in order to most efficiently hydrate data in memory +type VersionConstraintMatcher interface { + MatchesConstraint(constraint version.Constraint) (bool, error) +} + +// ByConstraintFunc returns criteria which will use the provided function as inclusion criteria +func ByConstraintFunc(constraintFunc func(constraint version.Constraint) (bool, error)) vulnerability.Criteria { + return &constraintFuncCriteria{fn: constraintFunc} +} + +// ByVersion returns criteria which constrains vulnerabilities to those with matching version constraints +func ByVersion(v version.Version) vulnerability.Criteria { + return ByConstraintFunc(func(constraint version.Constraint) (bool, error) { + satisfied, err := constraint.Satisfied(&v) + if err != nil { + var e *version.NonFatalConstraintError + if errors.As(err, &e) { + log.Warn(e) + } else { + return false, fmt.Errorf("failed to check constraint=%v version=%v: %w", constraint, v, err) + } + } + return satisfied, nil + }) +} + +// constraintFuncCriteria implements vulnerability.Criteria by providing a function implementing the same signature as MatchVulnerability +type constraintFuncCriteria struct { + fn func(constraint version.Constraint) (bool, error) +} + +func (f *constraintFuncCriteria) MatchesConstraint(constraint version.Constraint) (bool, error) { + return f.fn(constraint) +} + +func (f *constraintFuncCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, error) { + return f.fn(value.Constraint) +} + +var _ interface { + vulnerability.Criteria + VersionConstraintMatcher +} = (*constraintFuncCriteria)(nil) + +func MultiConstraintMatcher(a, b VersionConstraintMatcher) VersionConstraintMatcher { + return &multiConstraintMatcher{ + a: a, + b: b, + } +} + +// multiConstraintMatcher is used internally when multiple version constraint matchers are specified +type multiConstraintMatcher struct { + a, b VersionConstraintMatcher +} + +func (m *multiConstraintMatcher) MatchesConstraint(constraint version.Constraint) (bool, error) { + a, err := m.a.MatchesConstraint(constraint) + if a || err != nil { + return a, err + } + return m.b.MatchesConstraint(constraint) +} + +var _ interface { + VersionConstraintMatcher +} = (*multiConstraintMatcher)(nil) diff --git a/grype/search/version_constraint_test.go b/grype/search/version_constraint_test.go new file mode 100644 index 000000000000..5194e4a97ac7 --- /dev/null +++ b/grype/search/version_constraint_test.go @@ -0,0 +1,96 @@ +package search + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" +) + +func Test_ByVersion(t *testing.T) { + tests := []struct { + name string + version string + input vulnerability.Vulnerability + wantErr require.ErrorAssertionFunc + matches bool + }{ + { + name: "match", + version: "1.0", + input: vulnerability.Vulnerability{ + Constraint: version.MustGetConstraint("< 2.0", version.SemanticFormat), + }, + matches: true, + }, + { + name: "not match", + version: "2.0", + input: vulnerability.Vulnerability{ + Constraint: version.MustGetConstraint("< 2.0", version.SemanticFormat), + }, + matches: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v, err := version.NewVersion(tt.version, version.SemanticFormat) + require.NoError(t, err) + constraint := ByVersion(*v) + matches, err := constraint.MatchesVulnerability(tt.input) + wantErr := require.NoError + if tt.wantErr != nil { + wantErr = tt.wantErr + } + wantErr(t, err) + require.Equal(t, tt.matches, matches) + }) + } +} + +func Test_ByConstraintFunc(t *testing.T) { + tests := []struct { + name string + constraintFunc func(version.Constraint) (bool, error) + input vulnerability.Vulnerability + wantErr require.ErrorAssertionFunc + matches bool + }{ + { + name: "match", + constraintFunc: func(version.Constraint) (bool, error) { + return true, nil + }, + input: vulnerability.Vulnerability{ + Constraint: version.MustGetConstraint("< 2.0", version.SemanticFormat), + }, + matches: true, + }, + { + name: "not match", + constraintFunc: func(version.Constraint) (bool, error) { + return false, nil + }, + input: vulnerability.Vulnerability{ + Constraint: version.MustGetConstraint("< 2.0", version.SemanticFormat), + }, + matches: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + constraint := ByConstraintFunc(tt.constraintFunc) + matches, err := constraint.MatchesVulnerability(tt.input) + wantErr := require.NoError + if tt.wantErr != nil { + wantErr = tt.wantErr + } + wantErr(t, err) + require.Equal(t, tt.matches, matches) + }) + } +} diff --git a/grype/vulnerability/mock/vulnerability_provider.go b/grype/vulnerability/mock/vulnerability_provider.go new file mode 100644 index 000000000000..67f80ee6f3ac --- /dev/null +++ b/grype/vulnerability/mock/vulnerability_provider.go @@ -0,0 +1,78 @@ +package mock + +import ( + "github.com/anchore/grype/grype/search" + "github.com/anchore/grype/grype/vulnerability" +) + +// VulnerabilityProvider returns a new mock implementation of a vulnerability Provider, with the provided set of vulnerabilities +func VulnerabilityProvider(vulnerabilities ...vulnerability.Vulnerability) vulnerability.Provider { + return &mockProvider{ + Vulnerabilities: vulnerabilities, + } +} + +type mockProvider struct { + Vulnerabilities []vulnerability.Vulnerability +} + +func (s *mockProvider) Close() error { + return nil +} + +// VulnerabilityMetadata returns the metadata associated with a vulnerability +func (s *mockProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) { + for _, vuln := range s.Vulnerabilities { + if vuln.ID == ref.ID && vuln.Namespace == ref.Namespace { + var meta *vulnerability.Metadata + if m, ok := vuln.Reference.Internal.(vulnerability.Metadata); ok { + meta = &m + } + if m, ok := vuln.Reference.Internal.(*vulnerability.Metadata); ok { + meta = m + } + if meta != nil { + if meta.ID != vuln.ID { + meta.ID = vuln.ID + } + if meta.Namespace != vuln.Namespace { + meta.Namespace = vuln.Namespace + } + return meta, nil + } + } + } + return nil, nil +} + +func (s *mockProvider) FindVulnerabilities(criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) { + if err := search.ValidateCriteria(criteria); err != nil { + return nil, err + } + + var out []vulnerability.Vulnerability + out = append(out, s.Vulnerabilities...) + return filterE(out, func(v vulnerability.Vulnerability) (bool, error) { + for _, c := range criteria { + matches, err := c.MatchesVulnerability(v) + if !matches || err != nil { + return false, err + } + } + return true, nil + }) +} + +func filterE[T any](out []T, keep func(v T) (bool, error)) ([]T, error) { + for i := 0; i < len(out); i++ { + ok, err := keep(out[i]) + if err != nil { + return nil, err + } + if !ok { + out = append(out[:i], out[i+1:]...) + i-- + } + } + return out, nil +} diff --git a/grype/vulnerability/provider.go b/grype/vulnerability/provider.go index 0309d8e484b7..594535ec9c6f 100644 --- a/grype/vulnerability/provider.go +++ b/grype/vulnerability/provider.go @@ -1,6 +1,25 @@ package vulnerability +import "io" + +// Criteria interfaces are used for FindVulnerabilities calls +type Criteria interface { + // MatchesVulnerability returns true if the provided value meets the criteria + MatchesVulnerability(value Vulnerability) (bool, error) +} + +// MetadataProvider implementations provide ways to look up vulnerability metadata type MetadataProvider interface { // VulnerabilityMetadata returns the metadata associated with a vulnerability VulnerabilityMetadata(ref Reference) (*Metadata, error) } + +// Provider is the common interface for vulnerability sources to provide searching and metadata, such as a database +type Provider interface { + // FindVulnerabilities returns vulnerabilities matching all the provided criteria + FindVulnerabilities(criteria ...Criteria) ([]Vulnerability, error) + + MetadataProvider + + io.Closer +} diff --git a/grype/vulnerability/severity.go b/grype/vulnerability/severity.go index aa77c355c2a0..7e427e415e05 100644 --- a/grype/vulnerability/severity.go +++ b/grype/vulnerability/severity.go @@ -12,7 +12,7 @@ const ( ) var matcherTypeStr = []string{ - "unknown severity", + "unknown", // "unknown severity", "negligible", "low", "medium", diff --git a/grype/vulnerability/vulnerability.go b/grype/vulnerability/vulnerability.go index 9c629de7eec7..603ba253bfa9 100644 --- a/grype/vulnerability/vulnerability.go +++ b/grype/vulnerability/vulnerability.go @@ -11,6 +11,7 @@ import ( type Reference struct { ID string Namespace string + Internal any } type Vulnerability struct { diff --git a/grype/vulnerability_matcher.go b/grype/vulnerability_matcher.go index 387534a42998..acfbdc6d1649 100644 --- a/grype/vulnerability_matcher.go +++ b/grype/vulnerability_matcher.go @@ -1,21 +1,19 @@ package grype import ( + "errors" "fmt" - "slices" "strings" "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/matcher" - "github.com/anchore/grype/grype/db/v5/matcher/stock" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/grype/grypeerr" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/stock" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vex" "github.com/anchore/grype/grype/vulnerability" @@ -31,19 +29,13 @@ const ( ) type VulnerabilityMatcher struct { - Store v5.ProviderStore - Matchers []matcher.Matcher - IgnoreRules []match.IgnoreRule - FailSeverity *vulnerability.Severity - NormalizeByCVE bool - VexProcessor *vex.Processor -} - -func DefaultVulnerabilityMatcher(store v5.ProviderStore) *VulnerabilityMatcher { - return &VulnerabilityMatcher{ - Store: store, - Matchers: matcher.NewDefaultMatchers(matcher.Config{}), - } + VulnerabilityProvider vulnerability.Provider + ExclusionProvider match.ExclusionProvider + Matchers []match.Matcher + IgnoreRules []match.IgnoreRule + FailSeverity *vulnerability.Severity + NormalizeByCVE bool + VexProcessor *vex.Processor } func (m *VulnerabilityMatcher) FailAtOrAboveSeverity(severity *vulnerability.Severity) *VulnerabilityMatcher { @@ -51,7 +43,7 @@ func (m *VulnerabilityMatcher) FailAtOrAboveSeverity(severity *vulnerability.Sev return m } -func (m *VulnerabilityMatcher) WithMatchers(matchers []matcher.Matcher) *VulnerabilityMatcher { +func (m *VulnerabilityMatcher) WithMatchers(matchers []match.Matcher) *VulnerabilityMatcher { m.Matchers = matchers return m } @@ -74,7 +66,8 @@ func (m *VulnerabilityMatcher) FindMatches(pkgs []pkg.Package, context pkg.Conte remainingMatches, ignoredMatches, err = m.findDBMatches(pkgs, context, progressMonitor) if err != nil { - return remainingMatches, ignoredMatches, err + // errors returned from matchers during findDBMatches were being logged and not returned, so just log them here + log.WithFields("error", err).Debug("error(s) returned from findDBMatches") } remainingMatches, ignoredMatches, err = m.findVEXMatches(context, remainingMatches, ignoredMatches, progressMonitor) @@ -83,7 +76,7 @@ func (m *VulnerabilityMatcher) FindMatches(pkgs []pkg.Package, context pkg.Conte return remainingMatches, ignoredMatches, err } - if m.FailSeverity != nil && HasSeverityAtOrAbove(m.Store, *m.FailSeverity, *remainingMatches) { + if m.FailSeverity != nil && hasSeverityAtOrAbove(m.VulnerabilityProvider, *m.FailSeverity, *remainingMatches) { err = grypeerr.ErrAboveSeverityThreshold return remainingMatches, ignoredMatches, err } @@ -137,20 +130,23 @@ func (m *VulnerabilityMatcher) mergeIgnoredMatches(allIgnoredMatches ...[]match. return out } +//nolint:funlen func (m *VulnerabilityMatcher) searchDBForMatches( release *linux.Release, packages []pkg.Package, progressMonitor *monitorWriter, ) (match.Matches, error) { - var err error - res := match.NewMatches() + var errs error + var allMatches []match.Match + var allIgnored []match.IgnoredMatch matcherIndex, defaultMatcher := newMatcherIndex(m.Matchers) var d *distro.Distro if release != nil { - d, err = distro.NewFromRelease(*release) - if err != nil { - log.Warnf("unable to determine linux distribution: %+v", err) + d, errs = distro.NewFromRelease(*release) + if errs != nil { + log.Warnf("unable to determine linux distribution: %+v", errs) + errs = nil } if d != nil && d.Disabled() { log.Warnf("unsupported linux distribution: %s", d.Name()) @@ -158,14 +154,6 @@ func (m *VulnerabilityMatcher) searchDBForMatches( } } - distroFalsePositivesByLocationPath := make(map[string][]string) - if d != nil && (d.Type == distro.Wolfi || d.Type == distro.Chainguard || d.Type == distro.Alpine) { - distroFalsePositivesByLocationPath, err = indexFalsePositivesByLocation(d, packages, m.Store) - if err != nil { - return match.Matches{}, err - } - } - if defaultMatcher == nil { defaultMatcher = stock.NewStockMatcher(stock.MatcherConfig{UseCPEs: true}) } @@ -173,122 +161,55 @@ func (m *VulnerabilityMatcher) searchDBForMatches( progressMonitor.PackagesProcessed.Increment() log.WithFields("package", displayPackage(p)).Trace("searching for vulnerability matches") + // if there is no distro set, default to global distro + orig := p.Distro + if orig == nil { + p.Distro = d + } + matchAgainst, ok := matcherIndex[p.Type] if !ok { - matchAgainst = []matcher.Matcher{defaultMatcher} + matchAgainst = []match.Matcher{defaultMatcher} } for _, theMatcher := range matchAgainst { - matches, err := theMatcher.Match(m.Store, d, p) + matches, ignoredMatches, err := theMatcher.Match(m.VulnerabilityProvider, p) if err != nil { - log.WithFields("error", err, "package", displayPackage(p)).Warn("matcher failed") - continue + log.WithFields("error", err, "package", displayPackage(p)).Warn("matcher returned error") + errs = errors.Join(errs, err) } - matches = filterMatchesUsingDistroFalsePositives(matches, distroFalsePositivesByLocationPath) + allIgnored = append(allIgnored, ignoredMatches...) // Filter out matches based on records in the database exclusion table and hard-coded rules - filtered, dropped := match.ApplyExplicitIgnoreRules(m.Store, match.NewMatches(matches...)) + filtered, dropped := match.ApplyExplicitIgnoreRules(m.ExclusionProvider, match.NewMatches(matches...)) additionalMatches := filtered.Sorted() logPackageMatches(p, additionalMatches) logExplicitDroppedPackageMatches(p, dropped) - res.Add(additionalMatches...) + allMatches = append(allMatches, additionalMatches...) progressMonitor.MatchesDiscovered.Add(int64(len(additionalMatches))) // note: there is a difference between "ignore" and "dropped" matches. // ignored: matches that are filtered out due to user-provided ignore rules // dropped: matches that are filtered out due to hard-coded rules - updateVulnerabilityList(progressMonitor, additionalMatches, nil, dropped, m.Store) - } - } - - return res, nil -} - -func indexFalsePositivesByLocation( - d *distro.Distro, - packages []pkg.Package, - s v5.ProviderStore, -) (map[string][]string, error) { - distroFalsePositivesByLocationPath := make(map[string][]string) - - for _, p := range packages { - falsePositivesByLocation, err := getDistroFalsePositivesByLocation(s, d, p) - if err != nil { - return nil, err - } - for l, vulnIDs := range falsePositivesByLocation { - distroFalsePositivesByLocationPath[l] = append(distroFalsePositivesByLocationPath[l], vulnIDs...) - } - } - - return distroFalsePositivesByLocationPath, nil -} - -func getDistroFalsePositivesByLocation(s v5.ProviderStore, d *distro.Distro, p pkg.Package) (map[string][]string, error) { - result := make(map[string][]string) - - if data, ok := p.Metadata.(pkg.ApkMetadata); ok { - entries, err := s.GetByDistro(d, p) - if err != nil { - return nil, err - } - for _, entry := range entries { - if entry.Constraint.String() == "< 0 (apk)" { - for _, f := range data.Files { - result[f.Path] = append(result[f.Path], entry.ID) - } - } + updateVulnerabilityList(progressMonitor, additionalMatches, nil, dropped, m.VulnerabilityProvider) } - for _, upstreamPkg := range pkg.UpstreamPackages(p) { - entriesForUpstream, err := s.GetByDistro(d, upstreamPkg) - if err != nil { - return nil, err - } - for _, entry := range entriesForUpstream { - if entry.Constraint.String() == "< 0 (apk)" { - for _, f := range data.Files { - result[f.Path] = append(result[f.Path], entry.ID) - } - } - } - } + p.Distro = orig } - return result, nil -} + // apply ignores based on matchers returning ignore rules + filtered, dropped := match.ApplyIgnoreFilters(allMatches, ignoredMatchFilter(allIgnored)) + logIgnoredMatches(dropped) -func filterMatchesUsingDistroFalsePositives(ms []match.Match, falsePositivesByLocation map[string][]string) []match.Match { - var result []match.Match - for _, m := range ms { - isFalsePositive := false + // get deduplicated set of matches + res := match.NewMatches(filtered...) - for _, l := range m.Package.Locations.ToSlice() { - if fpVulnIDs, ok := falsePositivesByLocation[l.RealPath]; ok { - if slices.Contains(fpVulnIDs, m.Vulnerability.ID) { - isFalsePositive = true - break - } - - for _, relatedVulnerability := range m.Vulnerability.RelatedVulnerabilities { - if slices.Contains(fpVulnIDs, relatedVulnerability.ID) { - isFalsePositive = true - break - } - } - } - } - - if !isFalsePositive { - result = append(result, m) - } else { - log.WithFields("vuln", m.Vulnerability.ID, "package", displayPackage(m.Package)).Trace("dropping false positive using distro security data") - } - } + // update the total discovered matches after removing all duplicates and ignores + progressMonitor.MatchesDiscovered.Set(int64(res.Count())) - return result + return res, errs } func (m *VulnerabilityMatcher) findVEXMatches(context pkg.Context, remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch, progressMonitor *monitorWriter) (*match.Matches, []match.IgnoredMatch, error) { @@ -307,11 +228,12 @@ func (m *VulnerabilityMatcher) findVEXMatches(context pkg.Context, remainingMatc // note: this assumes that the diff can only be additive diffIgnoredMatches := ignoredMatchesDiff(ignoredMatchesAfterVex, ignoredMatches) - updateVulnerabilityList(progressMonitor, diffMatches.Sorted(), diffIgnoredMatches, nil, m.Store) + updateVulnerabilityList(progressMonitor, diffMatches.Sorted(), diffIgnoredMatches, nil, m.VulnerabilityProvider) return matchesAfterVex, ignoredMatchesAfterVex, nil } +// applyIgnoreRules applies the user-provided ignore rules, splitting ignored matches into a separate set func (m *VulnerabilityMatcher) applyIgnoreRules(matches match.Matches) (match.Matches, []match.IgnoredMatch) { var ignoredMatches []match.IgnoredMatch if len(m.IgnoreRules) == 0 { @@ -359,7 +281,7 @@ func (m *VulnerabilityMatcher) normalizeByCVE(match match.Match) match.Match { ref := effectiveCVERecordRefs[0] - upstreamMetadata, err := m.Store.GetMetadata(ref.ID, ref.Namespace) + upstreamMetadata, err := m.VulnerabilityProvider.VulnerabilityMetadata(ref) if err != nil { log.WithFields("id", ref.ID, "namespace", ref.Namespace, "error", err).Warn("unable to fetch effective CVE metadata") return match @@ -381,6 +303,43 @@ func (m *VulnerabilityMatcher) normalizeByCVE(match match.Match) match.Match { return match } +// ignoreRulesByLocation implements match.IgnoreFilter to filter each matching +// package that overlaps by location and have the same vulnerability ID (CVE) +type ignoreRulesByLocation struct { + locationToIgnoreRules map[string][]match.IgnoreRule +} + +func (i ignoreRulesByLocation) IgnoreMatch(m match.Match) []match.IgnoreRule { + for _, l := range m.Package.Locations.ToSlice() { + for _, rule := range i.locationToIgnoreRules[l.RealPath] { + if rule.Vulnerability == m.Vulnerability.ID { + return []match.IgnoreRule{rule} + } + for _, relatedVulnerability := range m.Vulnerability.RelatedVulnerabilities { + if rule.Vulnerability == relatedVulnerability.ID { + return []match.IgnoreRule{rule} + } + } + } + } + return nil +} + +// ignoreMatchFilter creates an ignore filter based on the provided IgnoredMatches to filter out "the same" +// vulnerabilities reported by other matchers based on overlapping file locations +func ignoredMatchFilter(ignores []match.IgnoredMatch) match.IgnoreFilter { + out := ignoreRulesByLocation{locationToIgnoreRules: map[string][]match.IgnoreRule{}} + for _, ignore := range ignores { + // TODO should this be syftPkg.FileOwner interface or similar? + if m, ok := ignore.Package.Metadata.(pkg.ApkMetadata); ok { + for _, f := range m.Files { + out.locationToIgnoreRules[f.Path] = append(out.locationToIgnoreRules[f.Path], ignore.AppliedIgnoreRules...) + } + } + } + return out +} + func displayPackage(p pkg.Package) string { if p.PURL != "" { return p.PURL @@ -407,9 +366,9 @@ func ignoredMatchesDiff(subject []match.IgnoredMatch, other []match.IgnoredMatch return diff } -func newMatcherIndex(matchers []matcher.Matcher) (map[syftPkg.Type][]matcher.Matcher, matcher.Matcher) { - matcherIndex := make(map[syftPkg.Type][]matcher.Matcher) - var defaultMatcher matcher.Matcher +func newMatcherIndex(matchers []match.Matcher) (map[syftPkg.Type][]match.Matcher, match.Matcher) { + matcherIndex := make(map[syftPkg.Type][]match.Matcher) + var defaultMatcher match.Matcher for _, m := range matchers { if m.Type() == match.StockMatcher { defaultMatcher = m @@ -417,7 +376,7 @@ func newMatcherIndex(matchers []matcher.Matcher) (map[syftPkg.Type][]matcher.Mat } for _, t := range m.PackageTypes() { if _, ok := matcherIndex[t]; !ok { - matcherIndex[t] = make([]matcher.Matcher, 0) + matcherIndex[t] = make([]match.Matcher, 0) } matcherIndex[t] = append(matcherIndex[t], m) @@ -432,12 +391,12 @@ func isCVE(id string) bool { return strings.HasPrefix(strings.ToLower(id), "cve-") } -func HasSeverityAtOrAbove(store v5.VulnerabilityMetadataProvider, severity vulnerability.Severity, matches match.Matches) bool { +func hasSeverityAtOrAbove(store vulnerability.MetadataProvider, severity vulnerability.Severity, matches match.Matches) bool { if severity == vulnerability.UnknownSeverity { return false } for m := range matches.Enumerate() { - metadata, err := store.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace) + metadata, err := store.VulnerabilityMetadata(m.Vulnerability.Reference) if err != nil { continue } @@ -469,9 +428,9 @@ func logListSummary(vl *monitorWriter) { } } -func updateVulnerabilityList(mon *monitorWriter, matches []match.Match, ignores []match.IgnoredMatch, dropped []match.IgnoredMatch, metadataProvider v5.VulnerabilityMetadataProvider) { +func updateVulnerabilityList(mon *monitorWriter, matches []match.Match, ignores []match.IgnoredMatch, dropped []match.IgnoredMatch, metadataProvider vulnerability.MetadataProvider) { for _, m := range matches { - metadata, err := metadataProvider.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace) + metadata, err := metadataProvider.VulnerabilityMetadata(m.Vulnerability.Reference) if err != nil || metadata == nil { mon.BySeverity[vulnerability.UnknownSeverity].Increment() continue diff --git a/grype/vulnerability_matcher_test.go b/grype/vulnerability_matcher_test.go index d63fb728c1ee..988b30ef9d64 100644 --- a/grype/vulnerability_matcher_test.go +++ b/grype/vulnerability_matcher_test.go @@ -10,20 +10,20 @@ import ( "github.com/stretchr/testify/require" "github.com/wagoodman/go-partybus" - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/matcher" - "github.com/anchore/grype/grype/db/v5/matcher/ruby" - "github.com/anchore/grype/grype/db/v5/search" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/grype/grypeerr" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher" + "github.com/anchore/grype/grype/matcher/apk" + "github.com/anchore/grype/grype/matcher/ruby" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/pkg/qualifier" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vex" "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/grype/vulnerability/mock" "github.com/anchore/grype/internal/bus" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" @@ -32,182 +32,89 @@ import ( "github.com/anchore/syft/syft/source" ) -type ack interface { - v5.VulnerabilityStoreReader - v5.VulnerabilityMetadataStoreReader - v5.VulnerabilityMatchExclusionStoreReader -} - -var _ ack = (*mockStore)(nil) - -type mockStore struct { - vulnerabilities map[string]map[string][]v5.Vulnerability - metadata map[string]map[string]*v5.VulnerabilityMetadata -} - -func (d *mockStore) GetVulnerabilityMatchExclusion(id string) ([]v5.VulnerabilityMatchExclusion, error) { - // panic("implement me") - return nil, nil -} - -// A mockStoreStubFn takes a reference to a mockStore and mutates it to contain -// a set of prescribed test data. -type mockStoreStubFn func(*mockStore) - -// newMockStore returns a new mock implementation of a Grype database store. If -// the stubFn parameter is not set to nil, the given stub function will be used -// to modify the data in the mock store, such as in preparation for tests. -func newMockStore(stubFn mockStoreStubFn) *mockStore { - d := &mockStore{ - vulnerabilities: make(map[string]map[string][]v5.Vulnerability), - metadata: make(map[string]map[string]*v5.VulnerabilityMetadata), - } - if stubFn != nil { - stubFn(d) - } - return d -} - -func (d *mockStore) GetVulnerabilityMetadata(id, namespace string) (*v5.VulnerabilityMetadata, error) { - return d.metadata[id][namespace], nil -} - -func (d *mockStore) GetAllVulnerabilityMetadata() (*[]v5.VulnerabilityMetadata, error) { - panic("implement me") -} - -func (d *mockStore) GetVulnerability(namespace, id string) ([]v5.Vulnerability, error) { - var results []v5.Vulnerability - for _, vulns := range d.vulnerabilities[namespace] { - for _, vuln := range vulns { - if vuln.ID == id { - results = append(results, vuln) - } - } - } - return results, nil -} - -func (d *mockStore) SearchForVulnerabilities(namespace, name string) ([]v5.Vulnerability, error) { - return d.vulnerabilities[namespace][name], nil -} - -func (d *mockStore) GetAllVulnerabilities() (*[]v5.Vulnerability, error) { - panic("implement me") -} - -func (d *mockStore) GetVulnerabilityNamespaces() ([]string, error) { - keys := make([]string, 0, len(d.vulnerabilities)) - for k := range d.vulnerabilities { - keys = append(keys, k) - } - - return keys, nil -} - -func defaultStubFn(d *mockStore) { - // METADATA ///////////////////////////////////////////////////////////////////////////////// - d.metadata["CVE-2014-fake-1"] = map[string]*v5.VulnerabilityMetadata{ - "debian:distro:debian:8": { - ID: "CVE-2014-fake-1", - Namespace: "debian:distro:debian:8", - Severity: "medium", - }, - } - - d.metadata["GHSA-2014-fake-3"] = map[string]*v5.VulnerabilityMetadata{ - "github:language:ruby": { - ID: "GHSA-2014-fake-3", - Namespace: "github:language:ruby", - Severity: "medium", +func testVulnerabilities() []vulnerability.Vulnerability { + return []vulnerability.Vulnerability{ + { + Reference: vulnerability.Reference{ + ID: "CVE-2014-fake-1", + Namespace: "debian:distro:debian:8", + Internal: vulnerability.Metadata{ + Severity: "medium", + }, + }, + PackageName: "neutron", + Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat), }, - } - - d.metadata["CVE-2014-fake-3"] = map[string]*v5.VulnerabilityMetadata{ - "nvd:cpe": { - ID: "CVE-2014-fake-3", - Namespace: "nvd:cpe", - Severity: "critical", + { + Reference: vulnerability.Reference{ + ID: "CVE-2013-fake-2", + Namespace: "debian:distro:debian:8", + }, + PackageName: "neutron", + Constraint: version.MustGetConstraint("< 2013.0.2-1", version.DebFormat), }, - } - - // VULNERABILITIES /////////////////////////////////////////////////////////////////////////// - d.vulnerabilities["debian:distro:debian:8"] = map[string][]v5.Vulnerability{ - "neutron": { - { - PackageName: "neutron", - Namespace: "debian:distro:debian:8", - VersionConstraint: "< 2014.1.3-6", - ID: "CVE-2014-fake-1", - VersionFormat: "deb", + { + Reference: vulnerability.Reference{ + ID: "GHSA-2014-fake-3", + Namespace: "github:language:ruby", + Internal: vulnerability.Metadata{ + Severity: "medium", + }, }, - { - PackageName: "neutron", - Namespace: "debian:distro:debian:8", - VersionConstraint: "< 2013.0.2-1", - ID: "CVE-2013-fake-2", - VersionFormat: "deb", + PackageName: "activerecord", + Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat), + RelatedVulnerabilities: []vulnerability.Reference{ + { + ID: "CVE-2014-fake-3", + Namespace: "nvd:cpe", + }, }, }, - } - d.vulnerabilities["github:language:ruby"] = map[string][]v5.Vulnerability{ - "activerecord": { - { - PackageName: "activerecord", - Namespace: "github:language:ruby", - VersionConstraint: "< 3.7.6", - ID: "GHSA-2014-fake-3", - VersionFormat: "unknown", - RelatedVulnerabilities: []v5.VulnerabilityReference{ - { - ID: "CVE-2014-fake-3", - Namespace: "nvd:cpe", - }, + { + Reference: vulnerability.Reference{ + ID: "CVE-2014-fake-3", + Namespace: "nvd:cpe", + Internal: vulnerability.Metadata{ + Severity: "critical", }, }, + PackageName: "activerecord", + Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", ""), + }, }, - } - d.vulnerabilities["nvd:cpe"] = map[string][]v5.Vulnerability{ - "activerecord": { - { - PackageName: "activerecord", - Namespace: "nvd:cpe", - VersionConstraint: "< 3.7.6", - ID: "CVE-2014-fake-3", - VersionFormat: "unknown", - CPEs: []string{ - "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", - }, + { + Reference: vulnerability.Reference{ + ID: "CVE-2014-fake-4", + Namespace: "nvd:cpe", }, - { - PackageName: "activerecord", - Namespace: "nvd:cpe", - VersionConstraint: "< 3.7.4", - ID: "CVE-2014-fake-4", - VersionFormat: "unknown", - CPEs: []string{ - "cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", - }, + PackageName: "activerecord", + Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""), }, - { - PackageName: "activerecord", - Namespace: "nvd:cpe", - VersionConstraint: "= 4.0.1", - ID: "CVE-2014-fake-5", - VersionFormat: "unknown", - CPEs: []string{ - "cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*", - }, + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-2014-fake-5", + Namespace: "nvd:cpe", }, - { - PackageName: "activerecord", - Namespace: "nvd:cpe", - VersionConstraint: "< 98SP3", - ID: "CVE-2014-fake-6", - VersionFormat: "unknown", - CPEs: []string{ - "cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*", - }, + PackageName: "activerecord", + Constraint: version.MustGetConstraint("= 4.0.1", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*", ""), + }, + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-2014-fake-6", + Namespace: "nvd:cpe", + }, + PackageName: "activerecord", + Constraint: version.MustGetConstraint("< 98SP3", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*", ""), }, }, } @@ -269,7 +176,7 @@ func Test_HasSeverityAtOrAbove(t *testing.T) { }, } - metadataProvider := v5.NewVulnerabilityMetadataProvider(newMockStore(defaultStubFn)) + metadataProvider := mock.VulnerabilityProvider(testVulnerabilities()...) for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -282,7 +189,7 @@ func Test_HasSeverityAtOrAbove(t *testing.T) { failOnSeverity = sev } - actual := HasSeverityAtOrAbove(metadataProvider, failOnSeverity, test.matches) + actual := hasSeverityAtOrAbove(metadataProvider, failOnSeverity, test.matches) if test.expectedResult != actual { t.Errorf("expected: %v got : %v", test.expectedResult, actual) @@ -292,14 +199,7 @@ func Test_HasSeverityAtOrAbove(t *testing.T) { } func TestVulnerabilityMatcher_FindMatches(t *testing.T) { - mkStr := newMockStore(defaultStubFn) - vp, err := v5.NewVulnerabilityProvider(mkStr) - require.NoError(t, err) - str := v5.ProviderStore{ - VulnerabilityProvider: vp, - VulnerabilityMetadataProvider: v5.NewVulnerabilityMetadataProvider(mkStr), - ExclusionProvider: v5.NewMatchExclusionProvider(mkStr), - } + vp := mock.VulnerabilityProvider(testVulnerabilities()...) neutron2013Pkg := pkg.Package{ ID: pkg.ID(uuid.NewString()), @@ -328,8 +228,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { } type fields struct { - Store v5.ProviderStore - Matchers []matcher.Matcher + Matchers []match.Matcher IgnoreRules []match.IgnoreRule FailSeverity *vulnerability.Severity NormalizeByCVE bool @@ -351,7 +250,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "no matches", fields: fields{ - Store: str, + //Store: str, Matchers: matcher.NewDefaultMatchers(matcher.Config{}), }, args: args{ @@ -374,7 +273,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "matches by exact-direct match (OS)", fields: fields{ - Store: str, + //Store: str, Matchers: matcher.NewDefaultMatchers(matcher.Config{}), }, args: args{ @@ -426,7 +325,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "fail on severity threshold", fields: fields{ - Store: str, + //Store: str, Matchers: matcher.NewDefaultMatchers(matcher.Config{}), FailSeverity: func() *vulnerability.Severity { x := vulnerability.LowSeverity @@ -482,7 +381,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "pass on severity threshold with VEX", fields: fields{ - Store: str, + //Store: str, Matchers: matcher.NewDefaultMatchers(matcher.Config{}), FailSeverity: func() *vulnerability.Severity { x := vulnerability.LowSeverity @@ -565,7 +464,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "matches by exact-direct match (language)", fields: fields{ - Store: str, + //Store: str, Matchers: matcher.NewDefaultMatchers(matcher.Config{ Ruby: ruby.MatcherConfig{ UseCPEs: true, @@ -597,17 +496,17 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { Details: match.Details{ { Type: match.CPEMatch, - SearchedBy: search.CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*", }, - Package: search.CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "activerecord", Version: "3.7.5", }, }, - Found: search.CPEResult{ + Found: match.CPEResult{ VulnerabilityID: "CVE-2014-fake-3", VersionConstraint: "< 3.7.6 (unknown)", CPEs: []string{ @@ -662,7 +561,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "normalize by cve", fields: fields{ - Store: str, + //Store: str, Matchers: matcher.NewDefaultMatchers( matcher.Config{ Ruby: ruby.MatcherConfig{ @@ -717,17 +616,17 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { }, { Type: match.CPEMatch, - SearchedBy: search.CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*", }, - Package: search.CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "activerecord", Version: "3.7.5", }, }, - Found: search.CPEResult{ + Found: match.CPEResult{ VulnerabilityID: "CVE-2014-fake-3", VersionConstraint: "< 3.7.6 (unknown)", CPEs: []string{ @@ -746,7 +645,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "normalize by cve -- ignore GHSA", fields: fields{ - Store: str, + //Store: str, Matchers: matcher.NewDefaultMatchers( matcher.Config{ Ruby: ruby.MatcherConfig{ @@ -786,17 +685,17 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { Details: match.Details{ { Type: match.CPEMatch, - SearchedBy: search.CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*", }, - Package: search.CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "activerecord", Version: "3.7.5", }, }, - Found: search.CPEResult{ + Found: match.CPEResult{ VulnerabilityID: "CVE-2014-fake-3", VersionConstraint: "< 3.7.6 (unknown)", CPEs: []string{ @@ -859,7 +758,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "normalize by cve -- ignore CVE", fields: fields{ - Store: str, + //Store: str, Matchers: matcher.NewDefaultMatchers( matcher.Config{ Ruby: ruby.MatcherConfig{ @@ -902,17 +801,17 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { Details: match.Details{ { Type: match.CPEMatch, - SearchedBy: search.CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*", }, - Package: search.CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "activerecord", Version: "3.7.5", }, }, - Found: search.CPEResult{ + Found: match.CPEResult{ VulnerabilityID: "CVE-2014-fake-3", VersionConstraint: "< 3.7.6 (unknown)", CPEs: []string{ @@ -979,7 +878,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "ignore CVE (not normalized by CVE)", fields: fields{ - Store: str, + //Store: str, Matchers: matcher.NewDefaultMatchers(matcher.Config{ Ruby: ruby.MatcherConfig{ UseCPEs: true, @@ -1059,17 +958,17 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { Details: match.Details{ { Type: match.CPEMatch, - SearchedBy: search.CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*", }, - Package: search.CPEPackageParameter{ + Package: match.CPEPackageParameter{ Name: "activerecord", Version: "3.7.5", }, }, - Found: search.CPEResult{ + Found: match.CPEResult{ VulnerabilityID: "CVE-2014-fake-3", VersionConstraint: "< 3.7.6 (unknown)", CPEs: []string{ @@ -1089,12 +988,12 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := &VulnerabilityMatcher{ - Store: tt.fields.Store, - Matchers: tt.fields.Matchers, - IgnoreRules: tt.fields.IgnoreRules, - FailSeverity: tt.fields.FailSeverity, - NormalizeByCVE: tt.fields.NormalizeByCVE, - VexProcessor: tt.fields.VexProcessor, + VulnerabilityProvider: vp, + Matchers: tt.fields.Matchers, + IgnoreRules: tt.fields.IgnoreRules, + FailSeverity: tt.fields.FailSeverity, + NormalizeByCVE: tt.fields.NormalizeByCVE, + VexProcessor: tt.fields.VexProcessor, } listener := &busListener{} @@ -1111,9 +1010,11 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { } var opts = []cmp.Option{ + cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(match.Match{}), cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"), - cmpopts.IgnoreFields(pkg.Package{}, "Locations"), + cmpopts.IgnoreFields(vulnerability.Reference{}, "Internal"), + cmpopts.IgnoreFields(pkg.Package{}, "Locations", "Distro"), cmpopts.IgnoreUnexported(match.IgnoredMatch{}), } @@ -1134,18 +1035,17 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { func Test_indexFalsePositivesByLocation(t *testing.T) { cases := []struct { name string - d distro.Distro pkgs []pkg.Package - stubFunc mockStoreStubFn + vulns []vulnerability.Vulnerability expectedResult map[string][]string errAssertion assert.ErrorAssertionFunc }{ { name: "false positive in wolfi package adds index entry", - d: distro.Distro{Type: distro.Wolfi}, pkgs: []pkg.Package{ { - Name: "foo", + Name: "foo", + Distro: &distro.Distro{Type: distro.Wolfi}, Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ { Path: "/bin/foo-binary", @@ -1153,18 +1053,15 @@ func Test_indexFalsePositivesByLocation(t *testing.T) { }}, }, }, - stubFunc: func(d *mockStore) { - d.vulnerabilities["wolfi:distro:wolfi:rolling"] = map[string][]v5.Vulnerability{ - "foo": { - { - ID: "GHSA-2014-fake-3", - PackageName: "foo", - Namespace: "wolfi:distro:wolfi:rolling", - VersionConstraint: "< 0", - VersionFormat: "apk", - }, + vulns: []vulnerability.Vulnerability{ + { + Reference: vulnerability.Reference{ + ID: "GHSA-2014-fake-3", + Namespace: "wolfi:distro:wolfi:rolling", }, - } + PackageName: "foo", + Constraint: version.MustGetConstraint("< 0", version.ApkFormat), + }, }, expectedResult: map[string][]string{ "/bin/foo-binary": {"GHSA-2014-fake-3"}, @@ -1173,10 +1070,10 @@ func Test_indexFalsePositivesByLocation(t *testing.T) { }, { name: "false positive in wolfi subpackage adds index entry", - d: distro.Distro{Type: distro.Wolfi}, pkgs: []pkg.Package{ { - Name: "subpackage-foo", + Name: "subpackage-foo", + Distro: &distro.Distro{Type: distro.Wolfi}, Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ { Path: "/bin/foo-subpackage-binary", @@ -1189,18 +1086,15 @@ func Test_indexFalsePositivesByLocation(t *testing.T) { }, }, }, - stubFunc: func(d *mockStore) { - d.vulnerabilities["wolfi:distro:wolfi:rolling"] = map[string][]v5.Vulnerability{ - "origin-foo": { - { - ID: "GHSA-2014-fake-3", - PackageName: "foo", - Namespace: "wolfi:distro:wolfi:rolling", - VersionConstraint: "< 0", - VersionFormat: "apk", - }, + vulns: []vulnerability.Vulnerability{ + { + Reference: vulnerability.Reference{ + ID: "GHSA-2014-fake-3", + Namespace: "wolfi:distro:wolfi:rolling", }, - } + PackageName: "origin-foo", + Constraint: version.MustGetConstraint("< 0", version.ApkFormat), + }, }, expectedResult: map[string][]string{ "/bin/foo-subpackage-binary": {"GHSA-2014-fake-3"}, @@ -1209,10 +1103,10 @@ func Test_indexFalsePositivesByLocation(t *testing.T) { }, { name: "fixed vuln (not a false positive) in wolfi package", - d: distro.Distro{Type: distro.Wolfi}, pkgs: []pkg.Package{ { - Name: "foo", + Name: "foo", + Distro: &distro.Distro{Type: distro.Wolfi}, Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ { Path: "/bin/foo-binary", @@ -1220,28 +1114,25 @@ func Test_indexFalsePositivesByLocation(t *testing.T) { }}, }, }, - stubFunc: func(d *mockStore) { - d.vulnerabilities["wolfi:distro:wolfi:rolling"] = map[string][]v5.Vulnerability{ - "foo": { - { - ID: "GHSA-2014-fake-3", - PackageName: "foo", - Namespace: "wolfi:distro:wolfi:rolling", - VersionConstraint: "< 1.2.3-r4", - VersionFormat: "apk", - }, + vulns: []vulnerability.Vulnerability{ + { + Reference: vulnerability.Reference{ + ID: "GHSA-2014-fake-3", + Namespace: "wolfi:distro:wolfi:rolling", }, - } + PackageName: "foo", + Constraint: version.MustGetConstraint("< 1.2.3-r4", version.ApkFormat), + }, }, expectedResult: map[string][]string{}, errAssertion: assert.NoError, }, { name: "no vuln data for wolfi package", - d: distro.Distro{Type: distro.Wolfi}, pkgs: []pkg.Package{ { - Name: "foo", + Name: "foo", + Distro: &distro.Distro{Type: distro.Wolfi}, Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ { Path: "/bin/foo-binary", @@ -1249,33 +1140,28 @@ func Test_indexFalsePositivesByLocation(t *testing.T) { }}, }, }, - stubFunc: func(d *mockStore) { - d.vulnerabilities["wolfi:distro:wolfi:rolling"] = map[string][]v5.Vulnerability{} - }, + vulns: []vulnerability.Vulnerability{}, expectedResult: map[string][]string{}, errAssertion: assert.NoError, }, { name: "no files listed for a wolfi package", - d: distro.Distro{Type: distro.Wolfi}, pkgs: []pkg.Package{ { Name: "foo", + Distro: &distro.Distro{Type: distro.Wolfi}, Metadata: pkg.ApkMetadata{Files: nil}, }, }, - stubFunc: func(d *mockStore) { - d.vulnerabilities["wolfi:distro:wolfi:rolling"] = map[string][]v5.Vulnerability{ - "foo": { - { - ID: "GHSA-2014-fake-3", - PackageName: "foo", - Namespace: "wolfi:distro:wolfi:rolling", - VersionConstraint: "< 0", - VersionFormat: "apk", - }, + vulns: []vulnerability.Vulnerability{ + { + Reference: vulnerability.Reference{ + ID: "GHSA-2014-fake-3", + Namespace: "wolfi:distro:wolfi:rolling", }, - } + PackageName: "foo", + Constraint: version.MustGetConstraint("< 0", version.ApkFormat), + }, }, expectedResult: map[string][]string{}, errAssertion: assert.NoError, @@ -1284,25 +1170,31 @@ func Test_indexFalsePositivesByLocation(t *testing.T) { for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { - s := createMockStore(t, tt.stubFunc) - actualResult, err := indexFalsePositivesByLocation(&tt.d, tt.pkgs, s) - tt.errAssertion(t, err) - assert.Equal(t, tt.expectedResult, actualResult) - }) - } -} + // create mock vulnerability provider + vp := mock.VulnerabilityProvider(tt.vulns...) + apkMatcher := &apk.Matcher{} -func createMockStore(t *testing.T, fn mockStoreStubFn) v5.ProviderStore { - t.Helper() - - mkStr := newMockStore(fn) - vp, err := v5.NewVulnerabilityProvider(mkStr) - require.NoError(t, err) + var allMatches []match.Match + var allIgnores []match.IgnoredMatch + for _, p := range tt.pkgs { + matches, ignores, err := apkMatcher.Match(vp, p) + require.NoError(t, err) + allMatches = append(allMatches, matches...) + allIgnores = append(allIgnores, ignores...) + } - return v5.ProviderStore{ - VulnerabilityProvider: vp, - VulnerabilityMetadataProvider: v5.NewVulnerabilityMetadataProvider(mkStr), - ExclusionProvider: v5.NewMatchExclusionProvider(mkStr), + actualResult := map[string][]string{} + for _, ignore := range allIgnores { + apkMetadata, ok := ignore.Package.Metadata.(pkg.ApkMetadata) + require.True(t, ok) + for _, f := range apkMetadata.Files { + for _, r := range ignore.AppliedIgnoreRules { + actualResult[f.Path] = append(actualResult[f.Path], r.Vulnerability) + } + } + } + assert.Equal(t, tt.expectedResult, actualResult) + }) } } @@ -1411,7 +1303,34 @@ func Test_filterMatchesUsingDistroFalsePositives(t *testing.T) { for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { - actual := filterMatchesUsingDistroFalsePositives(tt.inputMatches, tt.fpIndex) + var allIgnores []match.IgnoredMatch + for path, cves := range tt.fpIndex { + for _, cve := range cves { + allIgnores = append(allIgnores, match.IgnoredMatch{ + Match: match.Match{ + Package: pkg.Package{ + Metadata: pkg.ApkMetadata{ + Files: []pkg.ApkFileRecord{ + { + Path: path, + }, + }, + }, + }, + }, + AppliedIgnoreRules: []match.IgnoreRule{ + { + Vulnerability: cve, + }, + }, + }) + } + } + + filter := ignoredMatchFilter(allIgnores) + + actual, _ := match.ApplyIgnoreFilters(tt.inputMatches, filter) + assert.Equal(t, tt.expected, actual) }) } diff --git a/test/cli/utils_test.go b/test/cli/utils_test.go index 695da543cfcb..6227560283e9 100644 --- a/test/cli/utils_test.go +++ b/test/cli/utils_test.go @@ -50,28 +50,50 @@ func grypeCommandHasConfigArg(args ...string) bool { return false } -func getGrypeSnapshotLocation(tb testing.TB, goOS string) string { - if os.Getenv("GRYPE_BINARY_LOCATION") != "" { - // GRYPE_BINARY_LOCATION is the absolute path to the snapshot binary - return os.Getenv("GRYPE_BINARY_LOCATION") +func getGrypeSnapshotLocation(t testing.TB, goOS string) string { + // GRYPE_BINARY_LOCATION is the absolute path to the snapshot binary + const envKey = "GRYPE_BINARY_LOCATION" + if os.Getenv(envKey) != "" { + return os.Getenv(envKey) } + loc := getGrypeBinaryLocationByOS(t, goOS) + buildBinary(t, loc) + _ = os.Setenv(envKey, loc) + return loc +} +func getGrypeBinaryLocationByOS(t testing.TB, goOS string) string { // note: for amd64 we need to update the snapshot location with the v1 suffix // see : https://goreleaser.com/customization/build/#why-is-there-a-_v1-suffix-on-amd64-builds archPath := runtime.GOARCH if runtime.GOARCH == "amd64" { archPath = fmt.Sprintf("%s_v1", archPath) } - + // note: there is a subtle - vs _ difference between these versions switch goOS { case "darwin", "linux": - return path.Join(repoRoot(tb), fmt.Sprintf("snapshot/%s-build_%s_%s/grype", goOS, goOS, archPath)) + return path.Join(repoRoot(t), fmt.Sprintf("snapshot/%s-build_%s_%s/grype", goOS, goOS, archPath)) default: - tb.Fatalf("unsupported OS: %s", runtime.GOOS) + t.Fatalf("unsupported OS: %s", runtime.GOOS) } return "" } +func buildBinary(t testing.TB, loc string) { + wd, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(repoRoot(t))) + defer func() { + require.NoError(t, os.Chdir(wd)) + }() + t.Log("Building grype...") + c := exec.Command("go", "build", "-o", loc, "./cmd/grype") + c.Stdout = os.Stdout + c.Stderr = os.Stderr + c.Stdin = os.Stdin + require.NoError(t, c.Run()) +} + func getDockerRunCommand(tb testing.TB, args ...string) *exec.Cmd { tb.Helper() diff --git a/test/integration/db_mock_test.go b/test/integration/db_mock_test.go index 363e78e288b2..9aef8bb21404 100644 --- a/test/integration/db_mock_test.go +++ b/test/integration/db_mock_test.go @@ -1,285 +1,224 @@ package integration import ( - v5 "github.com/anchore/grype/grype/db/v5" + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/grype/vulnerability/mock" + "github.com/anchore/syft/syft/cpe" ) -// integrity check -var _ v5.VulnerabilityStoreReader = &mockStore{} - -type mockStore struct { - normalizedPackageNames map[string]map[string]string - backend map[string]map[string][]v5.Vulnerability -} - -func (s *mockStore) GetVulnerability(namespace, id string) ([]v5.Vulnerability, error) { - // TODO implement me - panic("implement me") -} - -func (s *mockStore) GetVulnerabilityNamespaces() ([]string, error) { - var results []string - for k := range s.backend { - results = append(results, k) - } - - return results, nil -} - -func (s *mockStore) GetVulnerabilityMatchExclusion(id string) ([]v5.VulnerabilityMatchExclusion, error) { - return nil, nil -} - -func newMockDbStore() *mockStore { - return &mockStore{ - normalizedPackageNames: map[string]map[string]string{ - "github:language:python": { - "pygments": "pygments", - "my-package": "my-package", +func newMockDbProvider() vulnerability.Provider { + return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ + { + Reference: vulnerability.Reference{ + ID: "CVE-jdk", + Namespace: "nvd:cpe", + }, + PackageName: "jdk", + Constraint: version.MustGetConstraint("< 1.8.0_401", version.JVMFormat), + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*", "")}, + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-alpine-libvncserver", + Namespace: "nvd:cpe", + }, + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("< 0.9.10", version.UnknownFormat), + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:lib_vnc_project-(server):libvncserver:*:*:*:*:*:*:*:*", "")}, + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-bogus-my-package-1", + Namespace: "nvd:cpe", + }, + PackageName: "my-package", + Constraint: version.MustGetConstraint("< 2.0", version.UnknownFormat), + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:bogus:my-package:*:*:*:*:*:*:something:*", "")}, + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-bogus-my-package-2-never-match", + Namespace: "nvd:cpe", + }, + PackageName: "my-package", + Constraint: version.MustGetConstraint("< 2.0", version.UnknownFormat), + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:something-wrong:my-package:*:*:*:*:*:*:something:*", "")}, + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-alpine-libvncserver", + Namespace: "alpine:distro:alpine:3.12", }, - "github:language:dotnet": { - "AWSSDK.Core": "awssdk.core", + PackageName: "libvncserver", + Constraint: version.MustGetConstraint("< 0.9.10", version.UnknownFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-azure-autorest-vuln-false-positive", + Namespace: "alpine:distro:alpine:3.12", }, + PackageName: "ko", + Constraint: version.MustGetConstraint("< 0", version.ApkFormat), }, - backend: map[string]map[string][]v5.Vulnerability{ - "nvd:cpe": { - "jdk": []v5.Vulnerability{ - { - ID: "CVE-jdk", - PackageName: "jdk", - VersionConstraint: "< 1.8.0_401", - VersionFormat: "jvm", - CPEs: []string{"cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*"}, - }, - }, - "libvncserver": []v5.Vulnerability{ - { - ID: "CVE-alpine-libvncserver", - VersionConstraint: "< 0.9.10", - VersionFormat: "unknown", - CPEs: []string{"cpe:2.3:a:lib_vnc_project-(server):libvncserver:*:*:*:*:*:*:*:*"}, - }, - }, - "my-package": []v5.Vulnerability{ - { - ID: "CVE-bogus-my-package-1", - VersionConstraint: "< 2.0", - VersionFormat: "unknown", - CPEs: []string{"cpe:2.3:a:bogus:my-package:*:*:*:*:*:*:something:*"}, - }, - { - ID: "CVE-bogus-my-package-2-never-match", - VersionConstraint: "< 2.0", - VersionFormat: "unknown", - CPEs: []string{"cpe:2.3:a:something-wrong:my-package:*:*:*:*:*:*:something:*"}, - }, - }, + { + Reference: vulnerability.Reference{ + ID: "CVE-npm-false-positive-in-apk-subpackage", + Namespace: "alpine:distro:alpine:3.12", }, - "alpine:distro:alpine:3.12": { - "libvncserver": []v5.Vulnerability{ - { - ID: "CVE-alpine-libvncserver", - VersionConstraint: "< 0.9.10", - VersionFormat: "unknown", - }, - }, - "ko": []v5.Vulnerability{ - { - ID: "CVE-azure-autorest-vuln-false-positive", - VersionConstraint: "< 0", - VersionFormat: "apk", - }, - }, - "npm-apk-package-with-false-positive": []v5.Vulnerability{ - { - ID: "CVE-npm-false-positive-in-apk-subpackage", - VersionConstraint: "< 0", - VersionFormat: "apk", - }, - }, + PackageName: "npm-apk-package-with-false-positive", + Constraint: version.MustGetConstraint("< 0", version.ApkFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-gentoo-skopeo", + Namespace: "gentoo:distro:gentoo:2.8", }, - "gentoo:distro:gentoo:2.8": { - "app-containers/skopeo": []v5.Vulnerability{ - { - ID: "CVE-gentoo-skopeo", - VersionConstraint: "< 1.6.0", - VersionFormat: "unknown", - }, - }, + PackageName: "app-containers/skopeo", + Constraint: version.MustGetConstraint("< 1.6.0", version.UnknownFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-coverage-main-module-vuln", + Namespace: "github:language:go", }, - "github:language:go": { - "github.com/anchore/coverage": []v5.Vulnerability{ - { - ID: "CVE-coverage-main-module-vuln", - VersionConstraint: "< 1.4.0", - VersionFormat: "unknown", - }, - }, - "github.com/google/uuid": []v5.Vulnerability{ - { - ID: "CVE-uuid-vuln", - VersionConstraint: "< 1.4.0", - VersionFormat: "unknown", - }, - }, - "github.com/azure/go-autorest/autorest": []v5.Vulnerability{ - { - ID: "CVE-azure-autorest-vuln-false-positive", - VersionConstraint: "< 0.11.30", - VersionFormat: "unknown", - }, - }, + PackageName: "github.com/anchore/coverage", + Constraint: version.MustGetConstraint("< 1.4.0", version.UnknownFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-uuid-vuln", + Namespace: "github:language:go", }, - "github:language:idris": { - "my-package": []v5.Vulnerability{ - { - ID: "CVE-bogus-my-package-2-idris", - VersionConstraint: "< 2.0", - VersionFormat: "unknown", - }, - }, + PackageName: "github.com/google/uuid", + Constraint: version.MustGetConstraint("< 1.4.0", version.UnknownFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-azure-autorest-vuln-false-positive", + Namespace: "github:language:go", }, - "github:language:javascript": { - "npm": []v5.Vulnerability{ - { - ID: "CVE-javascript-validator", - VersionConstraint: "> 5, < 7.2.1", - VersionFormat: "unknown", - }, - }, - "npm-apk-subpackage-with-false-positive": []v5.Vulnerability{ - { - ID: "CVE-npm-false-positive-in-apk-subpackage", - VersionConstraint: "< 2.0.0", - VersionFormat: "unknown", - }, - }, + PackageName: "github.com/azure/go-autorest/autorest", + Constraint: version.MustGetConstraint("< 0.11.30", version.UnknownFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-bogus-my-package-2-idris", + Namespace: "github:language:idris", }, - "github:language:python": { - "pygments": []v5.Vulnerability{ - { - ID: "CVE-python-pygments", - VersionConstraint: "< 2.6.2", - VersionFormat: "python", - }, - }, - "my-package": []v5.Vulnerability{}, + PackageName: "my-package", + Constraint: version.MustGetConstraint("< 2.0", version.UnknownFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-javascript-validator", + Namespace: "github:language:javascript", }, - "github:language:ruby": { - "bundler": []v5.Vulnerability{ - { - ID: "CVE-ruby-bundler", - VersionConstraint: "> 2.0.0, <= 2.1.4", - VersionFormat: "gemfile", - }, - }, + PackageName: "npm", + Constraint: version.MustGetConstraint("> 5, < 7.2.1", version.UnknownFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-npm-false-positive-in-apk-subpackage", + Namespace: "github:language:javascript", }, - "github:language:java": { - "org.anchore:example-java-app-maven": []v5.Vulnerability{ - { - ID: "CVE-java-example-java-app", - VersionConstraint: ">= 0.0.1, < 1.2.0", - VersionFormat: "unknown", - }, - }, + PackageName: "npm-apk-subpackage-with-false-positive", + Constraint: version.MustGetConstraint("< 2.0.0", version.UnknownFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-python-pygments", + Namespace: "github:language:python", }, - "github:language:dotnet": { - "awssdk.core": []v5.Vulnerability{ - { - ID: "CVE-dotnet-sample", - VersionConstraint: ">= 3.7.0.0, < 3.7.12.0", - VersionFormat: "dotnet", - }, - }, + PackageName: "pygments", + Constraint: version.MustGetConstraint("< 2.6.2", version.PythonFormat), + }, + //{ + // Reference: vulnerability.Reference{ + // ID: "CVE-my-package-python", + // Namespace: "github:language:python", + // }, + // PackageName: "my-package", + //}, + { + Reference: vulnerability.Reference{ + ID: "CVE-ruby-bundler", + Namespace: "github:language:ruby", // github:language:gem ?? + }, + PackageName: "bundler", + Constraint: version.MustGetConstraint("> 2.0.0, <= 2.1.4", version.UnknownFormat), //version.GemFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-java-example-java-app", + Namespace: "github:language:java", }, - "github:language:haskell": { - "shellcheck": []v5.Vulnerability{ - { - ID: "CVE-haskell-sample", - VersionConstraint: "< 0.9.0", - VersionFormat: "haskell", - }, - }, + PackageName: "org.anchore:example-java-app-maven", + Constraint: version.MustGetConstraint(">= 0.0.1, < 1.2.0", version.UnknownFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-dotnet-sample", + Namespace: "github:language:dotnet", }, - "github:language:rust": { - "hello-auditable": []v5.Vulnerability{ - { - ID: "CVE-rust-sample-1", - VersionConstraint: "< 0.2.0", - VersionFormat: "unknown", - }, - }, - "auditable": []v5.Vulnerability{ - { - ID: "CVE-rust-sample-2", - VersionConstraint: "< 0.2.0", - VersionFormat: "unknown", - }, - }, + PackageName: "awssdk.core", + Constraint: version.MustGetConstraint(">= 3.7.0.0, < 3.7.12.0", version.UnknownFormat), // was: "dotnet" + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-haskell-sample", + Namespace: "github:language:haskell", }, - "debian:distro:debian:8": { - "apt-dev": []v5.Vulnerability{ - { - ID: "CVE-dpkg-apt", - VersionConstraint: "<= 1.8.2", - VersionFormat: "dpkg", - }, - }, + PackageName: "shellcheck", + Constraint: version.MustGetConstraint("< 0.9.0", version.UnknownFormat), // was: "haskell" + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-rust-sample-1", + Namespace: "github:language:rust", }, - "redhat:distro:redhat:8": { - "dive": []v5.Vulnerability{ - { - ID: "CVE-rpmdb-dive", - VersionConstraint: "<= 1.0.42", - VersionFormat: "rpm", - }, - }, + PackageName: "hello-auditable", + Constraint: version.MustGetConstraint("< 0.2.0", version.UnknownFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-rust-sample-2", + Namespace: "github:language:rust", }, - "msrc:distro:windows:10816": { - "10816": []v5.Vulnerability{ - { - ID: "CVE-2016-3333", - VersionConstraint: "3200970 || 878787 || base", - VersionFormat: "kb", - }, - }, + PackageName: "auditable", + Constraint: version.MustGetConstraint("< 0.2.0", version.UnknownFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-dpkg-apt", + Namespace: "debian:distro:debian:8", }, - "sles:distro:sles:12.5": { - "dive": []v5.Vulnerability{ - { - ID: "CVE-rpmdb-dive", - VersionConstraint: "<= 1.0.42", - VersionFormat: "rpm", - }, - }, + PackageName: "apt-dev", + Constraint: version.MustGetConstraint("<= 1.8.2", version.DebFormat), // was: "dpkg" + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-rpmdb-dive", + Namespace: "redhat:distro:redhat:8", }, + PackageName: "dive", + Constraint: version.MustGetConstraint("<= 1.0.42", version.RpmFormat), }, - } -} - -func (s *mockStore) SearchForVulnerabilities(namespace, name string) ([]v5.Vulnerability, error) { - namespaceMap := s.backend[namespace] - if namespaceMap == nil { - return nil, nil - } - entries, ok := namespaceMap[name] - if !ok { - return entries, nil - } - for i := range entries { - entries[i].Namespace = namespace - } - return entries, nil -} - -func (s *mockStore) GetAllVulnerabilities() (*[]v5.Vulnerability, error) { - return nil, nil -} - -func (s *mockStore) GetVulnerabilityMetadata(id string, namespace string) (*v5.VulnerabilityMetadata, error) { - return nil, nil -} - -func (s *mockStore) GetAllVulnerabilityMetadata() (*[]v5.VulnerabilityMetadata, error) { - return nil, nil + { + Reference: vulnerability.Reference{ + ID: "CVE-2016-3333", + Namespace: "msrc:distro:windows:10816", + }, + PackageName: "10816", + Constraint: version.MustGetConstraint("3200970 || 878787 || base", version.KBFormat), + }, + { + Reference: vulnerability.Reference{ + ID: "CVE-rpmdb-dive", + Namespace: "sles:distro:sles:12.5", + }, + PackageName: "dive", + Constraint: version.MustGetConstraint("<= 1.0.42", version.RpmFormat), + }, + }...) } diff --git a/test/integration/match_by_image_test.go b/test/integration/match_by_image_test.go index a7b86491f0d4..c59b60c754dc 100644 --- a/test/integration/match_by_image_test.go +++ b/test/integration/match_by_image_test.go @@ -11,19 +11,18 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/grype/grype" - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/matcher" - "github.com/anchore/grype/grype/db/v5/matcher/dotnet" - "github.com/anchore/grype/grype/db/v5/matcher/golang" - "github.com/anchore/grype/grype/db/v5/matcher/java" - "github.com/anchore/grype/grype/db/v5/matcher/javascript" - "github.com/anchore/grype/grype/db/v5/matcher/python" - "github.com/anchore/grype/grype/db/v5/matcher/ruby" - "github.com/anchore/grype/grype/db/v5/matcher/rust" - "github.com/anchore/grype/grype/db/v5/matcher/stock" - "github.com/anchore/grype/grype/db/v5/search" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher" + "github.com/anchore/grype/grype/matcher/dotnet" + "github.com/anchore/grype/grype/matcher/golang" + "github.com/anchore/grype/grype/matcher/java" + "github.com/anchore/grype/grype/matcher/javascript" + "github.com/anchore/grype/grype/matcher/python" + "github.com/anchore/grype/grype/matcher/ruby" + "github.com/anchore/grype/grype/matcher/rust" + "github.com/anchore/grype/grype/matcher/stock" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/vex" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/stringutil" @@ -36,21 +35,22 @@ import ( "github.com/anchore/syft/syft/source" ) -func addAlpineMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { +func addAlpineMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/lib/apk/db/installed") if len(packages) != 3 { t.Logf("Alpine Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (alpine)") } thePkg := pkg.New(packages[0]) - theVuln := theStore.backend["alpine:distro:alpine:3.12"][thePkg.Name][0] - vulnObj, err := v5.NewVulnerability(theVuln) + vulns, err := theStore.FindVulnerabilities(byNamespace("alpine:distro:alpine:3.12"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) + require.NotEmpty(t, vulns) + vulnObj := vulns[0] theResult.Add(match.Match{ // note: we are matching on the secdb record, not NVD primarily - Vulnerability: *vulnObj, + Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { @@ -98,19 +98,20 @@ func addAlpineMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co }) } -func addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { +func addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/javascript/pkg-json/package.json") if len(packages) != 1 { t.Logf("Javascript Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (javascript)") } thePkg := pkg.New(packages[0]) - theVuln := theStore.backend["github:language:javascript"][thePkg.Name][0] - vulnObj, err := v5.NewVulnerability(theVuln) + vulns, err := theStore.FindVulnerabilities(byNamespace("github:language:javascript"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) + require.NotEmpty(t, vulns) + vulnObj := vulns[0] theResult.Add(match.Match{ - Vulnerability: *vulnObj, + Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { @@ -134,7 +135,7 @@ func addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPk }) } -func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { +func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/python/dist-info/METADATA") if len(packages) != 1 { for _, p := range packages { @@ -144,13 +145,13 @@ func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co t.Fatalf("problem with upstream syft cataloger (python)") } thePkg := pkg.New(packages[0]) - normalizedName := theStore.normalizedPackageNames["github:language:python"][thePkg.Name] - theVuln := theStore.backend["github:language:python"][normalizedName][0] - vulnObj, err := v5.NewVulnerability(theVuln) + vulns, err := theStore.FindVulnerabilities(byNamespace("github:language:python"), search.ByPackageName(strings.ToLower(thePkg.Name))) require.NoError(t, err) + require.NotEmpty(t, vulns) + vulnObj := vulns[0] theResult.Add(match.Match{ - Vulnerability: *vulnObj, + Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { @@ -174,7 +175,7 @@ func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co }) } -func addDotnetMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { +func addDotnetMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/dotnet/TestLibrary.deps.json") if len(packages) != 2 { // TestLibrary + AWSSDK.Core for _, p := range packages { @@ -184,13 +185,13 @@ func addDotnetMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co t.Fatalf("problem with upstream syft cataloger (dotnet)") } thePkg := pkg.New(packages[1]) - normalizedName := theStore.normalizedPackageNames["github:language:dotnet"][thePkg.Name] - theVuln := theStore.backend["github:language:dotnet"][normalizedName][0] - vulnObj, err := v5.NewVulnerability(theVuln) + vulns, err := theStore.FindVulnerabilities(byNamespace("github:language:dotnet"), search.ByPackageName(strings.ToLower(thePkg.Name))) require.NoError(t, err) + require.NotEmpty(t, vulns) + vulnObj := vulns[0] theResult.Add(match.Match{ - Vulnerability: *vulnObj, + Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { @@ -214,19 +215,20 @@ func addDotnetMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co }) } -func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { +func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/ruby/specifications/bundler.gemspec") if len(packages) != 1 { t.Logf("Ruby Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (ruby)") } thePkg := pkg.New(packages[0]) - theVuln := theStore.backend["github:language:ruby"][thePkg.Name][0] - vulnObj, err := v5.NewVulnerability(theVuln) + vulns, err := theStore.FindVulnerabilities(byNamespace("github:language:ruby"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) + require.NotEmpty(t, vulns) + vulnObj := vulns[0] theResult.Add(match.Match{ - Vulnerability: *vulnObj, + Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { @@ -250,7 +252,7 @@ func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll }) } -func addGolangMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { +func addGolangMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider, theResult *match.Matches) { modPackages := catalog.PackagesByPath("/golang/go.mod") if len(modPackages) != 1 { t.Logf("Golang Mod Packages: %+v", modPackages) @@ -279,12 +281,13 @@ func addGolangMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co } thePkg := pkg.New(p) - theVuln := theStore.backend["github:language:go"][thePkg.Name][0] - vulnObj, err := v5.NewVulnerability(theVuln) + vulns, err := theStore.FindVulnerabilities(byNamespace("github:language:go"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) + require.NotEmpty(t, vulns) + vulnObj := vulns[0] theResult.Add(match.Match{ - Vulnerability: *vulnObj, + Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { @@ -310,7 +313,7 @@ func addGolangMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co } } -func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { +func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider, theResult *match.Matches) { packages := make([]syftPkg.Package, 0) for p := range catalog.Enumerate(syftPkg.JavaPkg) { packages = append(packages, p) @@ -325,13 +328,13 @@ func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll lookup := groupId + ":" + theSyftPkg.Name thePkg := pkg.New(theSyftPkg) - - theVuln := theStore.backend["github:language:java"][lookup][0] - vulnObj, err := v5.NewVulnerability(theVuln) + vulns, err := theStore.FindVulnerabilities(byNamespace("github:language:java"), search.ByPackageName(lookup)) require.NoError(t, err) + require.NotEmpty(t, vulns) + vulnObj := vulns[0] theResult.Add(match.Match{ - Vulnerability: *vulnObj, + Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { @@ -355,7 +358,7 @@ func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll }) } -func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { +func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/var/lib/dpkg/status") if len(packages) != 1 { t.Logf("Dpkg Packages: %+v", packages) @@ -363,12 +366,13 @@ func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll } thePkg := pkg.New(packages[0]) // NOTE: this is an indirect match, in typical debian style - theVuln := theStore.backend["debian:distro:debian:8"][thePkg.Name+"-dev"][0] - vulnObj, err := v5.NewVulnerability(theVuln) + vulns, err := theStore.FindVulnerabilities(byNamespace("debian:distro:debian:8"), search.ByPackageName(thePkg.Name+"-dev")) require.NoError(t, err) + require.NotEmpty(t, vulns) + vulnObj := vulns[0] theResult.Add(match.Match{ - Vulnerability: *vulnObj, + Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { @@ -395,19 +399,20 @@ func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll }) } -func addPortageMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { +func addPortageMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/var/db/pkg/app-containers/skopeo-1.5.1/CONTENTS") if len(packages) != 1 { t.Logf("Portage Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (portage)") } thePkg := pkg.New(packages[0]) - theVuln := theStore.backend["gentoo:distro:gentoo:2.8"][thePkg.Name][0] - vulnObj, err := v5.NewVulnerability(theVuln) + vulns, err := theStore.FindVulnerabilities(byNamespace("gentoo:distro:gentoo:2.8"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) + require.NotEmpty(t, vulns) + vulnObj := vulns[0] theResult.Add(match.Match{ - Vulnerability: *vulnObj, + Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { @@ -434,19 +439,20 @@ func addPortageMatches(t *testing.T, theSource source.Source, catalog *syftPkg.C }) } -func addRhelMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { +func addRhelMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/var/lib/rpm/Packages") if len(packages) != 1 { t.Logf("RPMDB Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (RPMDB)") } thePkg := pkg.New(packages[0]) - theVuln := theStore.backend["redhat:distro:redhat:8"][thePkg.Name][0] - vulnObj, err := v5.NewVulnerability(theVuln) + vulns, err := theStore.FindVulnerabilities(byNamespace("redhat:distro:redhat:8"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) + require.NotEmpty(t, vulns) + vulnObj := vulns[0] theResult.Add(match.Match{ - Vulnerability: *vulnObj, + Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { @@ -473,20 +479,22 @@ func addRhelMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll }) } -func addSlesMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { +func addSlesMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/var/lib/rpm/Packages") if len(packages) != 1 { t.Logf("Sles Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (RPMDB)") } thePkg := pkg.New(packages[0]) - theVuln := theStore.backend["redhat:distro:redhat:8"][thePkg.Name][0] - vulnObj, err := v5.NewVulnerability(theVuln) + + vulns, err := theStore.FindVulnerabilities(byNamespace("redhat:distro:redhat:8"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) + require.NotEmpty(t, vulns) + vulnObj := vulns[0] vulnObj.Namespace = "sles:distro:sles:12.5" theResult.Add(match.Match{ - Vulnerability: *vulnObj, + Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { @@ -513,19 +521,20 @@ func addSlesMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll }) } -func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { +func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/haskell/stack.yaml") if len(packages) < 1 { - t.Logf("Haskel Packages: %+v", packages) + t.Logf("Haskell Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (haskell)") } thePkg := pkg.New(packages[0]) - theVuln := theStore.backend["github:language:haskell"][strings.ToLower(thePkg.Name)][0] - vulnObj, err := v5.NewVulnerability(theVuln) + vulns, err := theStore.FindVulnerabilities(byNamespace("github:language:haskell"), search.ByPackageName(strings.ToLower(thePkg.Name))) require.NoError(t, err) + require.NotEmpty(t, vulns) + vulnObj := vulns[0] theResult.Add(match.Match{ - Vulnerability: *vulnObj, + Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { @@ -549,7 +558,7 @@ func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.C }) } -func addJvmMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { +func addJvmMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/opt/java/openjdk/release") if len(packages) < 1 { t.Logf("JVM Packages: %+v", packages) @@ -558,28 +567,31 @@ func addJvmMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Colle for _, p := range packages { thePkg := pkg.New(p) - theVuln := theStore.backend["nvd:cpe"][strings.ToLower(thePkg.Name)][0] - vulnObj, err := v5.NewVulnerability(theVuln) + vulns, err := theStore.FindVulnerabilities(byNamespace("nvd:cpe"), search.ByPackageName(thePkg.Name)) + require.NoError(t, err) + require.NotEmpty(t, vulns) + vulnObj := vulns[0] + + // why is this being set? vulnObj.CPEs = []cpe.CPE{ cpe.Must("cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*", ""), } - require.NoError(t, err) theResult.Add(match.Match{ - Vulnerability: *vulnObj, + Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: search.CPEParameters{ + SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:a:oracle:jdk:1.8.0:update400:*:*:*:*:*:*", }, - Package: search.CPEPackageParameter{Name: "jdk", Version: "1.8.0_400-b07"}, + Package: match.CPEPackageParameter{Name: "jdk", Version: "1.8.0_400-b07"}, }, - Found: search.CPEResult{ + Found: match.CPEResult{ VulnerabilityID: "CVE-jdk", VersionConstraint: "< 1.8.0_401 (jvm)", CPEs: []string{ @@ -593,7 +605,7 @@ func addJvmMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Colle } } -func addRustMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { +func addRustMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/hello-auditable") if len(packages) < 1 { t.Logf("Rust Packages: %+v", packages) @@ -602,12 +614,13 @@ func addRustMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll for _, p := range packages { thePkg := pkg.New(p) - theVuln := theStore.backend["github:language:rust"][strings.ToLower(thePkg.Name)][0] - vulnObj, err := v5.NewVulnerability(theVuln) + vulns, err := theStore.FindVulnerabilities(byNamespace("github:language:rust"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) + require.NotEmpty(t, vulns) + vulnObj := vulns[0] theResult.Add(match.Match{ - Vulnerability: *vulnObj, + Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { @@ -641,11 +654,11 @@ func TestMatchByImage(t *testing.T) { tests := []struct { name string - expectedFn func(source.Source, *syftPkg.Collection, *mockStore) match.Matches + expectedFn func(source.Source, *syftPkg.Collection, vulnerability.Provider) match.Matches }{ { name: "image-debian-match-coverage", - expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches { + expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider) match.Matches { expectedMatches := match.NewMatches() addPythonMatches(t, theSource, catalog, theStore, &expectedMatches) addRubyMatches(t, theSource, catalog, theStore, &expectedMatches) @@ -660,7 +673,7 @@ func TestMatchByImage(t *testing.T) { }, { name: "image-centos-match-coverage", - expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches { + expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider) match.Matches { expectedMatches := match.NewMatches() addRhelMatches(t, theSource, catalog, theStore, &expectedMatches) return expectedMatches @@ -668,7 +681,7 @@ func TestMatchByImage(t *testing.T) { }, { name: "image-alpine-match-coverage", - expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches { + expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider) match.Matches { expectedMatches := match.NewMatches() addAlpineMatches(t, theSource, catalog, theStore, &expectedMatches) return expectedMatches @@ -676,7 +689,7 @@ func TestMatchByImage(t *testing.T) { }, { name: "image-sles-match-coverage", - expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches { + expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider) match.Matches { expectedMatches := match.NewMatches() addSlesMatches(t, theSource, catalog, theStore, &expectedMatches) return expectedMatches @@ -685,7 +698,7 @@ func TestMatchByImage(t *testing.T) { // TODO: add this back in when #744 is fully implemented (see https://github.com/anchore/grype/issues/744#issuecomment-2448163737) //{ // name: "image-portage-match-coverage", - // expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches { + // expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider) match.Matches { // expectedMatches := match.NewMatches() // addPortageMatches(t, theSource, catalog, theStore, &expectedMatches) // return expectedMatches @@ -693,7 +706,7 @@ func TestMatchByImage(t *testing.T) { //}, { name: "image-rust-auditable-match-coverage", - expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches { + expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider) match.Matches { expectedMatches := match.NewMatches() addRustMatches(t, theSource, catalog, theStore, &expectedMatches) return expectedMatches @@ -701,7 +714,7 @@ func TestMatchByImage(t *testing.T) { }, { name: "image-jvm-match-coverage", - expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches { + expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore vulnerability.Provider) match.Matches { expectedMatches := match.NewMatches() addJvmMatches(t, theSource, catalog, theStore, &expectedMatches) return expectedMatches @@ -711,7 +724,7 @@ func TestMatchByImage(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - theStore := newMockDbStore() + theProvider := newMockDbProvider() imagetest.GetFixtureImage(t, "docker-archive", test.name) tarPath := imagetest.GetFixtureImageTarPath(t, test.name) @@ -760,17 +773,7 @@ func TestMatchByImage(t *testing.T) { }, }) - vp, err := v5.NewVulnerabilityProvider(theStore) - require.NoError(t, err) - mp := v5.NewVulnerabilityMetadataProvider(theStore) - ep := v5.NewMatchExclusionProvider(theStore) - str := v5.ProviderStore{ - VulnerabilityProvider: vp, - VulnerabilityMetadataProvider: mp, - ExclusionProvider: ep, - } - - actualResults := grype.FindVulnerabilitiesForPackage(str, s.Artifacts.LinuxDistribution, matchers, pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{})) + actualResults := grype.FindVulnerabilitiesForPackage(theProvider, s.Artifacts.LinuxDistribution, matchers, pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{})) for _, m := range actualResults.Sorted() { for _, d := range m.Details { observedMatchers.Add(string(d.Matcher)) @@ -778,7 +781,7 @@ func TestMatchByImage(t *testing.T) { } // build expected matches from what's discovered from the catalog - expectedMatches := test.expectedFn(theSource, s.Artifacts.Packages, theStore) + expectedMatches := test.expectedFn(theSource, s.Artifacts.Packages, theProvider) assertMatches(t, expectedMatches.Sorted(), actualResults.Sorted()) }) @@ -946,8 +949,9 @@ func vexMatches(t *testing.T, ignoredMatches []match.IgnoredMatch, vexStatus vex func assertMatches(t *testing.T, expected, actual []match.Match) { t.Helper() opts := []cmp.Option{ + cmpopts.EquateEmpty(), cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"), - cmpopts.IgnoreFields(pkg.Package{}, "Locations"), + cmpopts.IgnoreFields(pkg.Package{}, "Locations", "Distro"), cmpopts.SortSlices(func(a, b match.Match) bool { return a.Package.ID < b.Package.ID }), @@ -957,3 +961,9 @@ func assertMatches(t *testing.T, expected, actual []match.Match) { t.Errorf("mismatch (-want +got):\n%s", diff) } } + +func byNamespace(ns string) vulnerability.Criteria { + return search.ByFunc(func(v vulnerability.Vulnerability) (bool, error) { + return v.Reference.Namespace == ns, nil + }) +} diff --git a/test/integration/match_by_sbom_document_test.go b/test/integration/match_by_sbom_document_test.go index cdffe97663fc..c4e766d17f13 100644 --- a/test/integration/match_by_sbom_document_test.go +++ b/test/integration/match_by_sbom_document_test.go @@ -11,7 +11,6 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/grype/grype" - v5 "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/syft/syft/source" @@ -76,17 +75,8 @@ func TestMatchBySBOMDocument(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - mkStr := newMockDbStore() - vp, err := v5.NewVulnerabilityProvider(mkStr) - require.NoError(t, err) - mp := v5.NewVulnerabilityMetadataProvider(mkStr) - ep := v5.NewMatchExclusionProvider(mkStr) - str := v5.ProviderStore{ - VulnerabilityProvider: vp, - VulnerabilityMetadataProvider: mp, - ExclusionProvider: ep, - } - matches, _, _, err := grype.FindVulnerabilities(str, fmt.Sprintf("sbom:%s", test.fixture), source.SquashedScope, nil) + vp := newMockDbProvider() + matches, _, _, err := grype.FindVulnerabilities(vp, fmt.Sprintf("sbom:%s", test.fixture), source.SquashedScope, nil) assert.NoError(t, err) details := make([]match.Detail, 0) ids := strset.New()