Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: update alpine matchers to use SecDB entries as fixed information rather than vuln source #1318

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
101 changes: 40 additions & 61 deletions grype/matcher/apk/matcher.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
package apk

import (
"fmt"

"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"
syftPkg "github.com/anchore/syft/syft/pkg"
)

// Matcher behaves a little differently here than its other implementations.
// Secdb provides a negative match to the NVD matches meaning it can only be
// used to turn off a vulnerability. The contraint is a lie. Only the "fixed_in_versions"
// Column shows the true match to turn off...
//
// Example....
/*
-----------------------------
Package Match in NVD:
zlib: v1.2.3-r2 | CVE X — affected versions: < v1.4.2

Secdb data shows
zlib: v1.2.3-r2 fixes CVE X

Expected result:
Match is not reported because of the Secdb fix
*/
type Matcher struct {
}

Expand All @@ -27,11 +41,11 @@ func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Pa
var matches = make([]match.Match, 0)

// direct matches with package
directMatches, err := m.findApkPackage(store, d, p)
cpeMatches, err := m.cpeMatchesWithoutSecDBFixes(store, d, p)
if err != nil {
return nil, err
}
matches = append(matches, directMatches...)
matches = append(matches, cpeMatches...)

// indirect matches with package source
indirectMatches, err := m.matchBySourceIndirection(store, d, p)
Expand All @@ -43,6 +57,7 @@ func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Pa
return matches, nil
}

// compares NVD matches against secdb fixes for a given distro
func (m *Matcher) cpeMatchesWithoutSecDBFixes(store vulnerability.Provider, d *distro.Distro, 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())
Expand All @@ -52,43 +67,44 @@ func (m *Matcher) cpeMatchesWithoutSecDBFixes(store vulnerability.Provider, d *d

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)
// get all secDB fixes for the provided distro
secDBVulnFixes, err := store.GetByDistro(d, p)
if err != nil {
return nil, err
}

secDBVulnerabilitiesByID := vulnerabilitiesByID(secDBVulnerabilities)

verObj, err := version.NewVersionFromPkg(p)
if err != nil {
return nil, fmt.Errorf("matcher failed to parse version pkg='%s' ver='%s': %w", p.Name, p.Version, err)
}
secDBFixesByID := fixesByID(secDBVulnFixes)

// remove cpe matches where there is an entry in the secDB for the particular package-vulnerability pairing
// and the installed package version should match the fixed in version for the secDB record.
var finalCpeMatches []match.Match

cveLoop:
for id, cpeMatchesForID := range cpeMatchesByID {
// check to see if there is a secdb entry for this ID (CVE)
secDBVulnerabilitiesForID, exists := secDBVulnerabilitiesByID[id]
secDBFixForID, exists := secDBFixesByID[id]
if !exists {
// does not exist in secdb, so the CPE record(s) should be added to the final results
finalCpeMatches = append(finalCpeMatches, cpeMatchesForID...)
continue
}

// there is a secdb entry...
for _, vuln := range secDBVulnerabilitiesForID {
for _, vuln := range secDBFixForID {
// ...is there a fixed in entry? (should always be yes)
if len(vuln.Fix.Versions) == 0 {
continue
}

// ...is the current package vulnerable?
vulnerable, err := vuln.Constraint.Satisfied(verObj)
if err != nil {
return nil, err
vulnerable := true
for _, fixedVersion := range vuln.Fix.Versions {
// we found that the packages version is the same
// as the fixed version for the given CVE in secdb
if fixedVersion == p.Version {
vulnerable = false
break
}
}

if vulnerable {
Expand All @@ -101,21 +117,6 @@ cveLoop:
return finalCpeMatches, nil
}

func deduplicateMatches(secDBMatches, cpeMatches []match.Match) (matches []match.Match) {
// add additional unique matches from CPE source that is unique from the SecDB matches
secDBMatchesByID := matchesByID(secDBMatches)
cpeMatchesByID := matchesByID(cpeMatches)
for id, cpeMatchesForID := range cpeMatchesByID {
// by this point all matches have been verified to be vulnerable within the given package version relative to the vulnerability source.
// now we will add unique CPE candidates that were not found in secdb.
if _, exists := secDBMatchesByID[id]; !exists {
// add the new CPE-based record (e.g. NVD) since it was not found in secDB
matches = append(matches, cpeMatchesForID...)
}
}
return matches
}

func matchesByID(matches []match.Match) map[string][]match.Match {
var results = make(map[string][]match.Match)
for _, secDBMatch := range matches {
Expand All @@ -124,45 +125,23 @@ func matchesByID(matches []match.Match) map[string][]match.Match {
return results
}

func vulnerabilitiesByID(vulns []vulnerability.Vulnerability) map[string][]vulnerability.Vulnerability {
func fixesByID(vulnFixes []vulnerability.Vulnerability) map[string][]vulnerability.Vulnerability {
var results = make(map[string][]vulnerability.Vulnerability)
for _, vuln := range vulns {
for _, vuln := range vulnFixes {
results[vuln.ID] = append(results[vuln.ID], vuln)
}

return results
}

func (m *Matcher) findApkPackage(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
// find Alpine SecDB matches for the given package name and version
secDBMatches, err := search.ByPackageDistro(store, d, p, m.Type())
if err != nil {
return nil, err
}

cpeMatches, err := m.cpeMatchesWithoutSecDBFixes(store, d, p)
if err != nil {
return nil, err
}

var matches []match.Match

// keep all secdb matches, as this is an authoritative source
matches = append(matches, secDBMatches...)

// keep only unique CPE matches
matches = append(matches, deduplicateMatches(secDBMatches, cpeMatches)...)

return matches, nil
}

func (m *Matcher) matchBySourceIndirection(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
var matches []match.Match

for _, indirectPackage := range pkg.UpstreamPackages(p) {
indirectMatches, err := m.findApkPackage(store, d, indirectPackage)
// direct matches with package
indirectMatches, err := m.cpeMatchesWithoutSecDBFixes(store, d, indirectPackage)
if err != nil {
return nil, fmt.Errorf("failed to find vulnerabilities for apk upstream source package: %w", err)
return nil, err
}
matches = append(matches, indirectMatches...)
}
Expand Down