diff --git a/grype/pkg/package.go b/grype/pkg/package.go index 3297f967969..aa57e0bcc8d 100644 --- a/grype/pkg/package.go +++ b/grype/pkg/package.go @@ -7,6 +7,7 @@ import ( "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/stringutil" + packageurl "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" @@ -100,6 +101,22 @@ 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 (p Package) DistroFragmentFromPURL() string { + if p.PURL == "" { + return "" + } + purl, err := packageurl.FromString(p.PURL) + if err != nil { + return "" + } + for k, v := range purl.Qualifiers.Map() { + if k == "distro" && v != "" { + return v + } + } + return "" +} + func removePackagesByOverlap(catalog *pkg.Collection, relationships []artifact.Relationship, distro *linux.Release) *pkg.Collection { byOverlap := map[artifact.ID]artifact.Relationship{} for _, r := range relationships { diff --git a/grype/vulnerability_matcher.go b/grype/vulnerability_matcher.go index 14f5299e604..0a3b28c1dec 100644 --- a/grype/vulnerability_matcher.go +++ b/grype/vulnerability_matcher.go @@ -94,9 +94,16 @@ func (m *VulnerabilityMatcher) findDBMatches(pkgs []pkg.Package, context pkg.Con var ignoredMatches []match.IgnoredMatch log.Trace("finding matches against DB") - matches, err := m.searchDBForMatches(context.Distro, pkgs, progressMonitor) - if err != nil { - return nil, nil, fmt.Errorf("unable to find matches in DB: %w", err) + // TODO: split pkgs by distro + packagesByDistro := groupPackagesByDistro(context.Distro, pkgs) + matches := match.NewMatches() + for d, ps := range packagesByDistro { + release := linuxReleaseFromDistroString(d, context.Distro) + toAdd, err := m.searchDBForMatches(release, ps, progressMonitor) + if err != nil { + return nil, nil, fmt.Errorf("unable to find matches in DB: %w", err) + } + matches.Add(toAdd.Sorted()...) } matches, ignoredMatches = m.applyIgnoreRules(matches) @@ -115,10 +122,42 @@ func (m *VulnerabilityMatcher) findDBMatches(pkgs []pkg.Package, context pkg.Con } if m.FailSeverity != nil && HasSeverityAtOrAbove(m.Store, *m.FailSeverity, matches) { - err = grypeerr.ErrAboveSeverityThreshold + return &matches, ignoredMatches, grypeerr.ErrAboveSeverityThreshold + } + + return &matches, ignoredMatches, nil +} + +func linuxReleaseFromDistroString(d string, detected *linux.Release) *linux.Release { + parts := strings.SplitN(d, "-", 2) + if len(parts) == 2 { + return &linux.Release{ + ID: parts[0], + Version: parts[1], + } + } + return detected +} + +func groupPackagesByDistro(detectedDistro *linux.Release, pkgs []pkg.Package) map[string][]pkg.Package { + packagesByDistro := make(map[string][]pkg.Package) + for _, p := range pkgs { + packageDistro := p.DistroFragmentFromPURL() + if packageDistro == "" { + // If we don't have a distro from the PURL + // assume that the packages was installed from the distro + // detected on the source + // detectedDistro.String() never has the same format as + // the distro fragment from the PURL + // TODO: which counts on a coincidence + if detectedDistro != nil { + packageDistro = detectedDistro.String() + } + } + packagesByDistro[packageDistro] = append(packagesByDistro[packageDistro], p) } - return &matches, ignoredMatches, err + return packagesByDistro } func (m *VulnerabilityMatcher) searchDBForMatches( @@ -138,6 +177,7 @@ func (m *VulnerabilityMatcher) searchDBForMatches( } if d != nil && d.Disabled() { log.Warnf("unsupported linux distribution: %s", d.Name()) + // TODO: unsupported distro with supported packages should be allowed return match.NewMatches(), nil } } diff --git a/grype/vulnerability_matcher_test.go b/grype/vulnerability_matcher_test.go index e72df760ab7..0da48b70d1b 100644 --- a/grype/vulnerability_matcher_test.go +++ b/grype/vulnerability_matcher_test.go @@ -127,6 +127,21 @@ func defaultStubFn(d *mockStore) { }, } + d.metadata["ALAS-2023-351"] = map[string]*grypeDB.VulnerabilityMetadata{ + "amazon:distro:amazonlinux:2023": { + ID: "ALAS-2023-351", + Namespace: "amazon:distro:amazonlinux:2023", + Severity: "medium", + }, + } + d.metadata["ALAS-2023-348"] = map[string]*grypeDB.VulnerabilityMetadata{ + "amazon:distro:amazonlinux:2023": { + ID: "ALAS-2023-348", + Namespace: "amazon:distro:amazonlinux:2023", + Severity: "medium", + }, + } + // VULNERABILITIES /////////////////////////////////////////////////////////////////////////// d.vulnerabilities["debian:distro:debian:8"] = map[string][]grypeDB.Vulnerability{ "neutron": { @@ -146,6 +161,28 @@ func defaultStubFn(d *mockStore) { }, }, } + + d.vulnerabilities["amazon:distro:amazonlinux:2023"] = map[string][]grypeDB.Vulnerability{ + "libtiff-tools": { + { + PackageName: "libtiff-tools", + Namespace: "amazon:distro:amazonlinux:2023", + VersionConstraint: "< 4.4.0-4.amzn2023.0.14", + ID: "ALAS-2023-351", + VersionFormat: "rpm", + }, + }, + "wireshark-cli": { + { + ID: "ALAS-2023-348", + PackageName: "wireshark-cli", + VersionConstraint: "< 4.0.8-2.amzn2023.0.1", + VersionFormat: "rpm", + Namespace: "amazon:distro:amazonlinux:2023", + }, + }, + } + d.vulnerabilities["github:language:ruby"] = map[string][]grypeDB.Vulnerability{ "activerecord": { { @@ -321,6 +358,14 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { Language: syftPkg.Ruby, } + wiresharkPackage := pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "wireshark-cli", + Version: "4.0.6-2.amzn2023.0.1", + Type: syftPkg.RpmPkg, + PURL: "pkg:rpm/amzn/wireshark-cli@4.0.6-2.amzn2023.0.1?arch=aarch64&epoch=1&upstream=wireshark-4.0.6-2.amzn2023.0.1.src.rpm&distro=amzn-2023", + } + type fields struct { Store store.Store Matchers []matcher.Matcher @@ -413,6 +458,89 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { wantIgnoredMatches: nil, wantErr: nil, }, + { + name: "matches from multiple distros: rpm on deb8", + fields: fields{ + Store: str, + Matchers: matcher.NewDefaultMatchers(matcher.Config{}), + }, + args: args{ + pkgs: []pkg.Package{ + neutron2013Pkg, + wiresharkPackage, + }, + context: pkg.Context{ + Distro: &linux.Release{ + ID: "debian", + VersionID: "8", + }, + }, + }, + wantMatches: match.NewMatches( + match.Match{ + Vulnerability: vulnerability.Vulnerability{ + Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat), + ID: "CVE-2014-fake-1", + Namespace: "debian:distro:debian:8", + PackageQualifiers: []qualifier.Qualifier{}, + CPEs: []cpe.CPE{}, + Advisories: []vulnerability.Advisory{}, + }, + Package: neutron2013Pkg, + Details: match.Details{ + { + Type: match.ExactDirectMatch, + SearchedBy: map[string]any{ + "distro": map[string]string{"type": "debian", "version": "8"}, + "namespace": "debian:distro:debian:8", + "package": map[string]string{"name": "neutron", "version": "2013.1.1-1"}, + }, + Found: map[string]any{ + "versionConstraint": "< 2014.1.3-6 (deb)", + "vulnerabilityID": "CVE-2014-fake-1", + }, + Matcher: "dpkg-matcher", + Confidence: 1, + }, + }, + }, + match.Match{ + Vulnerability: vulnerability.Vulnerability{ + Constraint: version.MustGetConstraint("", version.RpmFormat), + PackageQualifiers: []qualifier.Qualifier{}, + CPEs: []cpe.CPE{}, + ID: "ALAS-2023-348", + Namespace: "amazon:distro:amazonlinux:2023", + Advisories: []vulnerability.Advisory{}, + }, + Package: wiresharkPackage, + Details: match.Details{ + { + Type: match.ExactDirectMatch, + Matcher: "rpm-matcher", + SearchedBy: map[string]any{ + "distro": map[string]string{ + "type": "amazonlinux", + "version": "2023", + }, + "namespace": "amazon:distro:amazonlinux:2023", + "package": map[string]string{ + "name": "wireshark-cli", + "version": "0:4.0.6-2.amzn2023.0.1", + }, + }, + Found: map[string]any{ + "versionConstraint": "< 4.0.8-2.amzn2023.0.1 (rpm)", + "vulnerabilityID": "ALAS-2023-348", + }, + Confidence: 1.0, + }, + }, + }, + ), + wantIgnoredMatches: nil, + wantErr: nil, + }, { name: "fail on severity threshold", fields: fields{