Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions grype/matcher/apk/matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
44 changes: 41 additions & 3 deletions grype/matcher/internal/language.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package internal

import (
"fmt"
"slices"

"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/matcher/internal/result"
Expand Down Expand Up @@ -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
}
104 changes: 103 additions & 1 deletion grype/matcher/internal/language_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -243,6 +244,7 @@ func TestFindMatchesByPackageGolang(t *testing.T) {
cases := []struct {
p pkg.Package
expMatches map[string]string
unaffected bool
}{
{
p: pkg.Package{
Expand Down Expand Up @@ -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
},
}

Expand All @@ -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)
})
}
}
Loading
Loading