fix(license): handle SPDX WITH exceptions as single license in category detection (#9380)

This commit is contained in:
DmitriyLewen
2025-10-29 18:26:07 +06:00
committed by GitHub
parent 89fc7b65a5
commit 212f0781c5
2 changed files with 44 additions and 13 deletions

View File

@@ -13,17 +13,16 @@ import (
"github.com/aquasecurity/trivy/pkg/set"
)
type ScannerOption struct {
IgnoredLicenses []string
LicenseCategories map[types.LicenseCategory][]string
}
type Scanner struct {
categories map[types.LicenseCategory][]string
categories map[types.LicenseCategory]set.Set[string]
}
func NewScanner(categories map[types.LicenseCategory][]string) Scanner {
return Scanner{categories: categories}
return Scanner{
categories: lo.MapValues(categories, func(v []string, _ types.LicenseCategory) set.Set[string] {
return set.NewCaseInsensitive(v...)
}),
}
}
func (s *Scanner) Scan(licenseName string) (types.LicenseCategory, string) {
@@ -49,6 +48,14 @@ func (s *Scanner) detectCategory(license expression.Expression) types.LicenseCat
case expression.SimpleExpr:
category = s.licenseToCategory(e)
case expression.CompoundExpr:
// Detect license category for `WITH` operator as a simple expression
if e.Conjunction() == expression.TokenWith {
category = s.licenseToCategory(expression.SimpleExpr{
License: e.String(),
})
break
}
left := s.detectCategory(e.Left())
right := s.detectCategory(e.Right())
if left == types.CategoryUnknown || right == types.CategoryUnknown {
@@ -71,12 +78,12 @@ func (s *Scanner) detectCategory(license expression.Expression) types.LicenseCat
// If a match is found, it returns `unknown` category and severity.
func (s *Scanner) ScanTextLicense(licenseText string) (types.LicenseCategory, string) {
for cat, names := range s.categories {
for _, name := range names {
if !strings.HasPrefix(name, LicenseTextPrefix) {
for name := range names.Iter() {
n, ok := strings.CutPrefix(name, LicenseTextPrefix)
if !ok {
continue
}
n := strings.TrimPrefix(name, LicenseTextPrefix)
match, err := regexp.MatchString(n, licenseText)
if err != nil {
log.WithPrefix("license").Debug("Failed to match license text", log.String("license_text", licenseText), log.Err(err))
@@ -104,11 +111,11 @@ func categoryToSeverity(category types.LicenseCategory) dbTypes.Severity {
}
func (s *Scanner) licenseToCategory(se expression.SimpleExpr) types.LicenseCategory {
normalizedNames := set.New(se.String()) // The license name with suffix (e.g. AGPL-1.0-or-later)
normalizedNames.Append(se.License) // Also accept the license name without suffix (e.g. AGPL-1.0)
normalizedNames := set.NewCaseInsensitive(se.String()) // The license name with suffix (e.g. AGPL-1.0-or-later)
normalizedNames.Append(se.License) // Also accept the license name without suffix (e.g. AGPL-1.0)
for category, names := range s.categories {
if normalizedNames.Intersection(set.New(names...)).Size() > 0 {
if normalizedNames.Intersection(names).Size() > 0 {
return category
}
}

View File

@@ -108,6 +108,30 @@ func TestScanner_Scan(t *testing.T) {
wantCategory: types.CategoryForbidden,
wantSeverity: "CRITICAL",
},
{
name: "license with exception",
categories: map[types.LicenseCategory][]string{
types.CategoryForbidden: {
"GPL-2.0-only WITH Classpath-exception-2.0",
expression.GPL30,
expression.Apache20,
},
},
licenseName: "GPL-3.0-only OR GPL-2.0-only WITH Classpath-exception-2.0 AND Apache-2.0",
wantCategory: types.CategoryForbidden,
wantSeverity: "CRITICAL",
},
{
name: "with separator in lowercase",
categories: map[types.LicenseCategory][]string{
types.CategoryForbidden: {
"Similar to Apache License but with the acknowledgment clause removed.",
},
},
licenseName: "Similar to Apache License but with the acknowledgment clause removed.",
wantCategory: types.CategoryForbidden,
wantSeverity: "CRITICAL",
},
{
name: "compound unknown license",
categories: map[types.LicenseCategory][]string{