mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-05 20:40:16 -08:00
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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
5
pkg/result/testdata/.trivyignore
vendored
5
pkg/result/testdata/.trivyignore
vendored
@@ -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
|
||||
6
pkg/result/testdata/.trivyignore.yaml
vendored
6
pkg/result/testdata/.trivyignore.yaml
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user