mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 15:50:15 -08:00
fix(cyclonedx): handle multiple license types (#9378)
This commit is contained in:
@@ -295,19 +295,49 @@ func (m *Marshaler) Licenses(licenses []string) *cdx.Licenses {
|
||||
if len(licenses) == 0 {
|
||||
return nil
|
||||
}
|
||||
choices := lo.Map(licenses, func(license string, _ int) cdx.LicenseChoice {
|
||||
return m.normalizeLicenses(licenses)
|
||||
}
|
||||
|
||||
func (m *Marshaler) normalizeLicenses(licenses []string) *cdx.Licenses {
|
||||
expressions := lo.Map(licenses, func(license string, _ int) expression.Expression {
|
||||
return m.normalizeLicense(license)
|
||||
})
|
||||
// Check if all licenses are valid SPDX expressions
|
||||
allValidSPDX := lo.EveryBy(expressions, func(expr expression.Expression) bool {
|
||||
return expr.IsSPDXExpression()
|
||||
})
|
||||
|
||||
// Check if at least one is a CompoundExpr
|
||||
hasCompoundExpr := lo.ContainsBy(expressions, func(expr expression.Expression) bool {
|
||||
_, isCompound := expr.(expression.CompoundExpr)
|
||||
return isCompound
|
||||
})
|
||||
|
||||
// If all are valid SPDX AND at least one contains CompoundExpr, combine into single Expression
|
||||
if allValidSPDX && hasCompoundExpr {
|
||||
exprStrs := lo.Map(expressions, func(expr expression.Expression, _ int) string {
|
||||
return expr.String()
|
||||
})
|
||||
return &cdx.Licenses{{Expression: strings.Join(exprStrs, " AND ")}}
|
||||
}
|
||||
|
||||
// Otherwise use individual LicenseChoice entries with license.id or license.name
|
||||
choices := lo.Map(expressions, func(expr expression.Expression, _ int) cdx.LicenseChoice {
|
||||
if s, ok := expr.(expression.SimpleExpr); ok && s.IsSPDXExpression() {
|
||||
// Use license.id for valid SPDX ID (e.g., "MIT", "Apache-2.0")
|
||||
return cdx.LicenseChoice{License: &cdx.License{ID: s.String()}}
|
||||
}
|
||||
// Use license.name for everything else (invalid SPDX ID, SPDX expression, etc.)
|
||||
return cdx.LicenseChoice{License: &cdx.License{Name: expr.String()}}
|
||||
})
|
||||
return lo.ToPtr(cdx.Licenses(choices))
|
||||
}
|
||||
|
||||
func (m *Marshaler) normalizeLicense(license string) cdx.LicenseChoice {
|
||||
func (m *Marshaler) normalizeLicense(license string) expression.Expression {
|
||||
// Save text license as licenseChoice.license.name
|
||||
if after, ok := strings.CutPrefix(license, licensing.LicenseTextPrefix); ok {
|
||||
return cdx.LicenseChoice{
|
||||
License: &cdx.License{
|
||||
Name: after,
|
||||
},
|
||||
return expression.SimpleExpr{
|
||||
License: after,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,30 +349,10 @@ func (m *Marshaler) normalizeLicense(license string) cdx.LicenseChoice {
|
||||
if err != nil {
|
||||
// Not fail on the invalid license
|
||||
m.logger.Warn("Unable to marshal SPDX licenses", log.String("license", license))
|
||||
return cdx.LicenseChoice{}
|
||||
return expression.SimpleExpr{License: license}
|
||||
}
|
||||
|
||||
// The license is not a valid SPDX ID or SPDX expression
|
||||
if !normalizedLicenses.IsSPDXExpression() {
|
||||
// Use LicenseChoice.License.Name for invalid SPDX ID / SPDX expression
|
||||
return cdx.LicenseChoice{
|
||||
License: &cdx.License{Name: normalizedLicenses.String()},
|
||||
}
|
||||
}
|
||||
|
||||
// The license is a valid SPDX ID or SPDX expression
|
||||
var licenseChoice cdx.LicenseChoice
|
||||
switch normalizedLicenses.(type) {
|
||||
case expression.SimpleExpr:
|
||||
// Use LicenseChoice.License.ID for valid SPDX ID
|
||||
licenseChoice.License = &cdx.License{ID: normalizedLicenses.String()}
|
||||
case expression.CompoundExpr:
|
||||
// Use LicenseChoice.Expression for valid SPDX expression (with any conjunction)
|
||||
// e.g. "GPL-2.0 WITH Classpath-exception-2.0" or "GPL-2.0 AND MIT"
|
||||
licenseChoice.Expression = normalizedLicenses.String()
|
||||
}
|
||||
|
||||
return licenseChoice
|
||||
return normalizedLicenses
|
||||
}
|
||||
|
||||
func (*Marshaler) Properties(properties []core.Property) *[]cdx.Property {
|
||||
|
||||
@@ -2129,13 +2129,15 @@ func TestMarshaler_MarshalReport(t *testing.T) {
|
||||
|
||||
func TestMarshaler_Licenses(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
license string
|
||||
want *cdx.Licenses
|
||||
name string
|
||||
licenses []string
|
||||
want *cdx.Licenses
|
||||
}{
|
||||
{
|
||||
name: "SPDX ID",
|
||||
license: "MIT",
|
||||
name: "SPDX ID",
|
||||
licenses: []string{
|
||||
"MIT",
|
||||
},
|
||||
want: &cdx.Licenses{
|
||||
cdx.LicenseChoice{
|
||||
License: &cdx.License{
|
||||
@@ -2145,8 +2147,10 @@ func TestMarshaler_Licenses(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Unknown SPDX ID",
|
||||
license: "no-spdx-id-license",
|
||||
name: "Unknown SPDX ID",
|
||||
licenses: []string{
|
||||
"no-spdx-id-license",
|
||||
},
|
||||
want: &cdx.Licenses{
|
||||
cdx.LicenseChoice{
|
||||
License: &cdx.License{
|
||||
@@ -2156,8 +2160,10 @@ func TestMarshaler_Licenses(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "text license",
|
||||
license: "text://text of license",
|
||||
name: "text license",
|
||||
licenses: []string{
|
||||
"text://text of license",
|
||||
},
|
||||
want: &cdx.Licenses{
|
||||
cdx.LicenseChoice{
|
||||
License: &cdx.License{
|
||||
@@ -2167,8 +2173,10 @@ func TestMarshaler_Licenses(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SPDX license with exception",
|
||||
license: "AFL 2.0 with Linux-syscall-note",
|
||||
name: "SPDX license with exception",
|
||||
licenses: []string{
|
||||
"AFL 2.0 with Linux-syscall-note",
|
||||
},
|
||||
want: &cdx.Licenses{
|
||||
cdx.LicenseChoice{
|
||||
Expression: "AFL-2.0 WITH Linux-syscall-note",
|
||||
@@ -2176,8 +2184,10 @@ func TestMarshaler_Licenses(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SPDX license with wrong exception",
|
||||
license: "GPL-2.0-with-autoconf-exception+",
|
||||
name: "SPDX license with wrong exception",
|
||||
licenses: []string{
|
||||
"GPL-2.0-with-autoconf-exception+",
|
||||
},
|
||||
want: &cdx.Licenses{
|
||||
cdx.LicenseChoice{
|
||||
License: &cdx.License{
|
||||
@@ -2187,8 +2197,10 @@ func TestMarshaler_Licenses(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SPDX expression",
|
||||
license: "GPL-3.0-only OR AFL 2.0 with Linux-syscall-note AND GPL-3.0-only",
|
||||
name: "SPDX expression",
|
||||
licenses: []string{
|
||||
"GPL-3.0-only OR AFL 2.0 with Linux-syscall-note AND GPL-3.0-only",
|
||||
},
|
||||
want: &cdx.Licenses{
|
||||
cdx.LicenseChoice{
|
||||
Expression: "GPL-3.0-only OR AFL-2.0 WITH Linux-syscall-note AND GPL-3.0-only",
|
||||
@@ -2196,8 +2208,10 @@ func TestMarshaler_Licenses(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid SPDX expression",
|
||||
license: "wrong-spdx-id OR GPL-3.0-only",
|
||||
name: "invalid SPDX expression",
|
||||
licenses: []string{
|
||||
"wrong-spdx-id OR GPL-3.0-only",
|
||||
},
|
||||
want: &cdx.Licenses{
|
||||
cdx.LicenseChoice{
|
||||
License: &cdx.License{
|
||||
@@ -2207,16 +2221,93 @@ func TestMarshaler_Licenses(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty license",
|
||||
license: "",
|
||||
want: nil,
|
||||
name: "multiple SPDX IDs",
|
||||
licenses: []string{
|
||||
"AFL 2.0 with Linux-syscall-note",
|
||||
"GPL-3.0-only OR MIT",
|
||||
},
|
||||
want: &cdx.Licenses{
|
||||
cdx.LicenseChoice{
|
||||
Expression: "AFL-2.0 WITH Linux-syscall-note AND GPL-3.0-only OR MIT",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple SPDX expressions",
|
||||
licenses: []string{
|
||||
"MIT",
|
||||
"AFL 2.0",
|
||||
},
|
||||
want: &cdx.Licenses{
|
||||
cdx.LicenseChoice{
|
||||
License: &cdx.License{
|
||||
ID: "MIT",
|
||||
},
|
||||
},
|
||||
cdx.LicenseChoice{
|
||||
License: &cdx.License{
|
||||
ID: "AFL-2.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SPDX ID + license name",
|
||||
licenses: []string{
|
||||
"MIT",
|
||||
"license-name",
|
||||
},
|
||||
want: &cdx.Licenses{
|
||||
cdx.LicenseChoice{
|
||||
License: &cdx.License{
|
||||
ID: "MIT",
|
||||
},
|
||||
},
|
||||
cdx.LicenseChoice{
|
||||
License: &cdx.License{
|
||||
Name: "license-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SPDX ID + SPDX exception",
|
||||
licenses: []string{
|
||||
"MIT",
|
||||
"AFL 2.0 with Linux-Syscall-Note",
|
||||
},
|
||||
want: &cdx.Licenses{
|
||||
cdx.LicenseChoice{
|
||||
Expression: "MIT AND AFL-2.0 WITH Linux-syscall-note",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "license normalization error",
|
||||
licenses: []string{
|
||||
"Copyright (c) 2000, 2025, Oracle and/or its affiliates. Under GPLv2 license as shown in the Description field.",
|
||||
},
|
||||
want: &cdx.Licenses{
|
||||
cdx.LicenseChoice{
|
||||
License: &cdx.License{
|
||||
Name: "Copyright (c) 2000, 2025, Oracle and/or its affiliates. Under GPLv2 license as shown in the Description field.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty license",
|
||||
licenses: []string{
|
||||
"",
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
marshaler := cyclonedx.NewMarshaler("dev")
|
||||
got := marshaler.Licenses([]string{tt.license})
|
||||
got := marshaler.Licenses(tt.licenses)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user