Skip to content

Commit 012f3d7

Browse files
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>
1 parent 18c0ee8 commit 012f3d7

6 files changed

Lines changed: 133 additions & 11 deletions

File tree

docs/docs/configuration/filtering.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,7 @@ Trivy supports the [.trivyignore](#trivyignore) and [.trivyignore.yaml](#trivyig
280280
| Vulnerability | ✓ |
281281
| Misconfiguration | ✓ |
282282
| Secret | ✓ |
283-
| License | |
284-
283+
| License | ✓ |
285284
286285
```bash
287286
$ cat .trivyignore
@@ -300,6 +299,10 @@ AVD-DS-0002
300299
# Ignore secrets
301300
generic-unwanted-rule
302301
aws-account-id
302+
303+
# Ignore licenses
304+
GPL-3.0
305+
Apache-2.0 WITH LLVM-exception
303306
```
304307
305308
```bash
@@ -324,7 +327,7 @@ Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
324327
#### .trivyignore.yaml
325328
326329
| Scanner | Supported |
327-
|:----------------:|:---------:|
330+
| :--------------: | :-------: |
328331
| Vulnerability | ✓ |
329332
| Misconfiguration | ✓ |
330333
| Secret | ✓ |
@@ -378,8 +381,24 @@ licenses:
378381
- id: GPL-3.0 # License name is used as ID
379382
paths:
380383
- "usr/share/gcc/python/libstdcxx/v6/__init__.py"
384+
- id: MIT AND GPL-2.0-or-later # Compound license expressions are supported
385+
- id: Apache-2.0 WITH LLVM-exception # License expressions with exceptions are supported
386+
- id: LLVM-exception # Individual license components or exceptions can be ignored
381387
```
382388
389+
!!! info "Enhanced License Expression Support"
390+
Trivy supports filtering complex SPDX license expressions including:
391+
392+
- **Compound expressions** with AND/OR operators: `MIT AND GPL-2.0-or-later`
393+
- **License exceptions** with WITH operator: `Apache-2.0 WITH LLVM-exception`
394+
- **Individual components**: You can ignore specific license components or exceptions from compound expressions
395+
396+
When filtering compound expressions:
397+
398+
- **AND/OR expressions**: All individual license components must be explicitly ignored for the entire expression to be ignored
399+
- **WITH expressions**: License expressions with exceptions are treated as single entities and can be ignored as a whole
400+
- **Component matching**: You can also ignore individual license names or exception names to filter specific parts of compound expressions
401+
383402
Since this feature is experimental, you must explicitly specify the YAML file path using the `--ignorefile` flag.
384403
Once this functionality is stable, the YAML file will be loaded automatically.
385404

pkg/result/filter_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,27 @@ func TestFilter(t *testing.T) {
186186
Category: "restricted",
187187
Confidence: 1,
188188
}
189+
license3 = types.DetectedLicense{
190+
Name: "mit AND GPL-2.0-or-later",
191+
Severity: dbTypes.SeverityLow.String(),
192+
FilePath: "usr/share/gcc/python/libstdcxx/v6/__init__.py",
193+
Category: "restricted",
194+
Confidence: 1,
195+
}
196+
license4 = types.DetectedLicense{
197+
Name: "Apache-2.0 WITH LLVM-exception",
198+
Severity: dbTypes.SeverityLow.String(),
199+
FilePath: "usr/share/llvm/LICENSE.txt",
200+
Category: "restricted",
201+
Confidence: 1,
202+
}
203+
license5 = types.DetectedLicense{
204+
Name: "GPL-3.0 WITH GCC-exception-3.1",
205+
Severity: dbTypes.SeverityLow.String(),
206+
FilePath: "usr/share/gcc/LICENSE.txt",
207+
Category: "restricted",
208+
Confidence: 1,
209+
}
189210
)
190211
type args struct {
191212
report types.Report
@@ -360,6 +381,13 @@ func TestFilter(t *testing.T) {
360381
secret2,
361382
},
362383
},
384+
{
385+
Target: "LICENSE.txt",
386+
Licenses: []types.DetectedLicense{
387+
license1, // ignored
388+
license3,
389+
},
390+
},
363391
},
364392
},
365393
severities: []dbTypes.Severity{
@@ -431,6 +459,20 @@ func TestFilter(t *testing.T) {
431459
},
432460
},
433461
},
462+
{
463+
Target: "LICENSE.txt",
464+
Licenses: []types.DetectedLicense{
465+
license3,
466+
},
467+
ModifiedFindings: []types.ModifiedFinding{
468+
{
469+
Type: types.FindingTypeLicense,
470+
Status: types.FindingStatusIgnored,
471+
Source: "testdata/.trivyignore",
472+
Finding: license1,
473+
},
474+
},
475+
},
434476
},
435477
},
436478
},
@@ -472,6 +514,9 @@ func TestFilter(t *testing.T) {
472514
Licenses: []types.DetectedLicense{
473515
license1, // ignored
474516
license2,
517+
license3, // ignored by combination for 2 licenses
518+
license4, // ignored by WITH operator
519+
license5, // not ignored (different exception)
475520
},
476521
},
477522
},
@@ -565,6 +610,7 @@ func TestFilter(t *testing.T) {
565610
Target: "LICENSE.txt",
566611
Licenses: []types.DetectedLicense{
567612
license2,
613+
license5, // not ignored (different exception)
568614
},
569615
ModifiedFindings: []types.ModifiedFinding{
570616
{
@@ -573,6 +619,19 @@ func TestFilter(t *testing.T) {
573619
Source: "testdata/.trivyignore.yaml",
574620
Finding: license1,
575621
},
622+
{
623+
Type: types.FindingTypeLicense,
624+
Status: types.FindingStatusIgnored,
625+
Source: "testdata/.trivyignore.yaml",
626+
Statement: "All license components are individually ignored",
627+
Finding: license3,
628+
},
629+
{
630+
Type: types.FindingTypeLicense,
631+
Status: types.FindingStatusIgnored,
632+
Source: "testdata/.trivyignore.yaml",
633+
Finding: license4,
634+
},
576635
},
577636
},
578637
},

