mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-13 00:00:19 -08:00
feat(license): Support compound licenses (licenses using SPDX operators) (#8816)
Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
fe127715e5
commit
39f9ed128b
@@ -4,6 +4,7 @@ import (
|
||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||
"github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/licensing/expression"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/set"
|
||||
)
|
||||
|
||||
@@ -21,18 +22,46 @@ func NewScanner(categories map[types.LicenseCategory][]string) Scanner {
|
||||
}
|
||||
|
||||
func (s *Scanner) Scan(licenseName string) (types.LicenseCategory, string) {
|
||||
expr := NormalizeLicense(expression.SimpleExpr{License: licenseName})
|
||||
normalizedNames := set.New(expr.String()) // The license name with suffix (e.g. AGPL-1.0-or-later)
|
||||
if se, ok := expr.(expression.SimpleExpr); ok {
|
||||
normalizedNames.Append(se.License) // Also accept the license name without suffix (e.g. AGPL-1.0)
|
||||
visited := make(map[string]types.LicenseCategory)
|
||||
category := s.traverseLicenseExpression(licenseName, visited)
|
||||
|
||||
return category, categoryToSeverity(category).String()
|
||||
}
|
||||
|
||||
// traverseLicenseExpression recursive parses license expression to detect correct license category:
|
||||
// For Simple Expression - use category of license
|
||||
// For Compound Expression:
|
||||
// - `AND` operator - use category with maximum severity
|
||||
// - `OR` operator - use category with minimum severity
|
||||
// - one of expression has `UNKNOWN` category - use `UNKNOWN` category
|
||||
func (s *Scanner) traverseLicenseExpression(licenseName string, visited map[string]types.LicenseCategory) types.LicenseCategory {
|
||||
category := types.CategoryUnknown
|
||||
|
||||
detectCategory := func(expr expression.Expression) expression.Expression {
|
||||
// Skip if we already checked this license
|
||||
if cat, ok := visited[licenseName]; ok {
|
||||
category = cat
|
||||
return expr
|
||||
}
|
||||
|
||||
for category, names := range s.categories {
|
||||
if normalizedNames.Intersection(set.New(names...)).Size() > 0 {
|
||||
return category, categoryToSeverity(category).String()
|
||||
switch e := expr.(type) {
|
||||
case expression.SimpleExpr:
|
||||
category = s.licenseToCategory(e)
|
||||
case expression.CompoundExpr:
|
||||
category = s.compoundLicenseToCategory(e, visited)
|
||||
}
|
||||
|
||||
visited[licenseName] = category
|
||||
return expr
|
||||
}
|
||||
return types.CategoryUnknown, dbTypes.SeverityUnknown.String()
|
||||
|
||||
_, err := expression.Normalize(licenseName, NormalizeLicense, detectCategory)
|
||||
if err != nil {
|
||||
log.WithPrefix("license").Debug("Unable to detect license category", log.String("license", licenseName), log.Err(err))
|
||||
return types.CategoryUnknown
|
||||
}
|
||||
|
||||
return category
|
||||
}
|
||||
|
||||
func categoryToSeverity(category types.LicenseCategory) dbTypes.Severity {
|
||||
@@ -48,3 +77,50 @@ func categoryToSeverity(category types.LicenseCategory) dbTypes.Severity {
|
||||
}
|
||||
return dbTypes.SeverityUnknown
|
||||
}
|
||||
|
||||
func (s *Scanner) licenseToCategory(license expression.SimpleExpr) types.LicenseCategory {
|
||||
expr := NormalizeLicense(license)
|
||||
normalizedNames := set.New(expr.String()) // The license name with suffix (e.g. AGPL-1.0-or-later)
|
||||
if se, ok := expr.(expression.SimpleExpr); ok {
|
||||
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 {
|
||||
return category
|
||||
}
|
||||
}
|
||||
|
||||
return types.CategoryUnknown
|
||||
}
|
||||
|
||||
func (s *Scanner) compoundLicenseToCategory(license expression.CompoundExpr, visited map[string]types.LicenseCategory) types.LicenseCategory {
|
||||
switch license.Conjunction() {
|
||||
case expression.TokenAnd:
|
||||
return s.compoundLogicEvaluator(license, visited, true)
|
||||
case expression.TokenOR:
|
||||
return s.compoundLogicEvaluator(license, visited, false)
|
||||
default:
|
||||
return types.CategoryUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scanner) compoundLogicEvaluator(license expression.CompoundExpr, visited map[string]types.LicenseCategory, findMax bool) types.LicenseCategory {
|
||||
lCategory := s.traverseLicenseExpression(license.Left().String(), visited)
|
||||
lSeverity := categoryToSeverity(lCategory)
|
||||
rCategory := s.traverseLicenseExpression(license.Right().String(), visited)
|
||||
rSeverity := categoryToSeverity(rCategory)
|
||||
|
||||
if lSeverity == dbTypes.SeverityUnknown || rSeverity == dbTypes.SeverityUnknown {
|
||||
return types.CategoryUnknown
|
||||
}
|
||||
|
||||
// Compare the two severities, returns a negative value if left is more severe than right
|
||||
comparison := dbTypes.CompareSeverityString(lSeverity.String(), rSeverity.String())
|
||||
leftIsMoreSevere := comparison < 0
|
||||
|
||||
if (findMax && leftIsMoreSevere) || (!findMax && !leftIsMoreSevere) {
|
||||
return lCategory
|
||||
}
|
||||
return rCategory
|
||||
}
|
||||
|
||||
@@ -68,6 +68,62 @@ func TestScanner_Scan(t *testing.T) {
|
||||
wantCategory: types.CategoryRestricted,
|
||||
wantSeverity: "HIGH",
|
||||
},
|
||||
{
|
||||
name: "compound OR license",
|
||||
categories: map[types.LicenseCategory][]string{
|
||||
types.CategoryForbidden: {
|
||||
expression.GPL30,
|
||||
},
|
||||
types.CategoryRestricted: {
|
||||
expression.Apache20,
|
||||
},
|
||||
},
|
||||
licenseName: expression.GPL30 + " OR " + expression.Apache20,
|
||||
wantCategory: types.CategoryRestricted,
|
||||
wantSeverity: "HIGH",
|
||||
},
|
||||
{
|
||||
name: "compound AND license",
|
||||
categories: map[types.LicenseCategory][]string{
|
||||
types.CategoryForbidden: {
|
||||
expression.GPL30,
|
||||
},
|
||||
types.CategoryRestricted: {
|
||||
expression.Apache20,
|
||||
},
|
||||
},
|
||||
licenseName: expression.GPL30 + " AND " + expression.Apache20,
|
||||
wantCategory: types.CategoryForbidden,
|
||||
wantSeverity: "CRITICAL",
|
||||
},
|
||||
{
|
||||
name: "compound unknown license",
|
||||
categories: map[types.LicenseCategory][]string{
|
||||
types.CategoryForbidden: {
|
||||
expression.GPL30,
|
||||
},
|
||||
},
|
||||
licenseName: expression.GPL30 + " AND " + expression.Apache20,
|
||||
wantCategory: types.CategoryUnknown,
|
||||
wantSeverity: "UNKNOWN",
|
||||
},
|
||||
{
|
||||
name: "compound long license, recursive",
|
||||
categories: map[types.LicenseCategory][]string{
|
||||
types.CategoryForbidden: {
|
||||
expression.GPL30,
|
||||
},
|
||||
types.CategoryRestricted: {
|
||||
expression.BSD3Clause,
|
||||
},
|
||||
types.CategoryNotice: {
|
||||
expression.Apache20,
|
||||
},
|
||||
},
|
||||
licenseName: "(" + expression.BSD3Clause + " OR " + expression.GPL30 + ")" + " AND (" + expression.GPL30 + " OR " + expression.Apache20 + ")",
|
||||
wantCategory: types.CategoryRestricted,
|
||||
wantSeverity: "HIGH",
|
||||
},
|
||||
{
|
||||
name: "unknown",
|
||||
categories: make(map[types.LicenseCategory][]string),
|
||||
|
||||
@@ -335,7 +335,6 @@ func (s Service) scanApplicationLicenses(apps []ftypes.Application, scanner lice
|
||||
Licenses: langLicenses,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
Reference in New Issue
Block a user