diff --git a/grype/match/ignore.go b/grype/match/ignore.go index 83bb38f81fd..f8bc6cc29d3 100644 --- a/grype/match/ignore.go +++ b/grype/match/ignore.go @@ -29,6 +29,7 @@ type IgnoredMatch struct { // rule to apply. type IgnoreRule struct { Vulnerability string `yaml:"vulnerability" json:"vulnerability" mapstructure:"vulnerability"` + IncludeAliases bool `yaml:"include-aliases" json:"include-aliases" mapstructure:"include-aliases"` Reason string `yaml:"reason" json:"reason" mapstructure:"reason"` Namespace string `yaml:"namespace" json:"namespace" mapstructure:"namespace"` FixState string `yaml:"fix-state" json:"fix-state" mapstructure:"fix-state"` @@ -139,7 +140,7 @@ func getIgnoreConditionsForRule(rule IgnoreRule) []ignoreCondition { var ignoreConditions []ignoreCondition if v := rule.Vulnerability; v != "" { - ignoreConditions = append(ignoreConditions, ifVulnerabilityApplies(v)) + ignoreConditions = append(ignoreConditions, ifVulnerabilityApplies(v, rule.IncludeAliases)) } if ns := rule.Namespace; ns != "" { @@ -190,9 +191,19 @@ func ifFixStateApplies(fs string) ignoreCondition { } } -func ifVulnerabilityApplies(vulnerability string) ignoreCondition { +func ifVulnerabilityApplies(vulnerability string, includeAliases bool) ignoreCondition { return func(match Match) bool { - return vulnerability == match.Vulnerability.ID + if vulnerability == match.Vulnerability.ID { + return true + } + if includeAliases { + for _, related := range match.Vulnerability.RelatedVulnerabilities { + if vulnerability == related.ID { + return true + } + } + } + return false } } diff --git a/grype/match/ignore_test.go b/grype/match/ignore_test.go index c4e92114b72..115a5d9ecb4 100644 --- a/grype/match/ignore_test.go +++ b/grype/match/ignore_test.go @@ -41,6 +41,11 @@ var ( Fix: vulnerability.Fix{ State: vulnerability.FixStateNotFixed, }, + RelatedVulnerabilities: []vulnerability.Reference{ + { + ID: "CVE-123", + }, + }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), @@ -369,6 +374,40 @@ func TestApplyIgnoreRules(t *testing.T) { }, }, }, + { + name: "ignore related matches", + allMatches: allMatches, + ignoreRules: []IgnoreRule{ + { + Vulnerability: "CVE-123", + IncludeAliases: true, + }, + }, + expectedRemainingMatches: []Match{ + allMatches[2], + allMatches[3], + }, + expectedIgnoredMatches: []IgnoredMatch{ + { + Match: allMatches[0], + AppliedIgnoreRules: []IgnoreRule{ + { + Vulnerability: "CVE-123", + IncludeAliases: true, + }, + }, + }, + { + Match: allMatches[1], + AppliedIgnoreRules: []IgnoreRule{ + { + Vulnerability: "CVE-123", + IncludeAliases: true, + }, + }, + }, + }, + }, { name: "ignore subset of matches", allMatches: allMatches, diff --git a/grype/match/matcher.go b/grype/match/matcher.go index ceb0e42183d..b354e9d43d1 100644 --- a/grype/match/matcher.go +++ b/grype/match/matcher.go @@ -17,7 +17,7 @@ type Matcher interface { // 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) + Match(vp vulnerability.Provider, p pkg.Package) ([]Match, []IgnoreFilter, error) } // fatalError can be returned from a Matcher to indicate the matching process should stop. diff --git a/grype/matcher/apk/matcher.go b/grype/matcher/apk/matcher.go index b75e86c476f..a9caba68d46 100644 --- a/grype/matcher/apk/matcher.go +++ b/grype/matcher/apk/matcher.go @@ -4,7 +4,6 @@ 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" @@ -26,7 +25,7 @@ func (m *Matcher) Type() match.MatcherType { return match.ApkMatcher } -func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { var matches []match.Match // direct matches with package itself @@ -218,9 +217,8 @@ func (m *Matcher) findMatchesForOriginPackage(store vulnerability.Provider, p pk // 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(provider 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 && d.Type != distro.MinimOS { +func (m *Matcher) findNaksForPackage(provider vulnerability.Provider, p pkg.Package) ([]match.IgnoreFilter, error) { + if p.Distro == nil { return nil, nil } @@ -248,21 +246,24 @@ func (m *Matcher) findNaksForPackage(provider vulnerability.Provider, p pkg.Pack naks = append(naks, upstreamNaks...) } - var ignores []match.IgnoredMatch + meta, ok := p.Metadata.(pkg.ApkMetadata) + if !ok { + return nil, nil + } + + var ignores []match.IgnoreFilter 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", - }, - }, - }) + for _, f := range meta.Files { + ignores = append(ignores, + match.IgnoreRule{ + Vulnerability: nak.ID, + IncludeAliases: true, + Reason: "Explicit APK NAK", + Package: match.IgnoreRulePackage{ + Location: f.Path, + }, + }) + } } return ignores, nil diff --git a/grype/matcher/bitnami/matcher.go b/grype/matcher/bitnami/matcher.go index dad9cfab598..4c8999cda0c 100644 --- a/grype/matcher/bitnami/matcher.go +++ b/grype/matcher/bitnami/matcher.go @@ -18,7 +18,7 @@ func (m *Matcher) Type() match.MatcherType { return match.BitnamiMatcher } -func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { // Bitnami packages' metadata are built from the package URL which contains // info such as the package name, version, revision, distro or architecture. // ref: https://github.com/anchore/syft/blob/main/syft/pkg/bitnami.go#L3-L13 diff --git a/grype/matcher/dotnet/matcher.go b/grype/matcher/dotnet/matcher.go index a78c1d11ccb..461dd2e56ef 100644 --- a/grype/matcher/dotnet/matcher.go +++ b/grype/matcher/dotnet/matcher.go @@ -30,6 +30,6 @@ func (m *Matcher) Type() match.MatcherType { return match.DotnetMatcher } -func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } diff --git a/grype/matcher/dpkg/matcher.go b/grype/matcher/dpkg/matcher.go index 3470661a827..3650edd3def 100644 --- a/grype/matcher/dpkg/matcher.go +++ b/grype/matcher/dpkg/matcher.go @@ -21,7 +21,7 @@ func (m *Matcher) Type() match.MatcherType { return match.DpkgMatcher } -func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { matches := make([]match.Match, 0) sourceMatches, err := m.matchUpstreamPackages(store, p) diff --git a/grype/matcher/golang/matcher.go b/grype/matcher/golang/matcher.go index bdb014ca9b8..7a0c4a177bb 100644 --- a/grype/matcher/golang/matcher.go +++ b/grype/matcher/golang/matcher.go @@ -34,7 +34,7 @@ func (m *Matcher) Type() match.MatcherType { return match.GoModuleMatcher } -func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { matches := make([]match.Match, 0) mainModule := "" diff --git a/grype/matcher/internal/common.go b/grype/matcher/internal/common.go index b6b5e2e223b..23f5d14ca18 100644 --- a/grype/matcher/internal/common.go +++ b/grype/matcher/internal/common.go @@ -9,9 +9,9 @@ import ( "github.com/anchore/grype/internal/log" ) -func MatchPackageByEcosystemAndCPEs(store vulnerability.Provider, p pkg.Package, matcher match.MatcherType, includeCPEs bool) ([]match.Match, []match.IgnoredMatch, error) { +func MatchPackageByEcosystemAndCPEs(store vulnerability.Provider, p pkg.Package, matcher match.MatcherType, includeCPEs bool) ([]match.Match, []match.IgnoreFilter, error) { var matches []match.Match - var ignored []match.IgnoredMatch + var ignored []match.IgnoreFilter for _, name := range store.PackageSearchNames(p) { nameMatches, nameIgnores, err := MatchPackageByEcosystemPackageNameAndCPEs(store, p, name, matcher, includeCPEs) @@ -25,7 +25,7 @@ func MatchPackageByEcosystemAndCPEs(store vulnerability.Provider, p pkg.Package, return matches, ignored, nil } -func MatchPackageByEcosystemPackageNameAndCPEs(store vulnerability.Provider, p pkg.Package, packageName string, matcher match.MatcherType, includeCPEs bool) ([]match.Match, []match.IgnoredMatch, error) { +func MatchPackageByEcosystemPackageNameAndCPEs(store vulnerability.Provider, p pkg.Package, packageName string, matcher match.MatcherType, includeCPEs bool) ([]match.Match, []match.IgnoreFilter, error) { matches, ignored, err := MatchPackageByEcosystemPackageName(store, p, packageName, matcher) if err != nil { log.Debugf("could not match by package ecosystem (package=%+v): %v", p, err) diff --git a/grype/matcher/internal/distro.go b/grype/matcher/internal/distro.go index 65179bc66ba..34adf2f64a2 100644 --- a/grype/matcher/internal/distro.go +++ b/grype/matcher/internal/distro.go @@ -13,7 +13,7 @@ import ( "github.com/anchore/grype/internal/log" ) -func MatchPackageByDistro(provider vulnerability.Provider, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, []match.IgnoredMatch, error) { +func MatchPackageByDistro(provider vulnerability.Provider, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, []match.IgnoreFilter, error) { if p.Distro == nil { return nil, nil, nil } diff --git a/grype/matcher/internal/language.go b/grype/matcher/internal/language.go index 645d87d4660..66cbec79555 100644 --- a/grype/matcher/internal/language.go +++ b/grype/matcher/internal/language.go @@ -12,9 +12,9 @@ import ( "github.com/anchore/grype/internal/log" ) -func MatchPackageByLanguage(store vulnerability.Provider, p pkg.Package, matcherType match.MatcherType) ([]match.Match, []match.IgnoredMatch, error) { +func MatchPackageByLanguage(store vulnerability.Provider, p pkg.Package, matcherType match.MatcherType) ([]match.Match, []match.IgnoreFilter, error) { var matches []match.Match - var ignored []match.IgnoredMatch + var ignored []match.IgnoreFilter for _, name := range store.PackageSearchNames(p) { nameMatches, nameIgnores, err := MatchPackageByEcosystemPackageName(store, p, name, matcherType) @@ -28,7 +28,7 @@ func MatchPackageByLanguage(store vulnerability.Provider, p pkg.Package, matcher return matches, ignored, nil } -func MatchPackageByEcosystemPackageName(provider vulnerability.Provider, p pkg.Package, packageName string, matcherType match.MatcherType) ([]match.Match, []match.IgnoredMatch, error) { +func MatchPackageByEcosystemPackageName(provider vulnerability.Provider, p pkg.Package, packageName string, matcherType match.MatcherType) ([]match.Match, []match.IgnoreFilter, error) { if isUnknownVersion(p.Version) { log.WithFields("package", p.Name).Trace("skipping package with unknown version") return nil, nil, nil diff --git a/grype/matcher/java/matcher.go b/grype/matcher/java/matcher.go index 218200aca8f..11567ad6767 100644 --- a/grype/matcher/java/matcher.go +++ b/grype/matcher/java/matcher.go @@ -50,7 +50,7 @@ func (m *Matcher) Type() match.MatcherType { return match.JavaMatcher } -func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { var matches []match.Match if m.cfg.SearchMavenUpstream { diff --git a/grype/matcher/javascript/matcher.go b/grype/matcher/javascript/matcher.go index c0057ccbdc9..cb0d03e46a3 100644 --- a/grype/matcher/javascript/matcher.go +++ b/grype/matcher/javascript/matcher.go @@ -30,6 +30,6 @@ func (m *Matcher) Type() match.MatcherType { return match.JavascriptMatcher } -func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } diff --git a/grype/matcher/mock/matcher.go b/grype/matcher/mock/matcher.go index 11e444f0510..235fa47eb43 100644 --- a/grype/matcher/mock/matcher.go +++ b/grype/matcher/mock/matcher.go @@ -11,7 +11,7 @@ import ( // MatchFunc is a function that takes a vulnerability provider and a package, // and returns matches, ignored matches, and an error. -type MatchFunc func(vp vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) +type MatchFunc func(vp vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) // Matcher is a mock implementation of the match.Matcher interface. This is // intended for testing purposes only. @@ -36,7 +36,7 @@ func (m Matcher) Type() match.MatcherType { return "MOCK" } -func (m Matcher) Match(vp vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m Matcher) Match(vp vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { if m.matchFunc != nil { return m.matchFunc(vp, p) } diff --git a/grype/matcher/msrc/matcher.go b/grype/matcher/msrc/matcher.go index edb038b2619..7682b07facf 100644 --- a/grype/matcher/msrc/matcher.go +++ b/grype/matcher/msrc/matcher.go @@ -22,7 +22,7 @@ func (m *Matcher) Type() match.MatcherType { return match.MsrcMatcher } -func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, 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) diff --git a/grype/matcher/portage/matcher.go b/grype/matcher/portage/matcher.go index 2126cd337c7..cd8c53c689f 100644 --- a/grype/matcher/portage/matcher.go +++ b/grype/matcher/portage/matcher.go @@ -19,6 +19,6 @@ func (m *Matcher) Type() match.MatcherType { return match.PortageMatcher } -func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return internal.MatchPackageByDistro(store, p, m.Type()) } diff --git a/grype/matcher/python/matcher.go b/grype/matcher/python/matcher.go index 78916a52e3b..56111525318 100644 --- a/grype/matcher/python/matcher.go +++ b/grype/matcher/python/matcher.go @@ -30,6 +30,6 @@ func (m *Matcher) Type() match.MatcherType { return match.PythonMatcher } -func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } diff --git a/grype/matcher/rpm/matcher.go b/grype/matcher/rpm/matcher.go index 59971faaa9e..e8fc075aabf 100644 --- a/grype/matcher/rpm/matcher.go +++ b/grype/matcher/rpm/matcher.go @@ -23,7 +23,7 @@ func (m *Matcher) Type() match.MatcherType { } //nolint:funlen -func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { matches := make([]match.Match, 0) // let's match with a synthetic package that doesn't exist. We will create a new diff --git a/grype/matcher/ruby/matcher.go b/grype/matcher/ruby/matcher.go index 0fe094e511f..32ebf87d488 100644 --- a/grype/matcher/ruby/matcher.go +++ b/grype/matcher/ruby/matcher.go @@ -30,6 +30,6 @@ func (m *Matcher) Type() match.MatcherType { return match.RubyGemMatcher } -func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } diff --git a/grype/matcher/rust/matcher.go b/grype/matcher/rust/matcher.go index 9923cb8ad0c..8691cd05e72 100644 --- a/grype/matcher/rust/matcher.go +++ b/grype/matcher/rust/matcher.go @@ -30,6 +30,6 @@ func (m *Matcher) Type() match.MatcherType { return match.RustMatcher } -func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } diff --git a/grype/matcher/stock/matcher.go b/grype/matcher/stock/matcher.go index 26dd782f2cd..e7a16cb923a 100644 --- a/grype/matcher/stock/matcher.go +++ b/grype/matcher/stock/matcher.go @@ -30,6 +30,6 @@ func (m *Matcher) Type() match.MatcherType { return match.StockMatcher } -func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } diff --git a/grype/vulnerability_matcher.go b/grype/vulnerability_matcher.go index 0df6da3ca63..b991e03b2bd 100644 --- a/grype/vulnerability_matcher.go +++ b/grype/vulnerability_matcher.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "runtime/debug" + "slices" "strings" "github.com/wagoodman/go-partybus" @@ -143,7 +144,7 @@ func (m *VulnerabilityMatcher) searchDBForMatches( progressMonitor *monitorWriter, ) (match.Matches, error) { var allMatches []match.Match - var allIgnored []match.IgnoredMatch + var allIgnorers []match.IgnoreFilter matcherIndex, defaultMatcher := newMatcherIndex(m.Matchers) if defaultMatcher == nil { @@ -166,7 +167,7 @@ func (m *VulnerabilityMatcher) searchDBForMatches( matchAgainst = []match.Matcher{defaultMatcher} } for _, theMatcher := range matchAgainst { - matches, ignoredMatches, err := callMatcherSafely(theMatcher, m.VulnerabilityProvider, p) + matches, ignorers, err := callMatcherSafely(theMatcher, m.VulnerabilityProvider, p) if err != nil { if match.IsFatalError(err) { return match.Matches{}, err @@ -176,7 +177,7 @@ func (m *VulnerabilityMatcher) searchDBForMatches( matcherErrs = append(matcherErrs, err) } - allIgnored = append(allIgnored, ignoredMatches...) + allIgnorers = append(allIgnorers, ignorers...) // Filter out matches based on records in the database exclusion table and hard-coded rules filtered, dropped := match.ApplyExplicitIgnoreRules(m.ExclusionProvider, match.NewMatches(matches...)) @@ -198,7 +199,7 @@ func (m *VulnerabilityMatcher) searchDBForMatches( } // apply ignores based on matchers returning ignore rules - filtered, dropped := match.ApplyIgnoreFilters(allMatches, ignoredMatchFilter(allIgnored)) + filtered, dropped := match.ApplyIgnoreFilters(allMatches, ignoredMatchFilter(allIgnorers)) logIgnoredMatches(dropped) // get deduplicated set of matches @@ -210,7 +211,7 @@ func (m *VulnerabilityMatcher) searchDBForMatches( return res, errors.Join(matcherErrs...) } -func callMatcherSafely(m match.Matcher, vp vulnerability.Provider, p pkg.Package) (matches []match.Match, ignoredMatches []match.IgnoredMatch, err error) { +func callMatcherSafely(m match.Matcher, vp vulnerability.Provider, p pkg.Package) (matches []match.Match, ignoredMatches []match.IgnoreFilter, err error) { // handle individual matcher panics defer func() { if e := recover(); e != nil { @@ -314,37 +315,40 @@ func (m *VulnerabilityMatcher) normalizeByCVE(match match.Match) match.Match { // ignoreRulesByLocation implements match.IgnoreFilter to filter each matching // package that overlaps by location and have the same vulnerability ID (CVE) type ignoreRulesByLocation struct { + remainingFilters []match.IgnoreFilter 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} - } + if matched := rule.IgnoreMatch(m); matched != nil { + return matched } } } + for _, f := range i.remainingFilters { + if matched := f.IgnoreMatch(m); matched != nil { + return matched + } + } return nil } -// ignoreMatchFilter creates an ignore filter based on the provided IgnoredMatches to filter out "the same" +// ignoredMatchFilter creates an ignore filter based on location-based IgnoredMatches to filter out "the same" // vulnerabilities reported by other matchers based on overlapping file locations -func ignoredMatchFilter(ignores []match.IgnoredMatch) match.IgnoreFilter { +func ignoredMatchFilter(ignores []match.IgnoreFilter) 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...) - } + // the returned slice of remaining rules are not location-based rules + out.remainingFilters = slices.DeleteFunc(ignores, func(ignore match.IgnoreFilter) bool { + rule, ok := ignore.(match.IgnoreRule) + if ok && rule.Package.Location != "" && !strings.ContainsRune(rule.Package.Location, '*') { + // this rule is handled with location lookups, remove it from the remaining filter list + out.locationToIgnoreRules[rule.Package.Location] = append(out.locationToIgnoreRules[rule.Package.Location], rule) + return true } - } + return false + }) return out } diff --git a/grype/vulnerability_matcher_test.go b/grype/vulnerability_matcher_test.go index b9a7010578e..411e8ad1fbc 100644 --- a/grype/vulnerability_matcher_test.go +++ b/grype/vulnerability_matcher_test.go @@ -1033,21 +1033,21 @@ func Test_fatalErrors(t *testing.T) { }{ { name: "no error", - matcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + matcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return nil, nil, nil }, assertErr: assert.NoError, }, { name: "non-fatal error", - matcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + matcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return nil, nil, errors.New("some error") }, assertErr: assert.NoError, }, { name: "fatal error", - matcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + matcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return nil, nil, match.NewFatalError(match.UnknownMatcherType, errors.New("some error")) }, assertErr: assert.Error, @@ -1223,7 +1223,7 @@ func Test_indexFalsePositivesByLocation(t *testing.T) { apkMatcher := &apk.Matcher{} var allMatches []match.Match - var allIgnores []match.IgnoredMatch + var allIgnores []match.IgnoreFilter for _, p := range tt.pkgs { matches, ignores, err := apkMatcher.Match(vp, p) require.NoError(t, err) @@ -1233,13 +1233,14 @@ func Test_indexFalsePositivesByLocation(t *testing.T) { 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) - } + rule, ok := ignore.(match.IgnoreRule) + if !ok { + continue } + if rule.Package.Location == "" { + continue + } + actualResult[rule.Package.Location] = append(actualResult[rule.Package.Location], rule.Vulnerability) } assert.Equal(t, tt.expectedResult, actualResult) }) @@ -1351,25 +1352,14 @@ func Test_filterMatchesUsingDistroFalsePositives(t *testing.T) { for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { - var allIgnores []match.IgnoredMatch + var allIgnores []match.IgnoreFilter 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, - }, + allIgnores = append(allIgnores, match.IgnoreRule{ + Vulnerability: cve, + IncludeAliases: true, + Package: match.IgnoreRulePackage{ + Location: path, }, }) } @@ -1384,6 +1374,77 @@ func Test_filterMatchesUsingDistroFalsePositives(t *testing.T) { } } +func Test_ignoredMatchFilter(t *testing.T) { + matches := []match.Match{ + { + Vulnerability: vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-123", + }, + }, + Package: pkg.Package{ + Locations: file.NewLocationSet(file.NewLocation("/usr/bin/thing")), + }, + }, + { + Vulnerability: vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-456", + }, + }, + }, + { + Vulnerability: vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-789", + }, + Status: "filter-me", + }, + }, + } + + ignores := []match.IgnoreFilter{ + match.IgnoreRule{ + Reason: "test-location-ignore-rule", + Package: match.IgnoreRulePackage{ + Location: "/usr/bin/thing", + }, + }, + testIgnoreFilter{ + f: func(m match.Match) bool { + return m.Vulnerability.Status == "filter-me" + }, + }, + } + + f := ignoredMatchFilter(ignores) + + var ignoredReasons []string + for _, m := range matches { + got := f.IgnoreMatch(m) + for _, r := range got { + ignoredReasons = append(ignoredReasons, r.Reason) + } + } + + require.ElementsMatch(t, []string{"test-location-ignore-rule", "test-filtered"}, ignoredReasons) +} + +type testIgnoreFilter struct { + f func(match.Match) bool +} + +func (t testIgnoreFilter) IgnoreMatch(m match.Match) []match.IgnoreRule { + if t.f(m) { + return []match.IgnoreRule{ + { + Reason: "test-filtered", + }, + } + } + return nil +} + type panicyMatcher struct { matcherType match.MatcherType } @@ -1396,7 +1457,7 @@ func (m *panicyMatcher) Type() match.MatcherType { return m.matcherType } -func (m *panicyMatcher) Match(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { +func (m *panicyMatcher) Match(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { panic("test panic message") }