diff --git a/grype/matcher/apk/matcher_test.go b/grype/matcher/apk/matcher_test.go index b73ddede33d..6240e12d1ee 100644 --- a/grype/matcher/apk/matcher_test.go +++ b/grype/matcher/apk/matcher_test.go @@ -924,3 +924,170 @@ func Test_nakConstraint(t *testing.T) { }) } } + +func Test_nakIgnoreRules(t *testing.T) { + cases := []struct { + name string + pkgs []pkg.Package + vulns []vulnerability.Vulnerability + expectedLocationIgnores map[string][]string + errAssertion assert.ErrorAssertionFunc + }{ + { + name: "false positive in wolfi package adds index entry", + pkgs: []pkg.Package{ + { + Name: "foo", + Distro: &distro.Distro{Type: distro.Wolfi}, + Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ + { + Path: "/bin/foo-binary", + }, + }}, + }, + }, + vulns: []vulnerability.Vulnerability{ + { + Reference: vulnerability.Reference{ + ID: "GHSA-2014-fake-3", + Namespace: "wolfi:distro:wolfi:rolling", + }, + PackageName: "foo", + Constraint: version.MustGetConstraint("< 0", version.ApkFormat), + }, + }, + expectedLocationIgnores: map[string][]string{ + "/bin/foo-binary": {"GHSA-2014-fake-3"}, + }, + errAssertion: assert.NoError, + }, + { + name: "false positive in wolfi subpackage adds index entry", + pkgs: []pkg.Package{ + { + Name: "subpackage-foo", + Distro: &distro.Distro{Type: distro.Wolfi}, + Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ + { + Path: "/bin/foo-subpackage-binary", + }, + }}, + Upstreams: []pkg.UpstreamPackage{ + { + Name: "origin-foo", + }, + }, + }, + }, + vulns: []vulnerability.Vulnerability{ + { + Reference: vulnerability.Reference{ + ID: "GHSA-2014-fake-3", + Namespace: "wolfi:distro:wolfi:rolling", + }, + PackageName: "origin-foo", + Constraint: version.MustGetConstraint("< 0", version.ApkFormat), + }, + }, + expectedLocationIgnores: map[string][]string{ + "/bin/foo-subpackage-binary": {"GHSA-2014-fake-3"}, + }, + errAssertion: assert.NoError, + }, + { + name: "fixed vuln (not a false positive) in wolfi package", + pkgs: []pkg.Package{ + { + Name: "foo", + Distro: &distro.Distro{Type: distro.Wolfi}, + Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ + { + Path: "/bin/foo-binary", + }, + }}, + }, + }, + vulns: []vulnerability.Vulnerability{ + { + Reference: vulnerability.Reference{ + ID: "GHSA-2014-fake-3", + Namespace: "wolfi:distro:wolfi:rolling", + }, + PackageName: "foo", + Constraint: version.MustGetConstraint("< 1.2.3-r4", version.ApkFormat), + }, + }, + expectedLocationIgnores: map[string][]string{}, + errAssertion: assert.NoError, + }, + { + name: "no vuln data for wolfi package", + pkgs: []pkg.Package{ + { + Name: "foo", + Distro: &distro.Distro{Type: distro.Wolfi}, + Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ + { + Path: "/bin/foo-binary", + }, + }}, + }, + }, + vulns: []vulnerability.Vulnerability{}, + expectedLocationIgnores: map[string][]string{}, + errAssertion: assert.NoError, + }, + { + name: "no files listed for a wolfi package", + pkgs: []pkg.Package{ + { + Name: "foo", + Distro: &distro.Distro{Type: distro.Wolfi}, + Metadata: pkg.ApkMetadata{Files: nil}, + }, + }, + vulns: []vulnerability.Vulnerability{ + { + Reference: vulnerability.Reference{ + ID: "GHSA-2014-fake-3", + Namespace: "wolfi:distro:wolfi:rolling", + }, + PackageName: "foo", + Constraint: version.MustGetConstraint("< 0", version.ApkFormat), + }, + }, + expectedLocationIgnores: map[string][]string{}, + errAssertion: assert.NoError, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + // create mock vulnerability provider + vp := mock.VulnerabilityProvider(tt.vulns...) + apkMatcher := &Matcher{} + + var allMatches []match.Match + var allIgnores []match.IgnoreFilter + for _, p := range tt.pkgs { + matches, ignores, err := apkMatcher.Match(vp, p) + require.NoError(t, err) + allMatches = append(allMatches, matches...) + allIgnores = append(allIgnores, ignores...) + } + + actualResult := map[string][]string{} + for _, ignore := range allIgnores { + rule, ok := ignore.(match.IgnoreRule) + if !ok { + require.Fail(t, "expected ignore to be of type IgnoreRule") + } + if rule.Package.Location == "" { + require.Fail(t, "expected package location to be set in ignore rule") + } + actualResult[rule.Package.Location] = append(actualResult[rule.Package.Location], rule.Vulnerability) + } + require.Equal(t, tt.expectedLocationIgnores, actualResult) + }) + } +} diff --git a/grype/matcher/internal/language.go b/grype/matcher/internal/language.go index b9a18c1ef4e..0cf9ae60c2e 100644 --- a/grype/matcher/internal/language.go +++ b/grype/matcher/internal/language.go @@ -2,6 +2,7 @@ package internal import ( "fmt" + "slices" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal/result" @@ -53,13 +54,50 @@ func MatchPackageByEcosystemPackageName(vp vulnerability.Provider, p pkg.Package // we want to perform the same results, but look for explicit naks, which indicates that a vulnerability should not apply criteria = append(criteria, search.ForUnaffected()) - resolutions, err := provider.FindResults(criteria...) + unaffected, err := provider.FindResults(criteria...) if err != nil { return nil, nil, fmt.Errorf("matcher failed to fetch resolution language=%q pkg=%q: %w", p.Language, p.Name, err) } // remove any disclosures that have been explicitly nacked - remaining := disclosures.Remove(resolutions) + remaining := disclosures.Remove(unaffected) - return remaining.ToMatches(), nil, err + return remaining.ToMatches(), constructIgnoreFilters(unaffected, p), err +} + +func constructIgnoreFilters(unaffectedVulns result.Set, p pkg.Package) []match.IgnoreFilter { + var ignores []match.IgnoreFilter + + // collect all IDs to exclude + var ids []string + for _, vulnResults := range unaffectedVulns { + for _, vulnResult := range vulnResults { + ids = append(ids, vulnResult.ID) + for _, vuln := range vulnResult.Vulnerabilities { + if !slices.Contains(ids, vuln.ID) { + ids = append(ids, vuln.ID) + } + for _, id := range vuln.RelatedVulnerabilities { + if !slices.Contains(ids, id.ID) { + ids = append(ids, id.ID) + } + } + } + } + } + + // ignore rules for all IDs + for _, id := range ids { + ignores = append(ignores, match.IgnoreRule{ + Vulnerability: id, + IncludeAliases: true, + Reason: "UnaffectedPackageEntry", + Package: match.IgnoreRulePackage{ + Type: string(p.Type), + Name: p.Name, + Version: p.Version, + }, + }) + } + return ignores } diff --git a/grype/matcher/internal/language_test.go b/grype/matcher/internal/language_test.go index 77397f1ec8a..6eafac941a0 100644 --- a/grype/matcher/internal/language_test.go +++ b/grype/matcher/internal/language_test.go @@ -15,6 +15,7 @@ import ( "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" + "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -243,6 +244,7 @@ func TestFindMatchesByPackageGolang(t *testing.T) { cases := []struct { p pkg.Package expMatches map[string]string + unaffected bool }{ { p: pkg.Package{ @@ -273,6 +275,7 @@ func TestFindMatchesByPackageGolang(t *testing.T) { Type: syftPkg.GoModulePkg, }, expMatches: map[string]string{"CVE-2017-fake-2": "< 1.3.1 (go)"}, + unaffected: true, // this vuln matches an unaffected entry }, } @@ -285,8 +288,107 @@ func TestFindMatchesByPackageGolang(t *testing.T) { return strings.Compare(a.Vulnerability.ID, b.Vulnerability.ID) }) require.NoError(t, err) - assert.Empty(t, ignored) + if c.unaffected { + assert.NotEmpty(t, ignored) + } else { + assert.Empty(t, ignored) + } assertMatchesUsingIDsForVulnerabilities(t, expectedMatchGolang(c.p, c.expMatches), actual) }) } } + +func Test_unaffectedPackageIgnoreRules(t *testing.T) { + someProjectCPE := cpe.Must(`cpe:2.3:a:some_vendor:some_project:*:*:*:*:*:*:*:*`, cpe.DeclaredSource) + provider := mock.VulnerabilityProvider([]vulnerability.Vulnerability{ + { + Reference: vulnerability.Reference{ID: "vuln1", Namespace: "github:language:python"}, + Constraint: version.MustGetConstraint("< 1.2.3", version.PythonFormat), + PackageName: "some_project", + Unaffected: false, + }, + { + Reference: vulnerability.Reference{ID: "vuln2", Namespace: "github:language:python"}, + Constraint: version.MustGetConstraint("< 1.2.3", version.PythonFormat), + PackageName: "some_project", + Unaffected: true, + }, + { + Reference: vulnerability.Reference{ID: "vuln2", Namespace: "nvd:cpe"}, + Constraint: version.MustGetConstraint("< 1.2.3", version.PythonFormat), + PackageName: "some_project", + CPEs: []cpe.CPE{someProjectCPE}, + Unaffected: false, + }, + }...) + + tests := []struct { + name string + pkg pkg.Package + expected []match.IgnoreFilter + }{ + { + name: "matching unaffected", + pkg: pkg.Package{ + Name: "some_project", + Version: "1.2.2", + Language: syftPkg.Python, + Type: syftPkg.PythonPkg, + CPEs: []cpe.CPE{someProjectCPE}, + }, + expected: []match.IgnoreFilter{ + match.IgnoreRule{ + Vulnerability: "vuln2", + IncludeAliases: true, + Reason: "UnaffectedPackageEntry", + Package: match.IgnoreRulePackage{ + Name: "some_project", + Version: "1.2.2", + Type: string(syftPkg.PythonPkg), + }, + }, + }, + }, + { + name: "not unaffected by version", + pkg: pkg.Package{ + Name: "some_project", + Version: "1.2.4", + Language: syftPkg.Python, + Type: syftPkg.PythonPkg, + CPEs: []cpe.CPE{someProjectCPE}, + }, + expected: nil, + }, + { + name: "not unaffected by name", + pkg: pkg.Package{ + Name: "some_other_project", + Version: "1.2.2", + Language: syftPkg.Python, + Type: syftPkg.PythonPkg, + CPEs: []cpe.CPE{someProjectCPE}, + }, + expected: nil, + }, + { + name: "not unaffected by type", + pkg: pkg.Package{ + Name: "some_project", + Version: "1.2.2", + Language: syftPkg.Go, + Type: syftPkg.GoModulePkg, + CPEs: []cpe.CPE{someProjectCPE}, + }, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, ignoreRules, err := MatchPackageByEcosystemPackageName(provider, tt.pkg, tt.pkg.Name, "") + require.NoError(t, err) + assert.Equal(t, tt.expected, ignoreRules) + }) + } +} diff --git a/grype/vulnerability_matcher.go b/grype/vulnerability_matcher.go index c6dcb579b13..1034e0cd22f 100644 --- a/grype/vulnerability_matcher.go +++ b/grype/vulnerability_matcher.go @@ -302,16 +302,24 @@ func (m *VulnerabilityMatcher) normalizeByCVE(match match.Match) match.Match { return match } -// ignoreRulesByLocation implements match.IgnoreFilter to filter each matching +// ignoreRulesByIndex 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 +type ignoreRulesByIndex struct { + remainingFilters []match.IgnoreFilter + locationIgnoreRules map[string][]match.IgnoreRule + packageNameIgnoreRules map[string][]match.IgnoreRule } -func (i ignoreRulesByLocation) IgnoreMatch(m match.Match) []match.IgnoreRule { +func (i ignoreRulesByIndex) IgnoreMatch(m match.Match) []match.IgnoreRule { + if nameRules := i.packageNameIgnoreRules[m.Package.Name]; nameRules != nil { + for _, rule := range nameRules { + if matched := rule.IgnoreMatch(m); matched != nil { + return matched + } + } + } for _, l := range m.Package.Locations.ToSlice() { - for _, rule := range i.locationToIgnoreRules[l.RealPath] { + for _, rule := range i.locationIgnoreRules[l.RealPath] { if matched := rule.IgnoreMatch(m); matched != nil { return matched } @@ -328,14 +336,23 @@ func (i ignoreRulesByLocation) IgnoreMatch(m match.Match) []match.IgnoreRule { // 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.IgnoreFilter) match.IgnoreFilter { - out := ignoreRulesByLocation{locationToIgnoreRules: map[string][]match.IgnoreRule{}} + out := ignoreRulesByIndex{ + locationIgnoreRules: map[string][]match.IgnoreRule{}, + packageNameIgnoreRules: map[string][]match.IgnoreRule{}, + } // 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 + if rule, ok := ignore.(match.IgnoreRule); ok { + // return true to remove rules handled with index lookups from the remaining filter list + if rule.Package.Location != "" && !strings.ContainsRune(rule.Package.Location, '*') { + out.locationIgnoreRules[rule.Package.Location] = append(out.locationIgnoreRules[rule.Package.Location], rule) + return true + } + if rule.Package.Name != "" { + // this rule is handled with location lookups, remove it from the remaining filter list + out.packageNameIgnoreRules[rule.Package.Name] = append(out.packageNameIgnoreRules[rule.Package.Name], rule) + return true + } } return false }) diff --git a/grype/vulnerability_matcher_test.go b/grype/vulnerability_matcher_test.go index 3caeeee5e08..dcc60fbdb35 100644 --- a/grype/vulnerability_matcher_test.go +++ b/grype/vulnerability_matcher_test.go @@ -17,7 +17,6 @@ import ( "github.com/anchore/grype/grype/grypeerr" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher" - "github.com/anchore/grype/grype/matcher/apk" matcherMock "github.com/anchore/grype/grype/matcher/mock" "github.com/anchore/grype/grype/matcher/ruby" "github.com/anchore/grype/grype/pkg" @@ -1062,292 +1061,233 @@ func Test_fatalErrors(t *testing.T) { } } -func Test_indexFalsePositivesByLocation(t *testing.T) { - cases := []struct { - name string - pkgs []pkg.Package - vulns []vulnerability.Vulnerability - expectedResult map[string][]string - errAssertion assert.ErrorAssertionFunc - }{ - { - name: "false positive in wolfi package adds index entry", - pkgs: []pkg.Package{ - { - Name: "foo", - Distro: &distro.Distro{Type: distro.Wolfi}, - Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ - { - Path: "/bin/foo-binary", - }, - }}, - }, - }, - vulns: []vulnerability.Vulnerability{ - { - Reference: vulnerability.Reference{ - ID: "GHSA-2014-fake-3", - Namespace: "wolfi:distro:wolfi:rolling", - }, - PackageName: "foo", - Constraint: version.MustGetConstraint("< 0", version.ApkFormat), - }, - }, - expectedResult: map[string][]string{ - "/bin/foo-binary": {"GHSA-2014-fake-3"}, - }, - errAssertion: assert.NoError, - }, - { - name: "false positive in wolfi subpackage adds index entry", - pkgs: []pkg.Package{ - { - Name: "subpackage-foo", - Distro: &distro.Distro{Type: distro.Wolfi}, - Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ - { - Path: "/bin/foo-subpackage-binary", - }, - }}, - Upstreams: []pkg.UpstreamPackage{ - { - Name: "origin-foo", - }, +func Test_matchIgnoreFiltering(t *testing.T) { + // one commonly used filter uses APK NAK data to exclude false positives on language packages at the same locations + // based on packages in the APK DB. for example: + // APK package pkg1 is not vulnerable, but another python package is found with a slightly different name + // by the python cataloger when it scans the same files at the same locations that were associated with + // the APK package. the python package is checked by a separate matcher, so we surface ignore rules + // based on location and vuln id to exclude these false positives + ignoreByLocationAndVuln := func(locationToVulnIDs map[string][]string) []match.IgnoreFilter { + var out []match.IgnoreFilter + for path, vulnIDs := range locationToVulnIDs { + for _, vulnID := range vulnIDs { + out = append(out, match.IgnoreRule{ + Vulnerability: vulnID, + IncludeAliases: true, + Package: match.IgnoreRulePackage{ + Location: path, }, - }, - }, - vulns: []vulnerability.Vulnerability{ - { - Reference: vulnerability.Reference{ - ID: "GHSA-2014-fake-3", - Namespace: "wolfi:distro:wolfi:rolling", + }) + } + } + return out + } + + // with the addition of unaffected packages, ignore rules are returned by package language searches to account for + // searches for subsequent CPE searches based on the specific package + ignoreByPackageNameAndVuln := func(pkgNameToVulnIDs map[string][]string) []match.IgnoreFilter { + var out []match.IgnoreFilter + for packageName, vulnIDs := range pkgNameToVulnIDs { + for _, vulnID := range vulnIDs { + out = append(out, match.IgnoreRule{ + Vulnerability: vulnID, + IncludeAliases: true, + Package: match.IgnoreRulePackage{ + Name: packageName, }, - PackageName: "origin-foo", - Constraint: version.MustGetConstraint("< 0", version.ApkFormat), - }, - }, - expectedResult: map[string][]string{ - "/bin/foo-subpackage-binary": {"GHSA-2014-fake-3"}, - }, - errAssertion: assert.NoError, + }) + } + } + return out + } + + // construct matches here so we can make test cases more readable + + loc1 := "/usr/bin/pkg1" + loc2 := "/other/pkg1" + + vuln1 := "vuln1" + vuln2 := "vuln2" + + pkg1_vuln1_loc1 := match.Match{ + Package: pkg.Package{ + Type: syftPkg.PythonPkg, + Name: "pkg1", + Locations: file.NewLocationSet(file.NewLocation(loc1)), }, - { - name: "fixed vuln (not a false positive) in wolfi package", - pkgs: []pkg.Package{ - { - Name: "foo", - Distro: &distro.Distro{Type: distro.Wolfi}, - Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ - { - Path: "/bin/foo-binary", - }, - }}, - }, - }, - vulns: []vulnerability.Vulnerability{ - { - Reference: vulnerability.Reference{ - ID: "GHSA-2014-fake-3", - Namespace: "wolfi:distro:wolfi:rolling", - }, - PackageName: "foo", - Constraint: version.MustGetConstraint("< 1.2.3-r4", version.ApkFormat), - }, - }, - expectedResult: map[string][]string{}, - errAssertion: assert.NoError, + Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: vuln1}}, + } + pkg1_vuln2_loc1 := match.Match{ + Package: pkg.Package{ + Name: "pkg1", + Locations: file.NewLocationSet(file.NewLocation(loc1)), }, - { - name: "no vuln data for wolfi package", - pkgs: []pkg.Package{ - { - Name: "foo", - Distro: &distro.Distro{Type: distro.Wolfi}, - Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ - { - Path: "/bin/foo-binary", - }, - }}, - }, - }, - vulns: []vulnerability.Vulnerability{}, - expectedResult: map[string][]string{}, - errAssertion: assert.NoError, + Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: vuln2}}, + } + pkg1_vuln1_loc2 := match.Match{ + Package: pkg.Package{ + Name: "pkg1", + Locations: file.NewLocationSet(file.NewLocation(loc2)), }, - { - name: "no files listed for a wolfi package", - pkgs: []pkg.Package{ - { - Name: "foo", - Distro: &distro.Distro{Type: distro.Wolfi}, - Metadata: pkg.ApkMetadata{Files: nil}, - }, - }, - vulns: []vulnerability.Vulnerability{ - { - Reference: vulnerability.Reference{ - ID: "GHSA-2014-fake-3", - Namespace: "wolfi:distro:wolfi:rolling", - }, - PackageName: "foo", - Constraint: version.MustGetConstraint("< 0", version.ApkFormat), - }, - }, - expectedResult: map[string][]string{}, - errAssertion: assert.NoError, + Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: vuln1}}, + } + pkg2_vuln1_loc1 := match.Match{ + Package: pkg.Package{ + Type: syftPkg.PythonPkg, + Name: "pkg2", + Locations: file.NewLocationSet(file.NewLocation(loc1)), }, + Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: vuln1}}, } - - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - // create mock vulnerability provider - vp := mock.VulnerabilityProvider(tt.vulns...) - apkMatcher := &apk.Matcher{} - - var allMatches []match.Match - var allIgnores []match.IgnoreFilter - for _, p := range tt.pkgs { - matches, ignores, err := apkMatcher.Match(vp, p) - require.NoError(t, err) - allMatches = append(allMatches, matches...) - allIgnores = append(allIgnores, ignores...) - } - - actualResult := map[string][]string{} - for _, ignore := range allIgnores { - 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) - }) + pkg2_vuln2_loc1 := match.Match{ + Package: pkg.Package{ + Name: "pkg2", + Locations: file.NewLocationSet(file.NewLocation(loc1)), + }, + Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: vuln2}}, } -} -func Test_filterMatchesUsingDistroFalsePositives(t *testing.T) { cases := []struct { - name string - inputMatches []match.Match - fpIndex map[string][]string - expected []match.Match + name string + inputMatches []match.Match + ignoreFilters []match.IgnoreFilter + expected []match.Match }{ { name: "no input matches", inputMatches: nil, - fpIndex: map[string][]string{ - "/usr/bin/crane": {"CVE-2014-fake-3"}, - }, + ignoreFilters: ignoreByLocationAndVuln(map[string][]string{ + loc1: {vuln1}, + }), expected: nil, }, { - name: "happy path filtering", + name: "no ignore rules", inputMatches: []match.Match{ - { - Package: pkg.Package{ - Name: "crane", - Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")), - }, - Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}}, - }, + pkg1_vuln1_loc1, + pkg1_vuln1_loc2, }, - fpIndex: map[string][]string{ - "/usr/bin/crane": {"CVE-2014-fake-3"}, + ignoreFilters: nil, + expected: []match.Match{ + pkg1_vuln1_loc1, + pkg1_vuln1_loc2, }, - expected: nil, }, { - name: "location match but no vulns in FP index", + name: "happy path filtering", inputMatches: []match.Match{ - { - Package: pkg.Package{ - Name: "crane", - Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")), - }, - Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}}, - }, + pkg1_vuln1_loc1, }, - fpIndex: map[string][]string{ - "/usr/bin/crane": {}, + ignoreFilters: ignoreByLocationAndVuln(map[string][]string{ + loc1: {vuln1}, + }), + expected: nil, + }, + { + name: "location match different vuln", + inputMatches: []match.Match{ + pkg1_vuln1_loc1, }, + ignoreFilters: ignoreByLocationAndVuln(map[string][]string{ + loc1: {vuln2}, + }), expected: []match.Match{ - { - Package: pkg.Package{ - Name: "crane", - Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")), - }, - Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}}, - }, + pkg1_vuln1_loc1, }, }, { - name: "location match but matched vuln not in FP index", + name: "location across packages", inputMatches: []match.Match{ - { - Package: pkg.Package{ - Name: "crane", - Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")), - }, - Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}}, - }, + pkg1_vuln1_loc1, + pkg1_vuln2_loc1, + pkg2_vuln1_loc1, + pkg2_vuln2_loc1, + }, + ignoreFilters: ignoreByLocationAndVuln(map[string][]string{ + loc1: {vuln1}, + }), + expected: []match.Match{ + pkg1_vuln2_loc1, + pkg2_vuln2_loc1, }, - fpIndex: map[string][]string{ - "/usr/bin/crane": {"CVE-2016-fake-3"}, + }, + { + name: "package name", + inputMatches: []match.Match{ + pkg1_vuln1_loc1, + pkg1_vuln2_loc1, + pkg2_vuln1_loc1, }, + ignoreFilters: ignoreByPackageNameAndVuln(map[string][]string{ + pkg1_vuln1_loc1.Package.Name: {vuln1}, + }), expected: []match.Match{ - { - Package: pkg.Package{ - Name: "crane", - Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")), - }, - Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}}, - }, + pkg1_vuln2_loc1, + pkg2_vuln1_loc1, }, }, { - name: "empty FP index", + name: "not indexed rule", inputMatches: []match.Match{ - { - Package: pkg.Package{ - Name: "crane", - Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")), + pkg1_vuln1_loc1, + pkg1_vuln2_loc1, // not python package + pkg2_vuln1_loc1, + }, + ignoreFilters: []match.IgnoreFilter{ + match.IgnoreRule{ + Package: match.IgnoreRulePackage{ + Type: string(syftPkg.PythonPkg), // no indexed properties }, - Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}}, }, }, - fpIndex: map[string][]string{}, expected: []match.Match{ - { - Package: pkg.Package{ - Name: "crane", - Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")), - }, - Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}}, - }, + pkg1_vuln2_loc1, + }, + }, + { + name: "not indexed filter", + inputMatches: []match.Match{ + pkg1_vuln1_loc1, + pkg1_vuln1_loc2, + pkg2_vuln1_loc1, + pkg1_vuln2_loc1, + }, + ignoreFilters: []match.IgnoreFilter{ + testIgnoreFilter{func(m match.Match) bool { + return m.Vulnerability.ID == vuln1 + }}, + }, + expected: []match.Match{ + pkg1_vuln2_loc1, + }, + }, + { + name: "multiple rules mixed", + inputMatches: []match.Match{ + pkg1_vuln1_loc1, // removed by custom filter + pkg1_vuln1_loc2, + pkg2_vuln1_loc1, // removed by name + vuln + pkg1_vuln2_loc1, // removed by location + vuln + }, + ignoreFilters: append(append([]match.IgnoreFilter{ + testIgnoreFilter{func(m match.Match) bool { + return m.Vulnerability.ID == vuln1 + }}, + }, ignoreByLocationAndVuln(map[string][]string{ + loc2: {vuln2}, + })...), ignoreByPackageNameAndVuln(map[string][]string{ + pkg2_vuln1_loc1.Package.Name: {vuln1}, + })...), + expected: []match.Match{ + pkg1_vuln2_loc1, }, }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { - var allIgnores []match.IgnoreFilter - for path, cves := range tt.fpIndex { - for _, cve := range cves { - allIgnores = append(allIgnores, match.IgnoreRule{ - Vulnerability: cve, - IncludeAliases: true, - Package: match.IgnoreRulePackage{ - Location: path, - }, - }) - } - } - - filter := ignoredMatchFilter(allIgnores) + filter := ignoredMatchFilter(tt.ignoreFilters) actual, _ := match.ApplyIgnoreFilters(tt.inputMatches, filter) @@ -1356,7 +1296,7 @@ func Test_filterMatchesUsingDistroFalsePositives(t *testing.T) { } } -func Test_ignoredMatchFilter(t *testing.T) { +func Test_ignoredMatchFilterReasons(t *testing.T) { matches := []match.Match{ { Vulnerability: vulnerability.Vulnerability{