Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 38 additions & 28 deletions pkg/sbom/cyclonedx/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand All @@ -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{}
}

// 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 expression.SimpleExpr{License: license}
}

return licenseChoice
return normalizedLicenses
}

func (*Marshaler) Properties(properties []core.Property) *[]cdx.Property {
Expand Down
133 changes: 112 additions & 21 deletions pkg/sbom/cyclonedx/marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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{
Expand All @@ -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{
Expand All @@ -2167,17 +2173,21 @@ 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",
},
},
},
{
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{
Expand All @@ -2187,17 +2197,21 @@ 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",
},
},
},
{
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{
Expand All @@ -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)
})
}
Expand Down
Loading