feat(license): use separate SPDX ids to ignore SPDX expressions (#9087)

Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com>
Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
This commit is contained in:
Yuta Tokoi
2025-11-01 19:31:59 +11:00
committed by GitHub
parent 18c0ee86f3
commit 012f3d7535
6 changed files with 133 additions and 11 deletions

View File

@@ -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.

View File

@@ -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,
},
},
},
},

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -9,4 +9,7 @@ CVE-2019-0006 exp:9999-01-01 key2:value2
ID300
# secrets
generic-unwanted-rule
generic-unwanted-rule
# license
GPL-3.0

View File

@@ -37,4 +37,8 @@ secrets:
licenses:
- id: GPL-3.0
paths:
- "usr/share/gcc/python/libstdcxx/v6/__init__.py"
- "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