From 38a090c2189fc15327b26973d2e22b155cc40d7a Mon Sep 17 00:00:00 2001 From: Avi Deitcher Date: Tue, 7 Feb 2023 18:47:04 +0200 Subject: [PATCH] fix: add support for licenses not found on list (#1540) Signed-off-by: Avi Deitcher --- internal/spdxlicense/license.go | 19 ++++++-- internal/spdxlicense/license_test.go | 47 +++++++++++++++++-- .../common/cyclonedxhelpers/licenses.go | 16 +++++-- .../common/cyclonedxhelpers/licenses_test.go | 4 +- syft/formats/common/spdxhelpers/license.go | 20 +++++--- .../common/spdxhelpers/license_test.go | 2 +- .../common/spdxhelpers/to_format_model.go | 23 +++++++++ 7 files changed, 113 insertions(+), 18 deletions(-) diff --git a/internal/spdxlicense/license.go b/internal/spdxlicense/license.go index a3029d283..1f83034df 100644 --- a/internal/spdxlicense/license.go +++ b/internal/spdxlicense/license.go @@ -12,9 +12,22 @@ import ( // EX: gpl-2.0.0-only ---> GPL-2.0-only // See the debian link for more details on the spdx license differences +const ( + LicenseRefPrefix = "LicenseRef-" // prefix for non-standard licenses +) + //go:generate go run ./generate -func ID(id string) (string, bool) { - value, exists := licenseIDs[strings.ToLower(id)] - return value, exists +func ID(id string) (value, other string, exists bool) { + id = strings.TrimSpace(id) + // ignore blank strings or the joiner + if id == "" || id == "AND" { + return "", "", false + } + // first look for a canonical license + if value, exists := licenseIDs[strings.ToLower(id)]; exists { + return value, "", exists + } + // we did not find, so treat it as a separate license + return "", id, true } diff --git a/internal/spdxlicense/license_test.go b/internal/spdxlicense/license_test.go index 11456f3c3..23a54a87c 100644 --- a/internal/spdxlicense/license_test.go +++ b/internal/spdxlicense/license_test.go @@ -9,52 +9,91 @@ import ( func TestIDParse(t *testing.T) { var tests = []struct { shortName string - spdx string + id string + other string + found bool }{ { "GPL-1-only", "GPL-1.0-only", + "", + true, }, { "GPL-2", "GPL-2.0-only", + "", + true, }, { "GPL-2+", "GPL-2.0-or-later", + "", + true, }, { "GPL-3.0.0-or-later", "GPL-3.0-or-later", + "", + true, }, { "GPL-3-with-autoconf-exception", "GPL-3.0-with-autoconf-exception", + "", + true, }, { "CC-by-nc-3-de", "CC-BY-NC-3.0-DE", + "", + true, }, // the below few cases are NOT expected, however, seem unavoidable given the current approach { "w3c-20150513.0.0", "W3C-20150513", + "", + true, }, { "spencer-86.0.0", "Spencer-86", + "", + true, }, { "unicode-dfs-2015.0.0", "Unicode-DFS-2015", + "", + true, + }, + { + "Unknown", + "", + "Unknown", + true, + }, + { + " ", + "", + "", + false, + }, + { + "AND", + "", + "", + false, }, } for _, test := range tests { t.Run(test.shortName, func(t *testing.T) { - got, exists := ID(test.shortName) - assert.True(t, exists) - assert.Equal(t, test.spdx, got) + value, other, exists := ID(test.shortName) + assert.Equal(t, test.found, exists) + assert.Equal(t, test.id, value) + assert.Equal(t, test.other, other) }) } } diff --git a/syft/formats/common/cyclonedxhelpers/licenses.go b/syft/formats/common/cyclonedxhelpers/licenses.go index ab8aa9d3a..7518b0976 100644 --- a/syft/formats/common/cyclonedxhelpers/licenses.go +++ b/syft/formats/common/cyclonedxhelpers/licenses.go @@ -10,10 +10,11 @@ import ( func encodeLicenses(p pkg.Package) *cyclonedx.Licenses { lc := cyclonedx.Licenses{} for _, licenseName := range p.Licenses { - if value, exists := spdxlicense.ID(licenseName); exists { + if value, other, exists := spdxlicense.ID(licenseName); exists { lc = append(lc, cyclonedx.LicenseChoice{ License: &cyclonedx.License{ - ID: value, + ID: value, + Name: other, }, }) } @@ -28,7 +29,16 @@ func decodeLicenses(c *cyclonedx.Component) (out []string) { if c.Licenses != nil { for _, l := range *c.Licenses { if l.License != nil { - out = append(out, l.License.ID) + var lic string + switch { + case l.License.ID != "": + lic = l.License.ID + case l.License.Name != "": + lic = l.License.Name + default: + continue + } + out = append(out, lic) } } } diff --git a/syft/formats/common/cyclonedxhelpers/licenses_test.go b/syft/formats/common/cyclonedxhelpers/licenses_test.go index d8e7b37cf..64999e60c 100644 --- a/syft/formats/common/cyclonedxhelpers/licenses_test.go +++ b/syft/formats/common/cyclonedxhelpers/licenses_test.go @@ -27,7 +27,9 @@ func Test_encodeLicense(t *testing.T) { "made-up", }, }, - expected: nil, + expected: &cyclonedx.Licenses{ + {License: &cyclonedx.License{Name: "made-up"}}, + }, }, { name: "with SPDX license", diff --git a/syft/formats/common/spdxhelpers/license.go b/syft/formats/common/spdxhelpers/license.go index 3feaee994..7eb271ecb 100644 --- a/syft/formats/common/spdxhelpers/license.go +++ b/syft/formats/common/spdxhelpers/license.go @@ -22,12 +22,7 @@ func License(p pkg.Package) string { } // take all licenses and assume an AND expression; for information about license expressions see https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/ - var parsedLicenses []string - for _, l := range p.Licenses { - if value, exists := spdxlicense.ID(l); exists { - parsedLicenses = append(parsedLicenses, value) - } - } + parsedLicenses := parseLicenses(p.Licenses) if len(parsedLicenses) == 0 { return NOASSERTION @@ -35,3 +30,16 @@ func License(p pkg.Package) string { return strings.Join(parsedLicenses, " AND ") } + +func parseLicenses(raw []string) (parsedLicenses []string) { + for _, l := range raw { + if value, other, exists := spdxlicense.ID(l); exists { + parsed := value + if other != "" { + parsed = spdxlicense.LicenseRefPrefix + other + } + parsedLicenses = append(parsedLicenses, parsed) + } + } + return +} diff --git a/syft/formats/common/spdxhelpers/license_test.go b/syft/formats/common/spdxhelpers/license_test.go index ee51c16c1..747b330da 100644 --- a/syft/formats/common/spdxhelpers/license_test.go +++ b/syft/formats/common/spdxhelpers/license_test.go @@ -26,7 +26,7 @@ func Test_License(t *testing.T) { "made-up", }, }, - expected: NOASSERTION, + expected: "LicenseRef-made-up", }, { name: "with SPDX license", diff --git a/syft/formats/common/spdxhelpers/to_format_model.go b/syft/formats/common/spdxhelpers/to_format_model.go index 87e548069..6160c226e 100644 --- a/syft/formats/common/spdxhelpers/to_format_model.go +++ b/syft/formats/common/spdxhelpers/to_format_model.go @@ -124,6 +124,7 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document { Packages: toPackages(s.Artifacts.PackageCatalog, s), Files: toFiles(s), Relationships: relationships, + OtherLicenses: toOtherLicenses(s.Artifacts.PackageCatalog), } } @@ -511,6 +512,28 @@ func toFileTypes(metadata *source.FileMetadata) (ty []string) { return ty } +func toOtherLicenses(catalog *pkg.Catalog) []*spdx.OtherLicense { + licenses := map[string]bool{} + for _, pkg := range catalog.Sorted() { + for _, license := range parseLicenses(pkg.Licenses) { + if strings.HasPrefix(license, spdxlicense.LicenseRefPrefix) { + licenses[license] = true + } + } + } + var result []*spdx.OtherLicense + for license := range licenses { + // separate the actual ID from the prefix + name := strings.TrimPrefix(license, spdxlicense.LicenseRefPrefix) + result = append(result, &spdx.OtherLicense{ + LicenseIdentifier: license, + LicenseName: name, + ExtractedText: NONE, // we probably should have some extracted text here, but this is good enough for now + }) + } + return result +} + // TODO: handle SPDX excludes file case // f file is an "excludes" file, skip it /* exclude SPDX analysis file(s) */ // see: https://spdx.github.io/spdx-spec/v2.3/package-information/#79-package-verification-code-field