Skip to content

Commit 0d31842

Browse files
committed
feat: Add a flag to have APK packages to take advantage of OSV based more granular advisories.
Signed-off-by: Ville Aikas <vaikas@chainguard.dev>
1 parent bf83e61 commit 0d31842

3 files changed

Lines changed: 267 additions & 59 deletions

File tree

cmd/grype/cli/options/match.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ type rpmConfig struct {
9191

9292
// apkConfig contains configuration for the APK matcher.
9393
type apkConfig struct {
94-
UseUpstreamMatcher bool `yaml:"use-upstream-matcher" json:"use-upstream-matcher" mapstructure:"use-upstream-matcher"` // if the upstream/origin package should be used during matching
94+
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
9595
}
9696

9797
func defaultGolangConfig() golangConfig {

grype/matcher/apk/matcher.go

Lines changed: 51 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66

7+
"github.com/anchore/grype/grype/distro"
78
"github.com/anchore/grype/grype/match"
89
"github.com/anchore/grype/grype/matcher/internal"
910
"github.com/anchore/grype/grype/pkg"
@@ -35,6 +36,22 @@ func NewApkMatcher(cfg MatcherConfig) *Matcher {
3536
return &Matcher{cfg: cfg}
3637
}
3738

39+
// useUpstreamForPackage returns whether origin/upstream lookups should be performed
40+
// for this package. Alpine always requires upstream lookups regardless of the config flag —
41+
// it uses secdb-style advisories keyed by origin package name, so disabling lookups would
42+
// silently miss vulnerabilities. OSV-based distros with per-sub-package entries (e.g.
43+
// Chainguard, Wolfi) can disable upstream lookups via UseUpstreamMatcher=false to avoid
44+
// false positives from origin-level entries applying to unaffected sub-packages.
45+
//
46+
// TODO: if Alpine ever publishes per-sub-package OSV advisories, this hardcoded override
47+
// should be removed and Alpine should respect the flag like other distros.
48+
func (m *Matcher) useUpstreamForPackage(p pkg.Package) bool {
49+
if p.Distro != nil && p.Distro.Type == distro.Alpine {
50+
return true
51+
}
52+
return m.cfg.UseUpstreamMatcher
53+
}
54+
3855
func (m *Matcher) PackageTypes() []syftPkg.Type {
3956
return []syftPkg.Type{syftPkg.ApkPkg}
4057
}
@@ -53,71 +70,68 @@ func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Ma
5370
}
5471
matches = append(matches, directMatches...)
5572

56-
// indirect matches, via package's origin package
57-
if m.cfg.UseUpstreamMatcher {
58-
// full upstream matching: consult advisory for origin package name
73+
// For secdb-style advisories that lack per-sub-package granularity, vulnerabilities are
74+
// keyed under the origin/source package name rather than the individual sub-package.
75+
// This lookup propagates those matches to the installed sub-package. When using an OSV-based
76+
// advisory that has per-sub-package entries (e.g. Chainguard/Wolfi), this can be disabled
77+
// (UseUpstreamMatcher=false) to avoid false positives from origin-level entries applying to
78+
// unaffected sub-packages. Alpine is always exempt — see useUpstreamForPackage.
79+
if m.useUpstreamForPackage(p) {
5980
indirectMatches, err := m.findMatchesForOriginPackage(store, p)
6081
if err != nil {
6182
return nil, nil, err
6283
}
6384
matches = append(matches, indirectMatches...)
64-
} else {
65-
// CPE-only upstream matching: use origin's CPEs to find NVD vulns, but
66-
// filter against the direct sub-package's advisory (not the origin's)
67-
indirectMatches, err := m.findCPEMatchesForOriginPackages(store, p)
68-
if err != nil {
69-
return nil, nil, err
70-
}
71-
matches = append(matches, indirectMatches...)
7285
}
7386

7487
// APK sources are also able to NAK vulnerabilities, so we want to return these as explicit ignores in order
7588
// to allow rules later to use these to ignore "the same" vulnerability found in "the same" locations
76-
naks, err := m.findNaksForPackage(store, p, m.cfg.UseUpstreamMatcher)
89+
naks, err := m.findNaksForPackage(store, p)
7790

7891
return matches, naks, err
7992
}
8093

8194
//nolint:funlen,gocognit
82-
// cpeMatchesWithoutSecDBFixes finds CPE-indexed vulnerability matches for cpePkg (using its
83-
// CPEs for NVD lookup) but filters out matches already fixed according to the advisory for
84-
// secdbPkg. Normally cpePkg == secdbPkg, but when UseUpstreamMatcher is false, cpePkg is the
85-
// origin package (rewritten CPEs for NVD lookup) while secdbPkg is the direct sub-package
86-
// (whose advisory holds the authoritative fix/NAK data).
87-
func (m *Matcher) cpeMatchesWithoutSecDBFixes(provider vulnerability.Provider, cpePkg pkg.Package, secdbPkg pkg.Package) ([]match.Match, error) {
88-
// find CPE-indexed vulnerability matches using cpePkg's CPEs
89-
cpeMatches, err := internal.MatchPackageByCPEs(provider, cpePkg, m.Type())
95+
func (m *Matcher) cpeMatchesWithoutSecDBFixes(provider vulnerability.Provider, p pkg.Package) ([]match.Match, error) {
96+
// find CPE-indexed vulnerability matches specific to the given package name and version
97+
cpeMatches, err := internal.MatchPackageByCPEs(provider, p, m.Type())
9098
if err != nil {
91-
log.WithFields("package", cpePkg.Name, "error", err).Debug("failed to find CPE matches for package")
99+
log.WithFields("package", p.Name, "error", err).Debug("failed to find CPE matches for package")
92100
}
93-
if secdbPkg.Distro == nil {
101+
if p.Distro == nil {
94102
return cpeMatches, nil
95103
}
96104

97105
cpeMatchesByID := matchesByID(cpeMatches)
98106

99-
// remove cpe matches where there is an entry in the advisory for secdbPkg indicating the
100-
// installed version is already fixed.
107+
// Suppress CPE matches that the distro advisory has already marked as fixed.
108+
// When UseUpstreamMatcher is false we only consult the direct package's advisory here,
109+
// not the origin's. This means a CPE match won't be suppressed based on an origin-level
110+
// fix — a deliberate tradeoff: a sub-package with its own CPEs may produce a false
111+
// positive if the origin advisory already has the fix and version numbers are shared.
112+
// In practice this is uncommon since APK sub-packages rarely have independent CPEs in NVD.
101113
secDBVulnerabilities, err := provider.FindVulnerabilities(
102-
search.ByPackageName(secdbPkg.Name),
103-
search.ByDistro(*secdbPkg.Distro))
114+
search.ByPackageName(p.Name),
115+
search.ByDistro(*p.Distro))
104116
if err != nil {
105117
return nil, err
106118
}
107119

108-
for _, upstreamPkg := range pkg.UpstreamPackages(secdbPkg) {
109-
secDBVulnerabilitiesForUpstream, err := provider.FindVulnerabilities(
110-
search.ByPackageName(upstreamPkg.Name),
111-
search.ByDistro(*upstreamPkg.Distro))
112-
if err != nil {
113-
return nil, err
120+
if m.useUpstreamForPackage(p) {
121+
for _, upstreamPkg := range pkg.UpstreamPackages(p) {
122+
secDBVulnerabilitiesForUpstream, err := provider.FindVulnerabilities(
123+
search.ByPackageName(upstreamPkg.Name),
124+
search.ByDistro(*upstreamPkg.Distro))
125+
if err != nil {
126+
return nil, err
127+
}
128+
secDBVulnerabilities = append(secDBVulnerabilities, secDBVulnerabilitiesForUpstream...)
114129
}
115-
secDBVulnerabilities = append(secDBVulnerabilities, secDBVulnerabilitiesForUpstream...)
116130
}
117131

118132
secDBVulnerabilitiesByID := vulnerabilitiesByID(secDBVulnerabilities)
119133

120-
verObj := version.New(secdbPkg.Version, pkg.VersionFormat(secdbPkg))
134+
verObj := version.New(p.Version, pkg.VersionFormat(p))
121135

122136
var finalCpeMatches []match.Match
123137

@@ -204,7 +218,7 @@ func (m *Matcher) findMatchesForPackage(store vulnerability.Provider, p pkg.Pack
204218
}
205219

206220
// TODO: are there other errors that we should handle here that causes this to short circuit
207-
cpeMatches, err := m.cpeMatchesWithoutSecDBFixes(store, p, p)
221+
cpeMatches, err := m.cpeMatchesWithoutSecDBFixes(store, p)
208222
if err != nil && !errors.Is(err, internal.ErrEmptyCPEMatch) {
209223
return nil, err
210224
}
@@ -238,35 +252,14 @@ func (m *Matcher) findMatchesForOriginPackage(store vulnerability.Provider, cata
238252
return matches, nil
239253
}
240254

241-
// findCPEMatchesForOriginPackages is used when UseUpstreamMatcher is false. It still performs
242-
// CPE/NVD lookups using origin-rewritten CPEs (so NVD vulns keyed under the origin name are
243-
// found), but advisory filtering uses the direct sub-package's advisory rather than the
244-
// origin's. This supports distro advisories that are keyed per sub-package rather than per
245-
// origin package.
246-
func (m *Matcher) findCPEMatchesForOriginPackages(store vulnerability.Provider, catalogPkg pkg.Package) ([]match.Match, error) {
247-
var matches []match.Match
248-
249-
for _, indirectPackage := range pkg.UpstreamPackages(catalogPkg) {
250-
// cpePkg = origin (rewritten CPEs for NVD), secdbPkg = direct sub-package (advisory filtering)
251-
cpeMatches, err := m.cpeMatchesWithoutSecDBFixes(store, indirectPackage, catalogPkg)
252-
if err != nil && !errors.Is(err, internal.ErrEmptyCPEMatch) {
253-
return nil, fmt.Errorf("failed to find CPE vulnerabilities for apk upstream source package: %w", err)
254-
}
255-
matches = append(matches, cpeMatches...)
256-
}
257-
258-
match.ConvertToIndirectMatches(matches, catalogPkg)
259-
return matches, nil
260-
}
261-
262255
// NAK entries are those reported as explicitly not vulnerable by the upstream provider,
263256
// for example this entry is present in the v5 database:
264257
// 312891,CVE-2020-7224,openvpn,alpine:distro:alpine:3.10,,< 0,apk,,"[{""id"":""CVE-2020-7224"",""namespace"":""nvd:cpe""}]","[""0""]",fixed,
265258
// which indicates, for the alpine:3.10 distro, package openvpn is not vulnerable to CVE-2020-7224
266259
// we want to report these NAK entries as match.IgnoredMatch, to allow for later processing to create ignore rules
267260
// based on packages which overlap by location, such as a python binary found in addition to the python APK entry --
268261
// we want to NAK this vulnerability for BOTH packages
269-
func (m *Matcher) findNaksForPackage(provider vulnerability.Provider, p pkg.Package, useUpstreamMatcher bool) ([]match.IgnoreFilter, error) {
262+
func (m *Matcher) findNaksForPackage(provider vulnerability.Provider, p pkg.Package) ([]match.IgnoreFilter, error) {
270263
if p.Distro == nil {
271264
return nil, nil
272265
}
@@ -282,7 +275,7 @@ func (m *Matcher) findNaksForPackage(provider vulnerability.Provider, p pkg.Pack
282275
}
283276

284277
// append all the upstream naks
285-
if useUpstreamMatcher {
278+
if m.useUpstreamForPackage(p) {
286279
for _, upstreamPkg := range pkg.UpstreamPackages(p) {
287280
upstreamNaks, err := provider.FindVulnerabilities(
288281
search.ByDistro(*upstreamPkg.Distro),

0 commit comments

Comments
 (0)