diff --git a/docs/docs/configuration/filtering.md b/docs/docs/configuration/filtering.md index 72b2e8449d05..df6cc8726934 100644 --- a/docs/docs/configuration/filtering.md +++ b/docs/docs/configuration/filtering.md @@ -335,12 +335,13 @@ For the `.trivyignore.yaml` file, you can set ignored IDs separately for `vulner Available fields: -| Field | Required | Type | Description | -|------------|:--------:|---------------------|------------------------------------------------------------------------------------------------------------| -| id | ✓ | string | The identifier of the vulnerability, misconfiguration, secret, or license[^1]. | -| paths[^2] | | string array | The list of file paths to be ignored. If `paths` is not set, the ignore finding is applied to all files. | -| expired_at | | date (`yyyy-mm-dd`) | The expiration date of the ignore finding. If `expired_at` is not set, the ignore finding is always valid. | -| statement | | string | The reason for ignoring the finding. (This field is not used for filtering.) | +| Field | Required | Type | Description | +|------------|:--------:|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| id | ✓ | string | The identifier of the vulnerability, misconfiguration, secret, or license[^1]. | +| paths[^2] | | string array | The list of file paths to ignore. If `paths` is not set, the ignore finding is applied to all files. | +| purls | | string array | The list of PURLs to ignore packages. If `purls` is not set, the ignore finding is applied to all packages. This field is currently available only for vulnerabilities. | +| expired_at | | date (`yyyy-mm-dd`) | The expiration date of the ignore finding. If `expired_at` is not set, the ignore finding is always valid. | +| statement | | string | The reason for ignoring the finding. (This field is not used for filtering.) | ```bash $ cat .trivyignore.yaml @@ -352,6 +353,8 @@ vulnerabilities: - id: CVE-2023-2650 - id: CVE-2023-3446 - id: CVE-2023-3817 + purls: + - "pkg:deb/debian/libssl1.1" - id: CVE-2023-29491 expired_at: 2023-09-01 diff --git a/pkg/result/filter.go b/pkg/result/filter.go index 3feb82acc83e..dad9c0767316 100644 --- a/pkg/result/filter.go +++ b/pkg/result/filter.go @@ -113,7 +113,7 @@ func filterVulnerabilities(result *types.Result, severities []string, ignoreStat } // Filter by ignore file - if f := ignoreConfig.MatchVulnerability(vuln.VulnerabilityID, result.Target, vuln.PkgPath); f != nil { + if f := ignoreConfig.MatchVulnerability(vuln.VulnerabilityID, result.Target, vuln.PkgPath, vuln.PkgIdentifier.PURL); f != nil { result.ModifiedFindings = append(result.ModifiedFindings, types.NewModifiedFinding(vuln, types.FindingStatusIgnored, f.Statement, ignoreConfig.FilePath)) continue @@ -188,9 +188,9 @@ func filterSecrets(result *types.Result, severities []string, ignoreConfig Ignor func filterLicenses(result *types.Result, severities, ignoreLicenseNames []string, ignoreConfig IgnoreConfig) { // Merge ignore license names into ignored findings - var ignoreLicenses IgnoreFindings + var ignoreLicenses IgnoreConfig for _, licenseName := range ignoreLicenseNames { - ignoreLicenses = append(ignoreLicenses, IgnoreFinding{ + ignoreLicenses.Licenses = append(ignoreLicenses.Licenses, IgnoreFinding{ ID: licenseName, }) } @@ -203,7 +203,7 @@ func filterLicenses(result *types.Result, severities, ignoreLicenseNames []strin } // Filter by `--ignored-licenses` - if f := ignoreLicenses.Match(l.Name, l.FilePath); f != nil { + if f := ignoreLicenses.MatchLicense(l.Name, l.FilePath); f != nil { result.ModifiedFindings = append(result.ModifiedFindings, types.NewModifiedFinding(l, types.FindingStatusIgnored, "", "--ignored-licenses")) continue diff --git a/pkg/result/filter_test.go b/pkg/result/filter_test.go index 7a809447ad30..d98048c6af1b 100644 --- a/pkg/result/filter_test.go +++ b/pkg/result/filter_test.go @@ -84,6 +84,31 @@ func TestFilter(t *testing.T) { PkgName: "foo", InstalledVersion: "1.2.3", FixedVersion: "1.2.4", + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "foo", + Version: "1.2.3", + }, + }, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + } + vuln7 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2019-0007", + PkgName: "bar", + InstalledVersion: "2.3.4", + FixedVersion: "2.3.5", + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "bar", + Version: "2.3.4", + }, + }, Vulnerability: dbTypes.Vulnerability{ Severity: dbTypes.SeverityLow.String(), }, @@ -117,7 +142,7 @@ func TestFilter(t *testing.T) { } secret1 = types.DetectedSecret{ RuleID: "generic-wanted-rule", - Severity: dbTypes.SeverityLow.String(), + Severity: dbTypes.SeverityHigh.String(), Title: "Secret that should pass filter on rule id", StartLine: 1, EndLine: 2, @@ -174,30 +199,16 @@ func TestFilter(t *testing.T) { Results: []types.Result{ { Vulnerabilities: []types.DetectedVulnerability{ - vuln1, + vuln1, // filtered vuln2, }, Misconfigurations: []types.DetectedMisconfiguration{ misconf1, - misconf2, + misconf2, // filtered }, Secrets: []types.DetectedSecret{ - { - RuleID: "generic-critical-rule", - Severity: dbTypes.SeverityCritical.String(), - Title: "Critical Secret should pass filter", - StartLine: 1, - EndLine: 2, - Match: "*****", - }, - { - RuleID: "generic-low-rule", - Severity: dbTypes.SeverityLow.String(), - Title: "Low Secret should be ignored", - StartLine: 3, - EndLine: 4, - Match: "*****", - }, + secret1, + secret2, // filtered }, }, }, @@ -222,14 +233,7 @@ func TestFilter(t *testing.T) { misconf1, }, Secrets: []types.DetectedSecret{ - { - RuleID: "generic-critical-rule", - Severity: dbTypes.SeverityCritical.String(), - Title: "Critical Secret should pass filter", - StartLine: 1, - EndLine: 2, - Match: "*****", - }, + secret1, }, }, }, @@ -325,7 +329,7 @@ func TestFilter(t *testing.T) { Target: "deployment.yaml", Class: types.ClassConfig, Misconfigurations: []types.DetectedMisconfiguration{ - misconf1, // filtered by severity + misconf1, misconf2, misconf3, }, @@ -339,7 +343,10 @@ func TestFilter(t *testing.T) { }, }, }, - severities: []dbTypes.Severity{dbTypes.SeverityLow}, + severities: []dbTypes.Severity{ + dbTypes.SeverityLow, + dbTypes.SeverityHigh, + }, ignoreFile: "testdata/.trivyignore", }, want: types.Report{ @@ -377,9 +384,12 @@ func TestFilter(t *testing.T) { Class: types.ClassConfig, MisconfSummary: &types.MisconfSummary{ Successes: 1, - Failures: 0, + Failures: 1, Exceptions: 1, }, + Misconfigurations: []types.DetectedMisconfiguration{ + misconf1, + }, ModifiedFindings: []types.ModifiedFinding{ { Type: types.FindingTypeMisconfiguration, @@ -420,12 +430,13 @@ func TestFilter(t *testing.T) { vuln4, vuln5, // ignored vuln6, + vuln7, // filtered by PURL }, }, { Target: "app/Dockerfile", Misconfigurations: []types.DetectedMisconfiguration{ - misconf1, // filtered by severity + misconf1, // ignored misconf2, // ignored misconf3, }, @@ -448,7 +459,10 @@ func TestFilter(t *testing.T) { }, }, ignoreFile: "testdata/.trivyignore.yaml", - severities: []dbTypes.Severity{dbTypes.SeverityLow}, + severities: []dbTypes.Severity{ + dbTypes.SeverityLow, + dbTypes.SeverityHigh, + }, }, want: types.Report{ Results: types.Results{ @@ -477,6 +491,12 @@ func TestFilter(t *testing.T) { Source: "testdata/.trivyignore.yaml", Finding: vuln5, }, + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Finding: vuln7, + }, }, }, { @@ -484,12 +504,18 @@ func TestFilter(t *testing.T) { MisconfSummary: &types.MisconfSummary{ Successes: 0, Failures: 1, - Exceptions: 1, + Exceptions: 2, }, Misconfigurations: []types.DetectedMisconfiguration{ misconf3, }, ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeMisconfiguration, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Finding: misconf1, + }, { Type: types.FindingTypeMisconfiguration, Status: types.FindingStatusIgnored, diff --git a/pkg/result/ignore.go b/pkg/result/ignore.go index 70293fa41cc3..58e7e3c109a8 100644 --- a/pkg/result/ignore.go +++ b/pkg/result/ignore.go @@ -11,12 +11,13 @@ import ( "time" "github.com/bmatcuk/doublestar/v4" - "github.com/samber/lo" + "github.com/package-url/packageurl-go" "golang.org/x/xerrors" "gopkg.in/yaml.v3" "github.com/aquasecurity/trivy/pkg/clock" "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/purl" ) // IgnoreFinding represents an item to be ignored. @@ -26,11 +27,17 @@ type IgnoreFinding struct { // required: true ID string `yaml:"id"` - // Paths is the list of file paths to be ignored. + // Paths is the list of file paths to ignore. // If Paths is not set, the ignore finding is applied to all files. // required: false Paths []string `yaml:"paths"` + // PURLs is the list of packages to ignore. + // If PURLs is not set, the ignore finding is applied to packages. + // The field is currently available only for vulnerabilities. + // required: false + PURLs []*purl.PackageURL `yaml:"-"` // Filled in UnmarshalYAML + // ExpiredAt is the expiration date of the ignore finding. // If ExpiredAt is not set, the ignore finding is always valid. // required: false @@ -41,17 +48,50 @@ type IgnoreFinding struct { Statement string `yaml:"statement"` } +// UnmarshalYAML is a custom unmarshaler for IgnoreFinding that handles +// the conversion of PURLs from strings to purl.PackageURL objects. +func (i *IgnoreFinding) UnmarshalYAML(value *yaml.Node) error { + // Define a shadow type to prevent infinite recursion + type plain IgnoreFinding + var tmp struct { + plain `yaml:",inline"` + PURLs []string `yaml:"purls"` + } + if err := value.Decode(&tmp); err != nil { + return err + } + + *i = IgnoreFinding(tmp.plain) + + for _, pattern := range i.Paths { + if !doublestar.ValidatePattern(pattern) { + return xerrors.Errorf("invalid path pattern in the ignore file, id: %s, path: %s", i.ID, pattern) + } + } + + // Convert string PURLs to purl.PackageURL objects + for _, purlStr := range tmp.PURLs { + parsedPURL, err := purl.FromString(purlStr) + if err != nil { + return xerrors.Errorf("purl error in the ignore file: %w", err) + } + i.PURLs = append(i.PURLs, parsedPURL) + } + + return nil +} + type IgnoreFindings []IgnoreFinding -func (f *IgnoreFindings) Match(id, path string) *IgnoreFinding { +func (f *IgnoreFindings) Match(id, path string, pkg *packageurl.PackageURL) *IgnoreFinding { for _, finding := range *f { if id != finding.ID { continue } - - if !pathMatch(path, finding.Paths) { + if !matchPath(path, finding.Paths) || !matchPURL(pkg, finding.PURLs) { continue } + log.Logger.Debugw("Ignored", log.String("id", id), log.String("path", path)) return &finding @@ -59,7 +99,7 @@ func (f *IgnoreFindings) Match(id, path string) *IgnoreFinding { return nil } -func pathMatch(path string, patterns []string) bool { +func matchPath(path string, patterns []string) bool { if len(patterns) == 0 { return true } @@ -73,22 +113,26 @@ func pathMatch(path string, patterns []string) bool { return false } -func (f *IgnoreFindings) Filter(ctx context.Context) { +func matchPURL(target *packageurl.PackageURL, purls []*purl.PackageURL) bool { + if target == nil || len(purls) == 0 { + return true + } + + for _, p := range purls { + if p.Match(target) { + return true + } + } + return false +} + +func (f *IgnoreFindings) Prune(ctx context.Context) { var findings IgnoreFindings for _, finding := range *f { // Filter out expired ignore findings if !finding.ExpiredAt.IsZero() && finding.ExpiredAt.Before(clock.Now(ctx)) { continue } - - // Filter out invalid path patterns - finding.Paths = lo.Filter(finding.Paths, func(pattern string, _ int) bool { - if !doublestar.ValidatePattern(pattern) { - log.Logger.Errorf("Invalid path pattern in the ignore file: %q", pattern) - return false - } - return true - }) findings = append(findings, finding) } *f = findings @@ -103,13 +147,13 @@ type IgnoreConfig struct { Licenses IgnoreFindings `yaml:"licenses"` } -func (c *IgnoreConfig) MatchVulnerability(vulnID, filePath, pkgPath string) *IgnoreFinding { +func (c *IgnoreConfig) MatchVulnerability(vulnID, filePath, pkgPath string, pkg *packageurl.PackageURL) *IgnoreFinding { paths := []string{ filePath, pkgPath, } for _, p := range paths { - if f := c.Vulnerabilities.Match(vulnID, p); f != nil { + if f := c.Vulnerabilities.Match(vulnID, p, pkg); f != nil { return f } } @@ -122,7 +166,7 @@ func (c *IgnoreConfig) MatchMisconfiguration(misconfID, avdID, filePath string) avdID, } for _, id := range ids { - if f := c.Misconfigurations.Match(id, filePath); f != nil { + if f := c.Misconfigurations.Match(id, filePath, nil); f != nil { return f } } @@ -130,11 +174,11 @@ func (c *IgnoreConfig) MatchMisconfiguration(misconfID, avdID, filePath string) } func (c *IgnoreConfig) MatchSecret(secretID, filePath string) *IgnoreFinding { - return c.Secrets.Match(secretID, filePath) + return c.Secrets.Match(secretID, filePath, nil) } func (c *IgnoreConfig) MatchLicense(licenseID, filePath string) *IgnoreFinding { - return c.Licenses.Match(licenseID, filePath) + return c.Licenses.Match(licenseID, filePath, nil) } func parseIgnoreFile(ctx context.Context, ignoreFile string) (IgnoreConfig, error) { @@ -163,10 +207,10 @@ func parseIgnoreFile(ctx context.Context, ignoreFile string) (IgnoreConfig, erro } } - conf.Vulnerabilities.Filter(ctx) - conf.Misconfigurations.Filter(ctx) - conf.Secrets.Filter(ctx) - conf.Licenses.Filter(ctx) + conf.Vulnerabilities.Prune(ctx) + conf.Misconfigurations.Prune(ctx) + conf.Secrets.Prune(ctx) + conf.Licenses.Prune(ctx) conf.FilePath = filepath.ToSlash(filepath.Clean(ignoreFile)) return conf, nil diff --git a/pkg/result/testdata/.trivyignore.yaml b/pkg/result/testdata/.trivyignore.yaml index 5de48eb64a21..9690cb9944cd 100644 --- a/pkg/result/testdata/.trivyignore.yaml +++ b/pkg/result/testdata/.trivyignore.yaml @@ -13,6 +13,10 @@ vulnerabilities: expired_at: 2023-01-01 - id: CVE-2019-0006 expired_at: 2020-01-01 + - id: CVE-2019-0007 + purls: + - "pkg:golang/github.com/aquasecurity/aaa@9.9.9" + - "pkg:golang/github.com/aquasecurity/bar" # Match misconfigurations: - id: ID100