Files
trivy/pkg/licensing/scanner.go

125 lines
4.0 KiB
Go

package licensing
import (
"regexp"
"strings"
"github.com/samber/lo"
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"
)
type Scanner struct {
categories map[types.LicenseCategory]set.Set[string]
}
func NewScanner(categories map[types.LicenseCategory][]string) Scanner {
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) {
expr, err := expression.Normalize(licenseName, NormalizeLicense)
if err != nil {
return types.CategoryUnknown, ""
}
category := s.detectCategory(expr)
return category, categoryToSeverity(category).String()
}
// detectCategory recursively parses license expression to detect correct license category:
// For the simple expression - use category of license
// For the 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) detectCategory(license expression.Expression) types.LicenseCategory {
var category types.LicenseCategory
switch e := license.(type) {
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 {
category = types.CategoryUnknown
break
}
comparison := func(a, b types.LicenseCategory) bool {
if e.Conjunction() == expression.TokenAnd {
return int(categoryToSeverity(a)) > int(categoryToSeverity(b)) // Take the maximum severity for `AND` operator
}
return int(categoryToSeverity(a)) < int(categoryToSeverity(b)) // Take the minimum severity for `OR` operator
}
category = lo.MaxBy([]types.LicenseCategory{left, right}, comparison)
}
return category
}
// ScanTextLicense checks license names from `categories` as glob patterns and matches licenseText against those patterns.
// 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.Iter() {
n, ok := strings.CutPrefix(name, LicenseTextPrefix)
if !ok {
continue
}
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))
continue
} else if match {
return cat, categoryToSeverity(cat).String()
}
}
}
return types.CategoryUnknown, dbTypes.SeverityUnknown.String()
}
func categoryToSeverity(category types.LicenseCategory) dbTypes.Severity {
switch category {
case types.CategoryForbidden:
return dbTypes.SeverityCritical
case types.CategoryRestricted:
return dbTypes.SeverityHigh
case types.CategoryReciprocal:
return dbTypes.SeverityMedium
case types.CategoryNotice, types.CategoryPermissive, types.CategoryUnencumbered:
return dbTypes.SeverityLow
}
return dbTypes.SeverityUnknown
}
func (s *Scanner) licenseToCategory(se expression.SimpleExpr) types.LicenseCategory {
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(names).Size() > 0 {
return category
}
}
return types.CategoryUnknown
}