diff --git a/docs/docs/configuration/filtering.md b/docs/docs/configuration/filtering.md index a0bf32b69e..039db1143b 100644 --- a/docs/docs/configuration/filtering.md +++ b/docs/docs/configuration/filtering.md @@ -280,8 +280,7 @@ Trivy supports the [.trivyignore](#trivyignore) and [.trivyignore.yaml](#trivyig | Vulnerability | ✓ | | Misconfiguration | ✓ | | Secret | ✓ | -| License | | - +| License | ✓ | ```bash $ cat .trivyignore @@ -300,6 +299,10 @@ AVD-DS-0002 # Ignore secrets generic-unwanted-rule aws-account-id + +# Ignore licenses +GPL-3.0 +Apache-2.0 WITH LLVM-exception ``` ```bash @@ -324,7 +327,7 @@ Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0) #### .trivyignore.yaml | Scanner | Supported | -|:----------------:|:---------:| +| :--------------: | :-------: | | Vulnerability | ✓ | | Misconfiguration | ✓ | | Secret | ✓ | @@ -378,8 +381,24 @@ licenses: - id: GPL-3.0 # License name is used as ID paths: - "usr/share/gcc/python/libstdcxx/v6/__init__.py" + - id: MIT AND GPL-2.0-or-later # Compound license expressions are supported + - id: Apache-2.0 WITH LLVM-exception # License expressions with exceptions are supported + - id: LLVM-exception # Individual license components or exceptions can be ignored ``` +!!! info "Enhanced License Expression Support" + Trivy supports filtering complex SPDX license expressions including: + + - **Compound expressions** with AND/OR operators: `MIT AND GPL-2.0-or-later` + - **License exceptions** with WITH operator: `Apache-2.0 WITH LLVM-exception` + - **Individual components**: You can ignore specific license components or exceptions from compound expressions + + When filtering compound expressions: + + - **AND/OR expressions**: All individual license components must be explicitly ignored for the entire expression to be ignored + - **WITH expressions**: License expressions with exceptions are treated as single entities and can be ignored as a whole + - **Component matching**: You can also ignore individual license names or exception names to filter specific parts of compound expressions + Since this feature is experimental, you must explicitly specify the YAML file path using the `--ignorefile` flag. Once this functionality is stable, the YAML file will be loaded automatically. diff --git a/pkg/result/filter_test.go b/pkg/result/filter_test.go index a118486323..a3a63266d6 100644 --- a/pkg/result/filter_test.go +++ b/pkg/result/filter_test.go @@ -186,6 +186,27 @@ func TestFilter(t *testing.T) { Category: "restricted", Confidence: 1, } + license3 = types.DetectedLicense{ + Name: "mit AND GPL-2.0-or-later", + Severity: dbTypes.SeverityLow.String(), + FilePath: "usr/share/gcc/python/libstdcxx/v6/__init__.py", + Category: "restricted", + Confidence: 1, + } + license4 = types.DetectedLicense{ + Name: "Apache-2.0 WITH LLVM-exception", + Severity: dbTypes.SeverityLow.String(), + FilePath: "usr/share/llvm/LICENSE.txt", + Category: "restricted", + Confidence: 1, + } + license5 = types.DetectedLicense{ + Name: "GPL-3.0 WITH GCC-exception-3.1", + Severity: dbTypes.SeverityLow.String(), + FilePath: "usr/share/gcc/LICENSE.txt", + Category: "restricted", + Confidence: 1, + } ) type args struct { report types.Report @@ -360,6 +381,13 @@ func TestFilter(t *testing.T) { secret2, }, }, + { + Target: "LICENSE.txt", + Licenses: []types.DetectedLicense{ + license1, // ignored + license3, + }, + }, }, }, severities: []dbTypes.Severity{ @@ -431,6 +459,20 @@ func TestFilter(t *testing.T) { }, }, }, + { + Target: "LICENSE.txt", + Licenses: []types.DetectedLicense{ + license3, + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeLicense, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore", + Finding: license1, + }, + }, + }, }, }, }, @@ -472,6 +514,9 @@ func TestFilter(t *testing.T) { Licenses: []types.DetectedLicense{ license1, // ignored license2, + license3, // ignored by combination for 2 licenses + license4, // ignored by WITH operator + license5, // not ignored (different exception) }, }, }, @@ -565,6 +610,7 @@ func TestFilter(t *testing.T) { Target: "LICENSE.txt", Licenses: []types.DetectedLicense{ license2, + license5, // not ignored (different exception) }, ModifiedFindings: []types.ModifiedFinding{ { @@ -573,6 +619,19 @@ func TestFilter(t *testing.T) { Source: "testdata/.trivyignore.yaml", Finding: license1, }, + { + Type: types.FindingTypeLicense, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Statement: "All license components are individually ignored", + Finding: license3, + }, + { + Type: types.FindingTypeLicense, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Finding: license4, + }, }, }, }, diff --git a/pkg/result/ignore.go b/pkg/result/ignore.go index 99d62fca9a..23d599194b 100644 --- a/pkg/result/ignore.go +++ b/pkg/result/ignore.go @@ -16,6 +16,8 @@ import ( "gopkg.in/yaml.v3" "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/licensing" + "github.com/aquasecurity/trivy/pkg/licensing/expression" "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/purl" ) @@ -178,7 +180,42 @@ func (c *IgnoreConfig) MatchSecret(secretID, filePath string) *IgnoreFinding { } func (c *IgnoreConfig) MatchLicense(licenseID, filePath string) *IgnoreFinding { - return c.Licenses.Match(licenseID, filePath, nil) + if f := c.Licenses.Match(licenseID, filePath, nil); f != nil { + return f + } + + var licenseNotMatch bool + matchLicenses := func(expr expression.Expression) expression.Expression { + // If one of parts of the expression doesn't match - skip check for the rest of the expression + if licenseNotMatch { + return expr + } + + if e, ok := expr.(expression.CompoundExpr); ok && e.Conjunction() != expression.TokenWith { + // Check only license with `WITH` operator as single license + return e + } + + if !expr.IsSPDXExpression() || c.Licenses.Match(expr.String(), filePath, nil) == nil { + licenseNotMatch = true + } + return expr + } + + _, err := expression.Normalize(licenseID, licensing.NormalizeLicense, matchLicenses) + if err != nil { + log.WithPrefix("ignore").Debug("Unable to normalize license expression", log.String("license", licenseID), log.Err(err)) + return nil + } + + if !licenseNotMatch { + return &IgnoreFinding{ + ID: licenseID, + Statement: "All license components are individually ignored", + } + } + + return nil } func ParseIgnoreFile(ctx context.Context, ignoreFile string) (IgnoreConfig, error) { diff --git a/pkg/result/ignore_test.go b/pkg/result/ignore_test.go index 3daf015327..75c744ef1c 100644 --- a/pkg/result/ignore_test.go +++ b/pkg/result/ignore_test.go @@ -16,10 +16,10 @@ func TestParseIgnoreFile(t *testing.T) { // IDs in .trivyignore are treated as IDs for all scanners // as it is unclear which type of security issue they are - assert.Len(t, got.Vulnerabilities, 6) - assert.Len(t, got.Misconfigurations, 6) - assert.Len(t, got.Secrets, 6) - assert.Len(t, got.Licenses, 6) + assert.Len(t, got.Vulnerabilities, 7) + assert.Len(t, got.Misconfigurations, 7) + assert.Len(t, got.Secrets, 7) + assert.Len(t, got.Licenses, 7) }) t.Run("happy path valid YAML config file", func(t *testing.T) { @@ -29,7 +29,7 @@ func TestParseIgnoreFile(t *testing.T) { assert.Len(t, got.Vulnerabilities, 5) assert.Len(t, got.Misconfigurations, 3) assert.Len(t, got.Secrets, 3) - assert.Len(t, got.Licenses, 1) + assert.Len(t, got.Licenses, 5) }) t.Run("empty YAML file passed", func(t *testing.T) { diff --git a/pkg/result/testdata/.trivyignore b/pkg/result/testdata/.trivyignore index fb2e885ab8..7fe609d6b1 100644 --- a/pkg/result/testdata/.trivyignore +++ b/pkg/result/testdata/.trivyignore @@ -9,4 +9,7 @@ CVE-2019-0006 exp:9999-01-01 key2:value2 ID300 # secrets -generic-unwanted-rule \ No newline at end of file +generic-unwanted-rule + +# license +GPL-3.0 \ No newline at end of file diff --git a/pkg/result/testdata/.trivyignore.yaml b/pkg/result/testdata/.trivyignore.yaml index 9690cb9944..72e513a729 100644 --- a/pkg/result/testdata/.trivyignore.yaml +++ b/pkg/result/testdata/.trivyignore.yaml @@ -37,4 +37,8 @@ secrets: licenses: - id: GPL-3.0 paths: - - "usr/share/gcc/python/libstdcxx/v6/__init__.py" \ No newline at end of file + - "usr/share/gcc/python/libstdcxx/v6/__init__.py" + - id: MIT + - id: GPL-2.0-or-later + - id: Apache-2.0 WITH LLVM-exception + - id: LLVM-exception