pkg/result/ignore.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"gopkg.in/yaml.v3"
1717

1818
"github.com/aquasecurity/trivy/pkg/clock"
19+
"github.com/aquasecurity/trivy/pkg/licensing"
20+
"github.com/aquasecurity/trivy/pkg/licensing/expression"
1921
"github.com/aquasecurity/trivy/pkg/log"
2022
"github.com/aquasecurity/trivy/pkg/purl"
2123
)
@@ -178,7 +180,42 @@ func (c *IgnoreConfig) MatchSecret(secretID, filePath string) *IgnoreFinding {
178180
}
179181

180182
func (c *IgnoreConfig) MatchLicense(licenseID, filePath string) *IgnoreFinding {
181-
return c.Licenses.Match(licenseID, filePath, nil)
183+
if f := c.Licenses.Match(licenseID, filePath, nil); f != nil {
184+
return f
185+
}
186+
187+
var licenseNotMatch bool
188+
matchLicenses := func(expr expression.Expression) expression.Expression {
189+
// If one of parts of the expression doesn't match - skip check for the rest of the expression
190+
if licenseNotMatch {
191+
return expr
192+
}
193+
194+
if e, ok := expr.(expression.CompoundExpr); ok && e.Conjunction() != expression.TokenWith {
195+
// Check only license with `WITH` operator as single license
196+
return e
197+
}
198+
199+
if !expr.IsSPDXExpression() || c.Licenses.Match(expr.String(), filePath, nil) == nil {
200+
licenseNotMatch = true
201+
}
202+
return expr
203+
}
204+
205+
_, err := expression.Normalize(licenseID, licensing.NormalizeLicense, matchLicenses)
206+
if err != nil {
207+
log.WithPrefix("ignore").Debug("Unable to normalize license expression", log.String("license", licenseID), log.Err(err))
208+
return nil
209+
}
210+
211+
if !licenseNotMatch {
212+
return &IgnoreFinding{
213+
ID: licenseID,
214+
Statement: "All license components are individually ignored",
215+
}
216+
}
217+
218+
return nil
182219
}
183220

184221
func ParseIgnoreFile(ctx context.Context, ignoreFile string) (IgnoreConfig, error) {

pkg/result/ignore_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ func TestParseIgnoreFile(t *testing.T) {
1616

1717
// IDs in .trivyignore are treated as IDs for all scanners
1818
// as it is unclear which type of security issue they are
19-
assert.Len(t, got.Vulnerabilities, 6)
20-
assert.Len(t, got.Misconfigurations, 6)
21-
assert.Len(t, got.Secrets, 6)
22-
assert.Len(t, got.Licenses, 6)
19+
assert.Len(t, got.Vulnerabilities, 7)
20+
assert.Len(t, got.Misconfigurations, 7)
21+
assert.Len(t, got.Secrets, 7)
22+
assert.Len(t, got.Licenses, 7)
2323
})
2424

2525
t.Run("happy path valid YAML config file", func(t *testing.T) {
@@ -29,7 +29,7 @@ func TestParseIgnoreFile(t *testing.T) {
2929
assert.Len(t, got.Vulnerabilities, 5)
3030
assert.Len(t, got.Misconfigurations, 3)
3131
assert.Len(t, got.Secrets, 3)
32-
assert.Len(t, got.Licenses, 1)
32+
assert.Len(t, got.Licenses, 5)
3333
})
3434

3535
t.Run("empty YAML file passed", func(t *testing.T) {

pkg/result/testdata/.trivyignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ CVE-2019-0006 exp:9999-01-01 key2:value2
99
ID300
1010

1111
# secrets
12-
generic-unwanted-rule
12+
generic-unwanted-rule
13+
14+
# license
15+
GPL-3.0

pkg/result/testdata/.trivyignore.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,8 @@ secrets:
3737
licenses:
3838
- id: GPL-3.0
3939
paths:
40-
- "usr/share/gcc/python/libstdcxx/v6/__init__.py"
40+
- "usr/share/gcc/python/libstdcxx/v6/__init__.py"
41+
- id: MIT
42+
- id: GPL-2.0-or-later
43+
- id: Apache-2.0 WITH LLVM-exception
44+
- id: LLVM-exception

0 commit comments

Comments
 (0)