Skip to content
Open
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
25 changes: 25 additions & 0 deletions grant/license.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ func handleSPDXLicense(license syftPkg.License, licenses []License, licenseLocat

// we have what seems to be a valid SPDX license ID, let's try and get more info about it
spdxLicense, err := spdxlicense.GetLicenseByID(extractedLicense)
if err != nil {
// The parser returns "<license> WITH <exception>" as a single atom, but the
// SPDX license list is indexed by license ID alone, so the lookup above can
// only fail for these. Retry with the license the exception applies to; the
// full expression is still what ends up on the resulting License.
if base, ok := licenseFromExceptionExpression(extractedLicense); ok {
spdxLicense, err = spdxlicense.GetLicenseByID(base)
}
}
if err != nil {
log.Errorf("unable to get license by ID: %s; no matching spdx id found", extractedLicense)
// if we can't find a matching SPDX license, just add the license as-is
Expand All @@ -136,6 +145,22 @@ func handleSPDXLicense(license syftPkg.License, licenses []License, licenseLocat
return licenses
}

// licenseFromExceptionExpression returns the license an SPDX "WITH" expression
// applies to, for example "Apache-2.0" from "Apache-2.0 WITH LLVM-exception".
func licenseFromExceptionExpression(expression string) (string, bool) {
parts := strings.SplitN(expression, " WITH ", 2)
if len(parts) != 2 {
return "", false
}

license := strings.TrimRight(strings.TrimSpace(parts[0]), "+")
if license == "" || strings.TrimSpace(parts[1]) == "" {
return "", false
}

return license, true
}

func addNonSPDXLicense(licenses []License, license syftPkg.License, locations []string) []License {
// Filter out sha256: licenses - these are content hashes from Syft when license detection fails
if strings.HasPrefix(license.Value, "sha256:") {
Expand Down
51 changes: 51 additions & 0 deletions grant/license_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,54 @@ func TestConvertSyftLicenses_MalformedSPDXExpressionDoesNotPanic(t *testing.T) {
})
}
}

// TestConvertSyftLicenses_SPDXExpressionWithException covers SPDX expressions
// that use the WITH operator. The upstream parser hands these back as a single
// atom ("Apache-2.0 WITH LLVM-exception"), which is not a key in the SPDX
// license list, so grant used to log "unable to get license by ID" and fall back
// to an unresolved license.
func TestConvertSyftLicenses_SPDXExpressionWithException(t *testing.T) {
tests := []struct {
expression string
wantSPDX []string
wantLicense []string
}{
{
expression: "Apache-2.0 WITH LLVM-exception",
wantSPDX: []string{"Apache-2.0 WITH LLVM-exception"},
wantLicense: []string{"Apache-2.0"},
},
{
expression: "GPL-2.0-only WITH Classpath-exception-2.0",
wantSPDX: []string{"GPL-2.0-only WITH Classpath-exception-2.0"},
wantLicense: []string{"GPL-2.0-only"},
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.expression, func(t *testing.T) {
set := syftPkg.NewLicenseSet(syftPkg.License{Value: tt.expression, SPDXExpression: tt.expression})

got := ConvertSyftLicenses(set)

if len(got) != len(tt.wantSPDX) {
t.Fatalf("expected %d licenses, got %d", len(tt.wantSPDX), len(got))
}
for i, want := range tt.wantSPDX {
if !got[i].IsSPDX() {
t.Fatalf("expected %q to resolve to an SPDX license, got name %q", tt.expression, got[i].Name)
}
if got[i].SPDXExpression != want {
t.Errorf("got SPDX expression %q, want %q", got[i].SPDXExpression, want)
}
if got[i].LicenseID != tt.wantLicense[i] {
t.Errorf("got license id %q, want %q", got[i].LicenseID, tt.wantLicense[i])
}
if got[i].Name == "" {
t.Errorf("expected the license name to be populated from the SPDX list")
}
}
})
}
}