fix(cyclonedx): handle multiple license types (#9378)

This commit is contained in:
DmitriyLewen
2025-09-01 18:10:14 +06:00
committed by GitHub
parent 1ac9b1f07c
commit 46ab76a5af
2 changed files with 150 additions and 49 deletions

View File

@@ -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 {

View File

@@ -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)
})
}