fix: adds ignore rules for kernel-headers indirect matches (#1787)

* fix: adds ignore rules for kernel-headers indirect matches

Adds ignoring of kernel-headers indirect matches on kernel vulns
since the kernel-headers package does not have the kernel code in it
that kernel vulns are actually referring to.

Adds a config value to control this ignore behavior that defaults to
enabling the ignore rules.

Fixes: 1762

* Adds ignore rule support for match types and upstream package names.
* Adds default ignore rules for kernel-headers indirect matches on kernel
for rpms.

Signed-off-by: Zach Hill <zach@anchore.com>

* chore: add match-upstream-kernel-headers config to README.md

Signed-off-by: Zach Hill <zach@anchore.com>

* chore: update match labels

Signed-off-by: Keith Zantow <kzantow@gmail.com>

---------

Signed-off-by: Zach Hill <zach@anchore.com>
Signed-off-by: Keith Zantow <kzantow@gmail.com>
Co-authored-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
Zach Hill 2024-04-15 13:29:19 -07:00 committed by GitHub
parent 018b415abd
commit a7cbe3a26c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 378 additions and 44 deletions

View file

@ -703,6 +703,10 @@ file: ""
# same as --exclude ; GRYPE_EXCLUDE env var
exclude: []
# include matches on kernel-headers packages that are matched against upstream kernel package
# if 'false' any such matches are marked as ignored
match-upstream-kernel-headers: false
# os and/or architecture to use when referencing container images (e.g. "windows/armv6" or "arm64")
# same as --platform; GRYPE_PLATFORM env var
platform: ""

View file

@ -99,6 +99,10 @@ var ignoreVEXFixedNotAffected = []match.IgnoreRule{
{VexStatus: string(vex.StatusFixed)},
}
var ignoreLinuxKernelHeaders = []match.IgnoreRule{
{Package: match.IgnoreRulePackage{Name: "kernel-headers", UpstreamName: "kernel", Type: "rpm"}, MatchType: match.ExactIndirectMatch},
}
//nolint:funlen
func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs error) {
writer, err := format.MakeScanResultWriter(opts.Outputs, opts.File, format.PresentationConfig{
@ -124,6 +128,10 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs
opts.Ignore = append(opts.Ignore, ignoreFixedMatches...)
}
if !opts.MatchUpstreamKernelHeaders {
opts.Ignore = append(opts.Ignore, ignoreLinuxKernelHeaders...)
}
for _, ignoreState := range stringutil.SplitCommaSeparatedString(opts.IgnoreStates) {
switch grypeDb.FixState(ignoreState) {
case grypeDb.UnknownFixState, grypeDb.FixedState, grypeDb.NotFixedState, grypeDb.WontFixState:

View file

@ -11,30 +11,31 @@ import (
)
type Grype struct {
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, <presenter>=<file> the Presenter hint string to use for report formatting and the output file
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
Distro string `yaml:"distro" json:"distro" mapstructure:"distro"` // --distro, specify a distro to explicitly use
GenerateMissingCPEs bool `yaml:"add-cpes-if-none" json:"add-cpes-if-none" mapstructure:"add-cpes-if-none"` // --add-cpes-if-none, automatically generate CPEs if they are not present in import (e.g. from a 3rd party SPDX document)
OutputTemplateFile string `yaml:"output-template-file" json:"output-template-file" mapstructure:"output-template-file"` // -t, the template file to use for formatting the final report
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
OnlyFixed bool `yaml:"only-fixed" json:"only-fixed" mapstructure:"only-fixed"` // only fail if detected vulns have a fix
OnlyNotFixed bool `yaml:"only-notfixed" json:"only-notfixed" mapstructure:"only-notfixed"` // only fail if detected vulns don't have a fix
IgnoreStates string `yaml:"ignore-states" json:"ignore-wontfix" mapstructure:"ignore-wontfix"` // ignore detections for vulnerabilities matching these comma-separated fix states
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` // --platform, override the target platform for a container image
Search search `yaml:"search" json:"search" mapstructure:"search"`
Ignore []match.IgnoreRule `yaml:"ignore" json:"ignore" mapstructure:"ignore"`
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
DB Database `yaml:"db" json:"db" mapstructure:"db"`
ExternalSources externalSources `yaml:"external-sources" json:"externalSources" mapstructure:"external-sources"`
Match matchConfig `yaml:"match" json:"match" mapstructure:"match"`
FailOn string `yaml:"fail-on-severity" json:"fail-on-severity" mapstructure:"fail-on-severity"`
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
ShowSuppressed bool `yaml:"show-suppressed" json:"show-suppressed" mapstructure:"show-suppressed"`
ByCVE bool `yaml:"by-cve" json:"by-cve" mapstructure:"by-cve"` // --by-cve, indicates if the original match vulnerability IDs should be preserved or the CVE should be used instead
Name string `yaml:"name" json:"name" mapstructure:"name"`
DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"`
VexDocuments []string `yaml:"vex-documents" json:"vex-documents" mapstructure:"vex-documents"`
VexAdd []string `yaml:"vex-add" json:"vex-add" mapstructure:"vex-add"` // GRYPE_VEX_ADD
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, <presenter>=<file> the Presenter hint string to use for report formatting and the output file
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
Distro string `yaml:"distro" json:"distro" mapstructure:"distro"` // --distro, specify a distro to explicitly use
GenerateMissingCPEs bool `yaml:"add-cpes-if-none" json:"add-cpes-if-none" mapstructure:"add-cpes-if-none"` // --add-cpes-if-none, automatically generate CPEs if they are not present in import (e.g. from a 3rd party SPDX document)
OutputTemplateFile string `yaml:"output-template-file" json:"output-template-file" mapstructure:"output-template-file"` // -t, the template file to use for formatting the final report
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
OnlyFixed bool `yaml:"only-fixed" json:"only-fixed" mapstructure:"only-fixed"` // only fail if detected vulns have a fix
OnlyNotFixed bool `yaml:"only-notfixed" json:"only-notfixed" mapstructure:"only-notfixed"` // only fail if detected vulns don't have a fix
IgnoreStates string `yaml:"ignore-states" json:"ignore-wontfix" mapstructure:"ignore-wontfix"` // ignore detections for vulnerabilities matching these comma-separated fix states
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` // --platform, override the target platform for a container image
Search search `yaml:"search" json:"search" mapstructure:"search"`
Ignore []match.IgnoreRule `yaml:"ignore" json:"ignore" mapstructure:"ignore"`
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
DB Database `yaml:"db" json:"db" mapstructure:"db"`
ExternalSources externalSources `yaml:"external-sources" json:"externalSources" mapstructure:"external-sources"`
Match matchConfig `yaml:"match" json:"match" mapstructure:"match"`
FailOn string `yaml:"fail-on-severity" json:"fail-on-severity" mapstructure:"fail-on-severity"`
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
ShowSuppressed bool `yaml:"show-suppressed" json:"show-suppressed" mapstructure:"show-suppressed"`
ByCVE bool `yaml:"by-cve" json:"by-cve" mapstructure:"by-cve"` // --by-cve, indicates if the original match vulnerability IDs should be preserved or the CVE should be used instead
Name string `yaml:"name" json:"name" mapstructure:"name"`
DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"`
VexDocuments []string `yaml:"vex-documents" json:"vex-documents" mapstructure:"vex-documents"`
VexAdd []string `yaml:"vex-add" json:"vex-add" mapstructure:"vex-add"` // GRYPE_VEX_ADD
MatchUpstreamKernelHeaders bool `yaml:"match-upstream-kernel-headers" json:"match-upstream-kernel-headers" mapstructure:"match-upstream-kernel-headers"` // Show matches on kernel-headers packages where the match is on kernel upstream instead of marking them as ignored, default=false
}
var _ interface {
@ -44,12 +45,13 @@ var _ interface {
func DefaultGrype(id clio.Identification) *Grype {
return &Grype{
Search: defaultSearch(source.SquashedScope),
DB: DefaultDatabase(id),
Match: defaultMatchConfig(),
ExternalSources: defaultExternalSources(),
CheckForAppUpdate: true,
VexAdd: []string{},
Search: defaultSearch(source.SquashedScope),
DB: DefaultDatabase(id),
Match: defaultMatchConfig(),
ExternalSources: defaultExternalSources(),
CheckForAppUpdate: true,
VexAdd: []string{},
MatchUpstreamKernelHeaders: false,
}
}

View file

@ -24,15 +24,17 @@ type IgnoreRule struct {
Package IgnoreRulePackage `yaml:"package" json:"package" mapstructure:"package"`
VexStatus string `yaml:"vex-status" json:"vex-status" mapstructure:"vex-status"`
VexJustification string `yaml:"vex-justification" json:"vex-justification" mapstructure:"vex-justification"`
MatchType Type `yaml:"match-type" json:"match-type" mapstructure:"match-type"`
}
// IgnoreRulePackage describes the Package-specific fields that comprise the IgnoreRule.
type IgnoreRulePackage struct {
Name string `yaml:"name" json:"name" mapstructure:"name"`
Version string `yaml:"version" json:"version" mapstructure:"version"`
Language string `yaml:"language" json:"language" mapstructure:"language"`
Type string `yaml:"type" json:"type" mapstructure:"type"`
Location string `yaml:"location" json:"location" mapstructure:"location"`
Name string `yaml:"name" json:"name" mapstructure:"name"`
Version string `yaml:"version" json:"version" mapstructure:"version"`
Language string `yaml:"language" json:"language" mapstructure:"language"`
Type string `yaml:"type" json:"type" mapstructure:"type"`
Location string `yaml:"location" json:"location" mapstructure:"location"`
UpstreamName string `yaml:"upstream-name" json:"upstream-name" mapstructure:"upstream-name"`
}
// ApplyIgnoreRules iterates through the provided matches and, for each match,
@ -137,6 +139,13 @@ func getIgnoreConditionsForRule(rule IgnoreRule) []ignoreCondition {
ignoreConditions = append(ignoreConditions, ifFixStateApplies(fs))
}
if upstreamName := rule.Package.UpstreamName; upstreamName != "" {
ignoreConditions = append(ignoreConditions, ifUpstreamPackageNameApplies(upstreamName))
}
if matchType := rule.MatchType; matchType != "" {
ignoreConditions = append(ignoreConditions, ifMatchTypeApplies(matchType))
}
return ignoreConditions
}
@ -188,6 +197,28 @@ func ifPackageLocationApplies(location string) ignoreCondition {
}
}
func ifUpstreamPackageNameApplies(name string) ignoreCondition {
return func(match Match) bool {
for _, upstream := range match.Package.Upstreams {
if name == upstream.Name {
return true
}
}
return false
}
}
func ifMatchTypeApplies(matchType Type) ignoreCondition {
return func(match Match) bool {
for _, mType := range match.Details.Types() {
if mType == matchType {
return true
}
}
return false
}
}
func ruleLocationAppliesToMatch(location string, match Match) bool {
for _, packageLocation := range match.Package.Locations.ToSlice() {
if ruleLocationAppliesToPath(location, packageLocation.RealPath) {

View file

@ -86,6 +86,163 @@ var (
},
},
}
// For testing the match-type rules
matchTypesMatches = []Match{
// Direct match, not like a normal kernel header match
{
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-1",
Namespace: "fake-redhat-vulns",
Fix: vulnerability.Fix{
State: grypeDb.UnknownFixState,
},
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
Name: "kernel-headers1",
Version: "5.1.0",
Type: syftPkg.RpmPkg,
Upstreams: []pkg.UpstreamPackage{
{Name: "kernel2"},
},
},
Details: []Detail{
{
Type: ExactDirectMatch,
},
},
},
{
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2",
Namespace: "fake-deb-vulns",
Fix: vulnerability.Fix{
State: grypeDb.UnknownFixState,
},
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
Name: "kernel-headers2",
Version: "5.1.0",
Type: syftPkg.DebPkg,
Upstreams: []pkg.UpstreamPackage{
{Name: "kernel2"},
},
},
Details: []Detail{
{
Type: ExactIndirectMatch,
},
},
},
{
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-1",
Namespace: "npm-vulns",
Fix: vulnerability.Fix{
State: grypeDb.UnknownFixState,
},
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
Name: "npm1",
Version: "5.1.0",
Type: syftPkg.NpmPkg,
},
Details: []Detail{
{
Type: CPEMatch,
},
},
},
}
// For testing the match-type and upstream ignore rules
kernelHeadersMatches = []Match{
// RPM-like match similar to what we see from RedHat
{
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2",
Namespace: "fake-redhat-vulns",
Fix: vulnerability.Fix{
State: grypeDb.UnknownFixState,
},
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
Name: "kernel-headers",
Version: "5.1.0",
Type: syftPkg.RpmPkg,
Upstreams: []pkg.UpstreamPackage{
{Name: "kernel"},
},
},
Details: []Detail{
{
Type: ExactIndirectMatch,
},
},
},
// debian-like match, showing the kernel header package name w/embedded version
{
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2",
Namespace: "fake-debian-vulns",
Fix: vulnerability.Fix{
State: grypeDb.UnknownFixState,
},
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
Name: "linux-headers-5.2.0",
Version: "5.2.1",
Type: syftPkg.DebPkg,
Upstreams: []pkg.UpstreamPackage{
{Name: "linux"},
},
},
Details: []Detail{
{
Type: ExactIndirectMatch,
},
},
},
}
// For testing the match-type and upstream ignore rules
packageTypeMatches = []Match{
{
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2",
Namespace: "fake-redhat-vulns",
Fix: vulnerability.Fix{
State: grypeDb.UnknownFixState,
},
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
Name: "kernel-headers",
Version: "5.1.0",
Type: syftPkg.RpmPkg,
},
},
{
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2",
Namespace: "fake-debian-vulns",
Fix: vulnerability.Fix{
State: grypeDb.UnknownFixState,
},
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
Name: "linux-headers-5.2.0",
Version: "5.2.1",
Type: syftPkg.DebPkg,
},
},
}
)
func TestApplyIgnoreRules(t *testing.T) {
@ -309,6 +466,134 @@ func TestApplyIgnoreRules(t *testing.T) {
},
},
},
{
name: "ignore matches on indirect match-type",
allMatches: matchTypesMatches,
ignoreRules: []IgnoreRule{
{
MatchType: ExactIndirectMatch,
},
},
expectedRemainingMatches: []Match{
matchTypesMatches[0], matchTypesMatches[2],
},
expectedIgnoredMatches: []IgnoredMatch{
{
Match: matchTypesMatches[1],
AppliedIgnoreRules: []IgnoreRule{
{
MatchType: ExactIndirectMatch,
},
},
},
},
},
{
name: "ignore matches on cpe match-type",
allMatches: matchTypesMatches,
ignoreRules: []IgnoreRule{
{
MatchType: CPEMatch,
},
},
expectedRemainingMatches: []Match{
matchTypesMatches[0], matchTypesMatches[1],
},
expectedIgnoredMatches: []IgnoredMatch{
{
Match: matchTypesMatches[2],
AppliedIgnoreRules: []IgnoreRule{
{
MatchType: CPEMatch,
},
},
},
},
},
{
name: "ignore matches on upstream name",
allMatches: kernelHeadersMatches,
ignoreRules: []IgnoreRule{
{
Package: IgnoreRulePackage{
UpstreamName: "kernel",
},
},
},
expectedRemainingMatches: []Match{
kernelHeadersMatches[1],
},
expectedIgnoredMatches: []IgnoredMatch{
{
Match: kernelHeadersMatches[0],
AppliedIgnoreRules: []IgnoreRule{
{
Package: IgnoreRulePackage{
UpstreamName: "kernel",
},
},
},
},
},
},
{
name: "ignore matches on package type",
allMatches: packageTypeMatches,
ignoreRules: []IgnoreRule{
{
Package: IgnoreRulePackage{
Type: string(syftPkg.RpmPkg),
},
},
},
expectedRemainingMatches: []Match{
packageTypeMatches[1],
},
expectedIgnoredMatches: []IgnoredMatch{
{
Match: packageTypeMatches[0],
AppliedIgnoreRules: []IgnoreRule{
{
Package: IgnoreRulePackage{
Type: string(syftPkg.RpmPkg),
},
},
},
},
},
},
{
name: "ignore matches rpms for kernel-headers with kernel upstream",
allMatches: kernelHeadersMatches,
ignoreRules: []IgnoreRule{
{
Package: IgnoreRulePackage{
Name: "kernel-headers",
UpstreamName: "kernel",
Type: string(syftPkg.RpmPkg),
},
MatchType: ExactIndirectMatch,
},
},
expectedRemainingMatches: []Match{
kernelHeadersMatches[1],
},
expectedIgnoredMatches: []IgnoredMatch{
{
Match: kernelHeadersMatches[0],
AppliedIgnoreRules: []IgnoreRule{
{
Package: IgnoreRulePackage{
Name: "kernel-headers",
UpstreamName: "kernel",
Type: string(syftPkg.RpmPkg),
},
MatchType: ExactIndirectMatch,
},
},
},
},
},
}
for _, testCase := range cases {

View file

@ -14,13 +14,15 @@ type IgnoreRule struct {
Package *IgnoreRulePackage `json:"package,omitempty"`
VexStatus string `json:"vex-status,omitempty"`
VexJustification string `json:"vex-justification,omitempty"`
MatchType string `json:"match-type,omitempty"`
}
type IgnoreRulePackage struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Type string `json:"type,omitempty"`
Location string `json:"location,omitempty"`
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Type string `json:"type,omitempty"`
Location string `json:"location,omitempty"`
UpstreamName string `json:"upstream-name,omitempty"`
}
func newIgnoreRule(r match.IgnoreRule) IgnoreRule {
@ -29,10 +31,11 @@ func newIgnoreRule(r match.IgnoreRule) IgnoreRule {
// We'll only set the package part of the rule not to `nil` if there are any values to fill out.
if p := r.Package; p.Name != "" || p.Version != "" || p.Type != "" || p.Location != "" {
ignoreRulePackage = &IgnoreRulePackage{
Name: r.Package.Name,
Version: r.Package.Version,
Type: r.Package.Type,
Location: r.Package.Location,
Name: r.Package.Name,
Version: r.Package.Version,
Type: r.Package.Type,
Location: r.Package.Location,
UpstreamName: r.Package.UpstreamName,
}
}
@ -43,6 +46,7 @@ func newIgnoreRule(r match.IgnoreRule) IgnoreRule {
Package: ignoreRulePackage,
VexStatus: r.VexStatus,
VexJustification: r.VexJustification,
MatchType: string(r.MatchType),
}
}

@ -1 +1 @@
Subproject commit a535b74f1228c0919ee0008ee6fccbb99e376b3a
Subproject commit a8721b180fcea164460ae60d852ea5607a56005b