mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 07:40:48 -08:00
feat: allow ignoring findings by type in Rego (#9578)
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
This commit is contained in:
@@ -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).
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
11
pkg/result/testdata/ignore-by-type.rego
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
package trivy
|
||||
|
||||
ignore {
|
||||
input.Type == "license"
|
||||
input.PkgName == "foo"
|
||||
}
|
||||
|
||||
ignore {
|
||||
input.Type == "vulnerability"
|
||||
input.PkgName == "bar"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -33,4 +33,4 @@ type DetectedLicense struct {
|
||||
Link string
|
||||
}
|
||||
|
||||
func (DetectedLicense) findingType() FindingType { return FindingTypeLicense }
|
||||
func (DetectedLicense) FindingType() FindingType { return FindingTypeLicense }
|
||||
|
||||
@@ -38,4 +38,4 @@ const (
|
||||
MisconfStatusException MisconfStatus = "EXCEPTION"
|
||||
)
|
||||
|
||||
func (DetectedMisconfiguration) findingType() FindingType { return FindingTypeMisconfiguration }
|
||||
func (DetectedMisconfiguration) FindingType() FindingType { return FindingTypeMisconfiguration }
|
||||
|
||||
@@ -6,4 +6,4 @@ import (
|
||||
|
||||
type DetectedSecret ftypes.SecretFinding
|
||||
|
||||
func (DetectedSecret) findingType() FindingType { return FindingTypeSecret }
|
||||
func (DetectedSecret) FindingType() FindingType { return FindingTypeSecret }
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user