feat: allow ignoring findings by type in Rego (#9578)

Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
This commit is contained in:
Nikita Pivkin
2025-10-07 00:17:23 +06:00
committed by GitHub
parent 4bef183489
commit c638fc646c
9 changed files with 163 additions and 71 deletions

View File

@@ -480,6 +480,19 @@ ignore {
trivy image --ignore-policy examples/ignore-policies/basic.rego centos:7
```
To filter findings of a specific type based on a field that may exist in multiple structures (for example, `PkgName` in both `DetectedVulnerability` and `DetectedLicense`), you can use the `Type` field. This field is automatically added when exporting findings to Rego and indicates the kind of finding. Possible values are: `vulnerability`, `misconfiguration`, `secret`, and `license`.
For example, the following policy ignores vulnerabilities with a specific package name without affecting other finding types:
```rego
package trivy
ignore {
input.Type == "vulnerability"
input.PkgName == "foo"
}
```
For more advanced use cases, there is a built-in Rego library with helper functions that you can import into your policy using: `import data.lib.trivy`.
More info about the helper functions are in the library [here](https://github.com/aquasecurity/trivy/tree/{{ git.tag }}/pkg/result/module.go).

View File

@@ -71,7 +71,8 @@ func FilterResult(ctx context.Context, result *types.Result, ignoreConf IgnoreCo
filterLicenses(result, severities, opt.IgnoreLicenses, ignoreConf)
if opt.PolicyFile != "" {
if err := applyPolicy(ctx, result, opt.PolicyFile); err != nil {
policyFile := filepath.ToSlash(filepath.Clean(opt.PolicyFile))
if err := applyPolicy(ctx, result, policyFile); err != nil {
return xerrors.Errorf("failed to apply the policy: %w", err)
}
}
@@ -137,7 +138,7 @@ func filterMisconfigurations(result *types.Result, severities []string, includeN
}
// Count successes and failures
summarize(misconf.Status, result.MisconfSummary)
updateMisconfSummary(misconf.Status, result.MisconfSummary)
if misconf.Status != types.MisconfStatusFailure && !includeNonFailures {
continue
@@ -204,7 +205,7 @@ func filterLicenses(result *types.Result, severities, ignoreLicenseNames []strin
result.Licenses = filtered
}
func summarize(status types.MisconfStatus, summary *types.MisconfSummary) {
func updateMisconfSummary(status types.MisconfStatus, summary *types.MisconfSummary) {
switch status {
case types.MisconfStatusFailure:
summary.Failures++
@@ -229,83 +230,87 @@ func applyPolicy(ctx context.Context, result *types.Result, policyFile string) e
return xerrors.Errorf("unable to prepare for eval: %w", err)
}
policyFile = filepath.ToSlash(filepath.Clean(policyFile))
// Vulnerabilities
var filteredVulns []types.DetectedVulnerability
for _, vuln := range result.Vulnerabilities {
ignored, err := evaluate(ctx, query, vuln)
if err != nil {
return err
}
if ignored {
result.ModifiedFindings = append(result.ModifiedFindings,
types.NewModifiedFinding(vuln, types.FindingStatusIgnored, "Filtered by Rego", policyFile))
continue
}
filteredVulns = append(filteredVulns, vuln)
filteredVulns, modifiedVulns, err := filterFindingsByRego(ctx, query, result.Vulnerabilities, policyFile)
if err != nil {
return err
}
result.Vulnerabilities = filteredVulns
result.ModifiedFindings = append(result.ModifiedFindings, modifiedVulns...)
// Misconfigurations
var filteredMisconfs []types.DetectedMisconfiguration
for _, misconf := range result.Misconfigurations {
ignored, err := evaluate(ctx, query, misconf)
if err != nil {
return err
}
if ignored {
switch misconf.Status {
case types.MisconfStatusFailure:
result.MisconfSummary.Failures--
case types.MisconfStatusPassed:
result.MisconfSummary.Successes--
}
result.ModifiedFindings = append(result.ModifiedFindings,
types.NewModifiedFinding(misconf, types.FindingStatusIgnored, "Filtered by Rego", policyFile))
filteredMisconfs, modifiedMisconfs, err := filterFindingsByRego(ctx, query, result.Misconfigurations, policyFile)
if err != nil {
return err
}
for _, m := range modifiedMisconfs {
misconf, ok := m.Finding.(types.DetectedMisconfiguration)
if !ok {
continue
}
filteredMisconfs = append(filteredMisconfs, misconf)
switch misconf.Status {
case types.MisconfStatusFailure:
result.MisconfSummary.Failures--
case types.MisconfStatusPassed:
result.MisconfSummary.Successes--
}
}
result.Misconfigurations = filteredMisconfs
result.ModifiedFindings = append(result.ModifiedFindings, modifiedMisconfs...)
// Secrets
var filteredSecrets []types.DetectedSecret
for _, scrt := range result.Secrets {
ignored, err := evaluate(ctx, query, scrt)
if err != nil {
return err
}
if ignored {
result.ModifiedFindings = append(result.ModifiedFindings,
types.NewModifiedFinding(scrt, types.FindingStatusIgnored, "Filtered by Rego", policyFile))
continue
}
filteredSecrets = append(filteredSecrets, scrt)
filteredSecrets, modifiedSecrets, err := filterFindingsByRego(ctx, query, result.Secrets, policyFile)
if err != nil {
return err
}
result.Secrets = filteredSecrets
result.ModifiedFindings = append(result.ModifiedFindings, modifiedSecrets...)
// Licenses
var filteredLicenses []types.DetectedLicense
for _, lic := range result.Licenses {
ignored, err := evaluate(ctx, query, lic)
if err != nil {
return err
}
if ignored {
result.ModifiedFindings = append(result.ModifiedFindings,
types.NewModifiedFinding(lic, types.FindingStatusIgnored, "Filtered by Rego", policyFile))
continue
}
filteredLicenses = append(filteredLicenses, lic)
filteredLicenses, modifiedLicenses, err := filterFindingsByRego(ctx, query, result.Licenses, policyFile)
if err != nil {
return err
}
result.Licenses = filteredLicenses
result.ModifiedFindings = append(result.ModifiedFindings, modifiedLicenses...)
return nil
}
func evaluate(ctx context.Context, query rego.PreparedEvalQuery, input any) (bool, error) {
results, err := query.Eval(ctx, rego.EvalInput(input))
func filterFindingsByRego[T types.Finding](
ctx context.Context, query rego.PreparedEvalQuery, findings []T, policyFile string,
) ([]T, []types.ModifiedFinding, error) {
var filtered []T
var modified []types.ModifiedFinding
for _, finding := range findings {
ignored, err := evaluate(ctx, query, finding)
if err != nil {
return nil, nil, err
}
if ignored {
modified = append(modified,
types.NewModifiedFinding(finding, types.FindingStatusIgnored, "Filtered by Rego", policyFile))
continue
}
filtered = append(filtered, finding)
}
return filtered, modified, nil
}
func evaluate[T types.Finding](ctx context.Context, query rego.PreparedEvalQuery, finding T) (bool, error) {
type regoInput struct {
Data T `json:",inline"`
Type string `json:"Type"`
}
ri := regoInput{
Data: finding,
Type: string(finding.FindingType()),
}
results, err := query.Eval(ctx, rego.EvalInput(ri))
if err != nil {
return false, xerrors.Errorf("unable to evaluate the policy: %w", err)
} else if len(results) == 0 {

View File

@@ -173,12 +173,14 @@ func TestFilter(t *testing.T) {
license1 = types.DetectedLicense{
Name: "GPL-3.0",
Severity: dbTypes.SeverityLow.String(),
PkgName: "foo",
FilePath: "usr/share/gcc/python/libstdcxx/v6/__init__.py",
Category: "restricted",
Confidence: 1,
}
license2 = types.DetectedLicense{
Name: "GPL-3.0",
PkgName: "bar",
Severity: dbTypes.SeverityLow.String(),
FilePath: "usr/share/gcc/python/libstdcxx/v6/printers.py",
Category: "restricted",
@@ -620,7 +622,7 @@ func TestFilter(t *testing.T) {
{
Misconfigurations: []types.DetectedMisconfiguration{
misconf1,
misconf2,
misconf2, // passed
misconf3, // ignored by check
},
},
@@ -996,6 +998,67 @@ func TestFilter(t *testing.T) {
},
},
},
{
name: "ignore findings by type in policy file",
args: args{
report: types.Report{
Results: types.Results{
{
Target: "foo/package-lock.json",
Vulnerabilities: []types.DetectedVulnerability{
vuln1,
vuln7, // filtered by PkgName and Type
},
},
{
Target: "LICENSE.txt",
Licenses: []types.DetectedLicense{
license1, // filtered by PkgName and Type
license2,
},
},
},
},
policyFile: "testdata/ignore-by-type.rego",
severities: []dbTypes.Severity{
dbTypes.SeverityLow,
},
},
want: types.Report{
Results: types.Results{
{
Target: "foo/package-lock.json",
Vulnerabilities: []types.DetectedVulnerability{
vuln1,
},
ModifiedFindings: []types.ModifiedFinding{
{
Type: types.FindingTypeVulnerability,
Status: types.FindingStatusIgnored,
Source: "testdata/ignore-by-type.rego",
Statement: "Filtered by Rego",
Finding: vuln7,
},
},
},
{
Target: "LICENSE.txt",
Licenses: []types.DetectedLicense{
license2,
},
ModifiedFindings: []types.ModifiedFinding{
{
Type: types.FindingTypeLicense,
Status: types.FindingStatusIgnored,
Source: "testdata/ignore-by-type.rego",
Statement: "Filtered by Rego",
Finding: license1,
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

11
pkg/result/testdata/ignore-by-type.rego vendored Normal file
View File

@@ -0,0 +1,11 @@
package trivy
ignore {
input.Type == "license"
input.PkgName == "foo"
}
ignore {
input.Type == "vulnerability"
input.PkgName == "bar"
}

View File

@@ -25,8 +25,8 @@ const (
// Finding represents one of the findings that Trivy can detect,
// such as vulnerabilities, misconfigurations, secrets, and licenses.
type finding interface {
findingType() FindingType
type Finding interface {
FindingType() FindingType
}
// ModifiedFinding represents a security finding that has been modified by an external source,
@@ -39,12 +39,12 @@ type ModifiedFinding struct {
Status FindingStatus
Statement string
Source string
Finding finding // one of findings
Finding Finding // one of findings
}
func NewModifiedFinding(f finding, status FindingStatus, statement, source string) ModifiedFinding {
func NewModifiedFinding(f Finding, status FindingStatus, statement, source string) ModifiedFinding {
return ModifiedFinding{
Type: f.findingType(),
Type: f.FindingType(),
Status: status,
Statement: statement,
Source: source,
@@ -87,7 +87,7 @@ func (m *ModifiedFinding) UnmarshalJSON(data []byte) error {
return nil
}
func unmarshalFinding[T finding](data []byte) (T, error) {
func unmarshalFinding[T Finding](data []byte) (T, error) {
var f T
err := json.Unmarshal(data, &f)
return f, err

View File

@@ -33,4 +33,4 @@ type DetectedLicense struct {
Link string
}
func (DetectedLicense) findingType() FindingType { return FindingTypeLicense }
func (DetectedLicense) FindingType() FindingType { return FindingTypeLicense }

View File

@@ -38,4 +38,4 @@ const (
MisconfStatusException MisconfStatus = "EXCEPTION"
)
func (DetectedMisconfiguration) findingType() FindingType { return FindingTypeMisconfiguration }
func (DetectedMisconfiguration) FindingType() FindingType { return FindingTypeMisconfiguration }

View File

@@ -6,4 +6,4 @@ import (
type DetectedSecret ftypes.SecretFinding
func (DetectedSecret) findingType() FindingType { return FindingTypeSecret }
func (DetectedSecret) FindingType() FindingType { return FindingTypeSecret }

View File

@@ -30,7 +30,7 @@ type DetectedVulnerability struct {
types.Vulnerability
}
func (DetectedVulnerability) findingType() FindingType { return FindingTypeVulnerability }
func (DetectedVulnerability) FindingType() FindingType { return FindingTypeVulnerability }
// BySeverity implements sort.Interface based on the Severity field.
type BySeverity []DetectedVulnerability