diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go index f24829ba302..ae9fa881f72 100644 --- a/cmd/grype/cli/commands/root.go +++ b/cmd/grype/cli/commands/root.go @@ -20,6 +20,7 @@ import ( "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/apk" "github.com/anchore/grype/grype/matcher/dpkg" "github.com/anchore/grype/grype/matcher/golang" "github.com/anchore/grype/grype/matcher/hex" @@ -388,6 +389,9 @@ func getMatcherConfig(opts *options.Grype) matcher.Config { MissingEpochStrategy: opts.Match.Rpm.MissingEpochStrategy, UseCPEsForEOL: opts.Match.Rpm.UseCPEsForEOL, }, + Apk: apk.MatcherConfig{ + UseUpstreamMatcher: opts.Match.Apk.UseUpstreamMatcher, + }, } } diff --git a/cmd/grype/cli/options/match.go b/cmd/grype/cli/options/match.go index b21becb72b7..88d033562e3 100644 --- a/cmd/grype/cli/options/match.go +++ b/cmd/grype/cli/options/match.go @@ -21,6 +21,7 @@ type matchConfig struct { Stock matcherConfig `yaml:"stock" json:"stock" mapstructure:"stock"` // settings for the default/stock matcher Dpkg dpkgConfig `yaml:"dpkg" json:"dpkg" mapstructure:"dpkg"` // settings for the dpkg matcher Rpm rpmConfig `yaml:"rpm" json:"rpm" mapstructure:"rpm"` // settings for the rpm matcher + Apk apkConfig `yaml:"apk" json:"apk" mapstructure:"apk"` // settings for the apk matcher } var _ interface { @@ -88,6 +89,11 @@ type rpmConfig struct { UseCPEsForEOL bool `yaml:"use-cpes-for-eol" json:"use-cpes-for-eol" mapstructure:"use-cpes-for-eol"` // if CPEs should be used for EOL distro packages } +// apkConfig contains configuration for the APK matcher. +type apkConfig struct { + UseUpstreamMatcher bool `yaml:"use-upstream-matcher" json:"use-upstream-matcher" mapstructure:"use-upstream-matcher"` // if the upstream/origin package name should be used during matching +} + func defaultGolangConfig() golangConfig { return golangConfig{ matcherConfig: matcherConfig{ @@ -130,6 +136,7 @@ func defaultMatchConfig() matchConfig { Stock: useCpe, Dpkg: defaultDpkgConfig(), Rpm: defaultRpmConfig(), + Apk: apkConfig{UseUpstreamMatcher: true}, } } @@ -182,4 +189,5 @@ func (cfg *matchConfig) DescribeFields(descriptions clio.FieldDescriptionSet) { eolCpeDescription := `use CPE matching for packages from end-of-life distributions` descriptions.Add(&cfg.Dpkg.UseCPEsForEOL, eolCpeDescription) descriptions.Add(&cfg.Rpm.UseCPEsForEOL, eolCpeDescription) + descriptions.Add(&cfg.Apk.UseUpstreamMatcher, `use the upstream/origin package name when matching APK vulnerabilities`) } diff --git a/grype/matcher/apk/matcher.go b/grype/matcher/apk/matcher.go index ac6ec12df96..efc44ae178d 100644 --- a/grype/matcher/apk/matcher.go +++ b/grype/matcher/apk/matcher.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" + "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" @@ -23,7 +24,33 @@ var ( }) ) -type Matcher struct{} +type MatcherConfig struct { + UseUpstreamMatcher bool +} + +type Matcher struct { + cfg MatcherConfig +} + +func NewApkMatcher(cfg MatcherConfig) *Matcher { + return &Matcher{cfg: cfg} +} + +// useUpstreamForPackage returns whether origin/upstream lookups should be performed +// for this package. Alpine always requires upstream lookups regardless of the config flag — +// it uses secdb-style advisories keyed by origin package name, so disabling lookups would +// silently miss vulnerabilities. OSV-based distros with per-sub-package entries (e.g. +// Chainguard, Wolfi) can disable upstream lookups via UseUpstreamMatcher=false to avoid +// false positives from origin-level entries applying to unaffected sub-packages. +// +// TODO: if Alpine ever publishes per-sub-package OSV advisories, this hardcoded override +// should be removed and Alpine should respect the flag like other distros. +func (m *Matcher) useUpstreamForPackage(p pkg.Package) bool { + if p.Distro != nil && p.Distro.Type == distro.Alpine { + return true + } + return m.cfg.UseUpstreamMatcher +} func (m *Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{syftPkg.ApkPkg} @@ -43,12 +70,19 @@ func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Ma } matches = append(matches, directMatches...) - // indirect matches, via package's origin package - indirectMatches, err := m.findMatchesForOriginPackage(store, p) - if err != nil { - return nil, nil, err + // For secdb-style advisories that lack per-sub-package granularity, vulnerabilities are + // keyed under the origin/source package name rather than the individual sub-package. + // This lookup propagates those matches to the installed sub-package. When using an OSV-based + // advisory that has per-sub-package entries (e.g. Chainguard/Wolfi), this can be disabled + // (UseUpstreamMatcher=false) to avoid false positives from origin-level entries applying to + // unaffected sub-packages. Alpine is always exempt — see useUpstreamForPackage. + if m.useUpstreamForPackage(p) { + indirectMatches, err := m.findMatchesForOriginPackage(store, p) + if err != nil { + return nil, nil, err + } + matches = append(matches, indirectMatches...) } - matches = append(matches, indirectMatches...) // 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 @@ -70,8 +104,12 @@ func (m *Matcher) cpeMatchesWithoutSecDBFixes(provider vulnerability.Provider, p 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. + // Suppress CPE matches that the distro advisory has already marked as fixed. + // When UseUpstreamMatcher is false we only consult the direct package's advisory here, + // not the origin's. This means a CPE match won't be suppressed based on an origin-level + // fix — a deliberate tradeoff: a sub-package with its own CPEs may produce a false + // positive if the origin advisory already has the fix and version numbers are shared. + // In practice this is uncommon since APK sub-packages rarely have independent CPEs in NVD. secDBVulnerabilities, err := provider.FindVulnerabilities( search.ByPackageName(p.Name), search.ByDistro(*p.Distro)) @@ -79,14 +117,16 @@ func (m *Matcher) cpeMatchesWithoutSecDBFixes(provider vulnerability.Provider, p return nil, err } - for _, upstreamPkg := range pkg.UpstreamPackages(p) { - secDBVulnerabilitiesForUpstream, err := provider.FindVulnerabilities( - search.ByPackageName(upstreamPkg.Name), - search.ByDistro(*upstreamPkg.Distro)) - if err != nil { - return nil, err + if m.useUpstreamForPackage(p) { + for _, upstreamPkg := range pkg.UpstreamPackages(p) { + secDBVulnerabilitiesForUpstream, err := provider.FindVulnerabilities( + search.ByPackageName(upstreamPkg.Name), + search.ByDistro(*upstreamPkg.Distro)) + if err != nil { + return nil, err + } + secDBVulnerabilities = append(secDBVulnerabilities, secDBVulnerabilitiesForUpstream...) } - secDBVulnerabilities = append(secDBVulnerabilities, secDBVulnerabilitiesForUpstream...) } secDBVulnerabilitiesByID := vulnerabilitiesByID(secDBVulnerabilities) @@ -235,17 +275,19 @@ func (m *Matcher) findNaksForPackage(provider vulnerability.Provider, p pkg.Pack } // append all the upstream naks - for _, upstreamPkg := range pkg.UpstreamPackages(p) { - upstreamNaks, err := provider.FindVulnerabilities( - search.ByDistro(*upstreamPkg.Distro), - search.ByPackageName(upstreamPkg.Name), - nakConstraint, - ) - if err != nil { - return nil, err - } + if m.useUpstreamForPackage(p) { + for _, upstreamPkg := range pkg.UpstreamPackages(p) { + upstreamNaks, err := provider.FindVulnerabilities( + search.ByDistro(*upstreamPkg.Distro), + search.ByPackageName(upstreamPkg.Name), + nakConstraint, + ) + if err != nil { + return nil, err + } - naks = append(naks, upstreamNaks...) + naks = append(naks, upstreamNaks...) + } } meta, ok := p.Metadata.(pkg.ApkMetadata) diff --git a/grype/matcher/apk/matcher_test.go b/grype/matcher/apk/matcher_test.go index 6240e12d1ee..9d6318f631b 100644 --- a/grype/matcher/apk/matcher_test.go +++ b/grype/matcher/apk/matcher_test.go @@ -32,7 +32,7 @@ func TestSecDBOnlyMatch(t *testing.T) { vp := mock.VulnerabilityProvider(secDbVuln) - m := Matcher{} + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: true}) d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ @@ -108,7 +108,7 @@ func TestBothSecdbAndNvdMatches(t *testing.T) { vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) - m := Matcher{} + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: true}) d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ @@ -191,7 +191,7 @@ func TestBothSecdbAndNvdMatches_DifferentFixInfo(t *testing.T) { }, } vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) - m := Matcher{} + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: true}) d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ @@ -268,7 +268,7 @@ func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) { vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) - m := Matcher{} + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: true}) d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ @@ -333,7 +333,7 @@ func TestNvdOnlyMatches(t *testing.T) { } vp := mock.VulnerabilityProvider(nvdVuln) - m := Matcher{} + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: true}) d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ @@ -400,7 +400,7 @@ func TestNvdOnlyMatches_FixInNvd(t *testing.T) { } vp := mock.VulnerabilityProvider(nvdVuln) - m := Matcher{} + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: true}) d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ @@ -477,7 +477,7 @@ func TestNvdMatchesProperVersionFiltering(t *testing.T) { } vp := mock.VulnerabilityProvider(nvdVulnMatch, nvdVulnNoMatch) - m := Matcher{} + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: true}) d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ @@ -548,7 +548,7 @@ func TestNvdMatchesWithSecDBFix(t *testing.T) { vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) - m := Matcher{} + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: true}) d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ @@ -594,7 +594,7 @@ func TestNvdMatchesNoConstraintWithSecDBFix(t *testing.T) { vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) - m := Matcher{} + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: true}) d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ @@ -638,7 +638,7 @@ func TestNVDMatchCanceledByOriginPackageInSecDB(t *testing.T) { } vp := mock.VulnerabilityProvider(nvdVuln, secDBVuln) - m := Matcher{} + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: true}) d := distro.New(distro.Wolfi, "", "") p := pkg.Package{ @@ -679,7 +679,7 @@ func TestDistroMatchBySourceIndirection(t *testing.T) { } vp := mock.VulnerabilityProvider(secDbVuln) - m := Matcher{} + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: true}) d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ @@ -749,7 +749,7 @@ func TestSecDBMatchesStillCountedWithCpeErrors(t *testing.T) { vp := mock.VulnerabilityProvider(secDbVuln) - m := Matcher{} + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: true}) d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ @@ -816,7 +816,7 @@ func TestNVDMatchBySourceIndirection(t *testing.T) { } vp := mock.VulnerabilityProvider(nvdVuln) - m := Matcher{} + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: true}) d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ @@ -869,6 +869,311 @@ func TestNVDMatchBySourceIndirection(t *testing.T) { assertMatches(t, expected, actual) } +// Tests for UseUpstreamMatcher=false: all origin/upstream lookups must be skipped. +// The intent is to support distro advisories keyed per sub-package rather than per origin. + +func TestUpstreamMatcherDisabled_AlpineAlwaysUsesUpstream(t *testing.T) { + // Alpine uses secdb-style advisories keyed by origin package name. + // Even with UseUpstreamMatcher=false, Alpine must still perform origin lookups + // or vulnerabilities would be silently missed. + secDbVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-2", + Namespace: "secdb:distro:alpine:3.12", + }, + PackageName: "thingsync", + Constraint: version.MustGetConstraint("< 2.0.14-r1", version.ApkFormat), + } + vp := mock.VulnerabilityProvider(secDbVuln) + + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: false}) + d := distro.New(distro.Alpine, "3.12.0", "") + + p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "thingsync-compat", + Version: "2.0.14-r0", + Type: syftPkg.ApkPkg, + Distro: d, + Upstreams: []pkg.UpstreamPackage{ + {Name: "thingsync"}, + }, + } + + // Alpine origin lookup must fire despite flag=false + actual, _, err := m.Match(vp, p) + assert.NoError(t, err) + assert.Len(t, actual, 1) + assert.Equal(t, match.ExactIndirectMatch, actual[0].Details[0].Type) +} + +func TestAlpine_OriginNAKStillPropagated_WhenFlagFalse(t *testing.T) { + // NAK entries for Alpine sub-packages come from the origin package advisory. + // Even with UseUpstreamMatcher=false, Alpine must still propagate origin NAKs + // or the NAK-based ignore rules would silently stop working for Alpine. + originNakVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-2", + Namespace: "secdb:distro:alpine:3.12", + }, + PackageName: "thingsync", + Constraint: version.MustGetConstraint("< 0", version.ApkFormat), + } + vp := mock.VulnerabilityProvider(originNakVuln) + + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: false}) + d := distro.New(distro.Alpine, "3.12.0", "") + + p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "thingsync-compat", + Version: "2.0.14-r0", + Type: syftPkg.ApkPkg, + Distro: d, + Upstreams: []pkg.UpstreamPackage{ + {Name: "thingsync"}, + }, + Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ + {Path: "/usr/bin/entrypoint.sh"}, + }}, + } + + _, ignores, err := m.Match(vp, p) + assert.NoError(t, err) + assert.Len(t, ignores, 1, "Alpine origin NAK must still produce an ignore rule when UseUpstreamMatcher=false") +} + +func TestAlpine_CPEFilteredByOriginSecdb_WhenFlagFalse(t *testing.T) { + // When NVD has a CPE match and the Alpine secdb says the origin package is already + // fixed, the CPE match must be suppressed. This must hold even with UseUpstreamMatcher=false + // since Alpine always consults origin advisory data for CPE filtering. + nvdVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-1", + Namespace: "nvd:cpe", + }, + PackageName: "thingsync", + Constraint: version.MustGetConstraint("<= 2.0.14-r1", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:a:thingsync:thingsync:*:*:*:*:*:*:*:*", ""), + }, + } + // Origin secdb says: CVE-2020-1 is fixed in thingsync 2.0.14-r1 + secDbVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2020-1", + Namespace: "secdb:distro:alpine:3.12", + }, + PackageName: "thingsync", + Constraint: version.MustGetConstraint("< 2.0.14-r1", version.ApkFormat), + Fix: vulnerability.Fix{ + Versions: []string{"2.0.14-r1"}, + State: vulnerability.FixStateFixed, + }, + } + vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) + + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: false}) + d := distro.New(distro.Alpine, "3.12.0", "") + + // thingsync-compat is at 2.0.14-r1 — already fixed per the origin secdb + p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "thingsync-compat", + Version: "2.0.14-r1", + Type: syftPkg.ApkPkg, + Distro: d, + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:a:thingsync-compat:thingsync-compat:*:*:*:*:*:*:*:*", ""), + }, + Upstreams: []pkg.UpstreamPackage{ + {Name: "thingsync"}, + }, + } + + // The NVD CPE match must be suppressed because the origin secdb says it's already fixed + actual, _, err := m.Match(vp, p) + assert.NoError(t, err) + assert.Empty(t, actual, "Alpine CPE match must be suppressed by origin secdb fix data even when UseUpstreamMatcher=false") +} + +func TestUpstreamMatcherDisabled_OriginAdvisoryNotUsed(t *testing.T) { + // Advisory has an entry for the origin ("thingsync") but NOT for the sub-package ("thingsync-compat"). + // With UseUpstreamMatcher=false the origin lookup is skipped and nothing should match. + secDbVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CGA-xcpc-gm23-prj9", + Namespace: "chainguard:distro:chainguard:rolling", + }, + PackageName: "thingsync", + Constraint: version.MustGetConstraint("< 2.0.14-r1", version.ApkFormat), + } + vp := mock.VulnerabilityProvider(secDbVuln) + + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: false}) + d := distro.New(distro.Chainguard, "", "") + + p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "thingsync-compat", + Version: "2.0.14-r0", + Type: syftPkg.ApkPkg, + Distro: d, + Upstreams: []pkg.UpstreamPackage{ + {Name: "thingsync"}, + }, + } + + actual, _, err := m.Match(vp, p) + assert.NoError(t, err) + assert.Empty(t, actual) +} + +func TestUpstreamMatcherDisabled_DirectAdvisoryUsed(t *testing.T) { + // Advisory has a direct entry for the sub-package ("thingsync-compat"). + // With UseUpstreamMatcher=false this direct entry must still be found. + directVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CGA-xcpc-gm23-prj9", + Namespace: "chainguard:distro:chainguard:rolling", + }, + PackageName: "thingsync-compat", + Constraint: version.MustGetConstraint("< 2.0.14-r1", version.ApkFormat), + } + vp := mock.VulnerabilityProvider(directVuln) + + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: false}) + d := distro.New(distro.Chainguard, "", "") + + p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "thingsync-compat", + Version: "2.0.14-r0", + Type: syftPkg.ApkPkg, + Distro: d, + Upstreams: []pkg.UpstreamPackage{ + {Name: "thingsync"}, + }, + } + + actual, _, err := m.Match(vp, p) + assert.NoError(t, err) + assert.Len(t, actual, 1) + assert.Equal(t, "thingsync-compat", actual[0].Vulnerability.PackageName) + assert.Equal(t, match.ExactDirectMatch, actual[0].Details[0].Type) +} + +func TestUpstreamMatcherDisabled_NVDOriginCPENotUsed(t *testing.T) { + // NVD has a CPE entry keyed under the origin ("thingsync") CPE. + // With UseUpstreamMatcher=false origin CPE lookups are skipped and nothing should match. + nvdVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-2025-68121", + Namespace: "nvd:cpe", + }, + PackageName: "thingsync", + Constraint: version.MustGetConstraint("< 2.0.14-r1", version.UnknownFormat), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:a:thingsync:thingsync:*:*:*:*:*:*:*:*", ""), + }, + } + vp := mock.VulnerabilityProvider(nvdVuln) + + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: false}) + d := distro.New(distro.Chainguard, "", "") + + p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "thingsync-compat", + Version: "2.0.14-r0", + Type: syftPkg.ApkPkg, + Distro: d, + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:a:thingsync-compat:thingsync-compat:*:*:*:*:*:*:*:*", ""), + }, + Upstreams: []pkg.UpstreamPackage{ + {Name: "thingsync"}, + }, + } + + actual, _, err := m.Match(vp, p) + assert.NoError(t, err) + assert.Empty(t, actual) +} + +func TestUpstreamMatcherDisabled_DirectNAKRespected(t *testing.T) { + // The sub-package ("thingsync-compat") has a direct NAK entry (< 0) in the advisory. + // With UseUpstreamMatcher=false this direct NAK must still produce an ignore rule. + nakVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CGA-xcpc-gm23-prj9", + Namespace: "chainguard:distro:chainguard:rolling", + }, + PackageName: "thingsync-compat", + Constraint: version.MustGetConstraint("< 0", version.ApkFormat), + } + vp := mock.VulnerabilityProvider(nakVuln) + + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: false}) + + p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "thingsync-compat", + Version: "2.0.14-r0", + Type: syftPkg.ApkPkg, + Distro: &distro.Distro{Type: distro.Chainguard}, + Upstreams: []pkg.UpstreamPackage{ + {Name: "thingsync"}, + }, + Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ + {Path: "/usr/bin/entrypoint.sh"}, + }}, + } + + _, ignores, err := m.Match(vp, p) + assert.NoError(t, err) + assert.Len(t, ignores, 1) + + rule, ok := ignores[0].(match.IgnoreRule) + require.True(t, ok) + assert.Equal(t, "CGA-xcpc-gm23-prj9", rule.Vulnerability) + assert.Equal(t, "/usr/bin/entrypoint.sh", rule.Package.Location) +} + +func TestUpstreamMatcherDisabled_OriginNAKNotPropagated(t *testing.T) { + // The origin ("thingsync") has a NAK entry but the sub-package ("thingsync-compat") does not. + // With UseUpstreamMatcher=false the origin NAK must NOT propagate to the sub-package. + originNakVuln := vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CGA-xcpc-gm23-prj9", + Namespace: "chainguard:distro:chainguard:rolling", + }, + PackageName: "thingsync", + Constraint: version.MustGetConstraint("< 0", version.ApkFormat), + } + vp := mock.VulnerabilityProvider(originNakVuln) + + m := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: false}) + + p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "thingsync-compat", + Version: "2.0.14-r0", + Type: syftPkg.ApkPkg, + Distro: &distro.Distro{Type: distro.Chainguard}, + Upstreams: []pkg.UpstreamPackage{ + {Name: "thingsync"}, + }, + Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ + {Path: "/usr/bin/entrypoint.sh"}, + }}, + } + + _, ignores, err := m.Match(vp, p) + assert.NoError(t, err) + assert.Empty(t, ignores) +} + func assertMatches(t *testing.T, expected, actual []match.Match) { t.Helper() var opts = []cmp.Option{ @@ -1065,7 +1370,7 @@ func Test_nakIgnoreRules(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // create mock vulnerability provider vp := mock.VulnerabilityProvider(tt.vulns...) - apkMatcher := &Matcher{} + apkMatcher := NewApkMatcher(MatcherConfig{UseUpstreamMatcher: true}) var allMatches []match.Match var allIgnores []match.IgnoreFilter diff --git a/grype/matcher/matchers.go b/grype/matcher/matchers.go index 7ca44d65804..e4e386aafae 100644 --- a/grype/matcher/matchers.go +++ b/grype/matcher/matchers.go @@ -33,6 +33,7 @@ type Config struct { Stock stock.MatcherConfig Dpkg dpkg.MatcherConfig Rpm rpm.MatcherConfig + Apk apk.MatcherConfig } func NewDefaultMatchers(mc Config) []match.Matcher { @@ -44,7 +45,7 @@ func NewDefaultMatchers(mc Config) []match.Matcher { rpm.NewRpmMatcher(mc.Rpm), java.NewJavaMatcher(mc.Java), javascript.NewJavascriptMatcher(mc.Javascript), - &apk.Matcher{}, + apk.NewApkMatcher(mc.Apk), golang.NewGolangMatcher(mc.Golang), &msrc.Matcher{}, &portage.Matcher{},