mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 02:26:42 +01:00
feat: update syft license construction to be able to look up by URL (#4132)
--------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
This commit is contained in:
parent
104df88143
commit
89470ecdd3
@ -31,6 +31,13 @@ var licenseIDs = map[string]string{
|
||||
{{ printf "%q" $k }}: {{ printf "%q" $v }},
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
// urlToLicense maps license URLs from the seeAlso field to license IDs
|
||||
var urlToLicense = map[string]string{
|
||||
{{- range $url, $id := .URLToLicense }}
|
||||
{{ printf "%q" $url }}: {{ printf "%q" $id }},
|
||||
{{- end }}
|
||||
}
|
||||
`))
|
||||
|
||||
var versionMatch = regexp.MustCompile(`([0-9]+)\.?([0-9]+)?\.?([0-9]+)?\.?`)
|
||||
@ -68,17 +75,20 @@ func run() error {
|
||||
}()
|
||||
|
||||
licenseIDs := processSPDXLicense(result)
|
||||
urlToLicense := buildURLToLicenseMap(result)
|
||||
|
||||
err = tmp.Execute(f, struct {
|
||||
Timestamp time.Time
|
||||
URL string
|
||||
Version string
|
||||
LicenseIDs map[string]string
|
||||
Timestamp time.Time
|
||||
URL string
|
||||
Version string
|
||||
LicenseIDs map[string]string
|
||||
URLToLicense map[string]string
|
||||
}{
|
||||
Timestamp: time.Now(),
|
||||
URL: url,
|
||||
Version: result.Version,
|
||||
LicenseIDs: licenseIDs,
|
||||
Timestamp: time.Now(),
|
||||
URL: url,
|
||||
Version: result.Version,
|
||||
LicenseIDs: licenseIDs,
|
||||
URLToLicense: urlToLicense,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@ -156,3 +166,30 @@ func cleanLicenseID(id string) string {
|
||||
cleanID := strings.ToLower(id)
|
||||
return strings.ReplaceAll(cleanID, "-", "")
|
||||
}
|
||||
|
||||
// buildURLToLicenseMap creates a mapping from license URLs (from seeAlso fields) to license IDs
|
||||
func buildURLToLicenseMap(result LicenseList) map[string]string {
|
||||
urlMap := make(map[string]string)
|
||||
|
||||
for _, l := range result.Licenses {
|
||||
// Skip deprecated licenses
|
||||
if l.Deprecated {
|
||||
// Find replacement license if available
|
||||
replacement := result.findReplacementLicense(l)
|
||||
if replacement != nil {
|
||||
// Map deprecated license URLs to the replacement license
|
||||
for _, url := range l.SeeAlso {
|
||||
urlMap[url] = replacement.ID
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Add URLs from non-deprecated licenses
|
||||
for _, url := range l.SeeAlso {
|
||||
urlMap[url] = l.ID
|
||||
}
|
||||
}
|
||||
|
||||
return urlMap
|
||||
}
|
||||
|
||||
@ -35,3 +35,20 @@ func cleanLicenseID(id string) string {
|
||||
id = strings.ToLower(id)
|
||||
return strings.ReplaceAll(id, "-", "")
|
||||
}
|
||||
|
||||
// LicenseInfo contains license ID and name
|
||||
type LicenseInfo struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
// LicenseByURL returns the license ID and name for a given URL from the SPDX license list
|
||||
// The URL should match one of the URLs in the seeAlso field of an SPDX license
|
||||
func LicenseByURL(url string) (LicenseInfo, bool) {
|
||||
url = strings.TrimSpace(url)
|
||||
if id, exists := urlToLicense[url]; exists {
|
||||
return LicenseInfo{
|
||||
ID: id,
|
||||
}, true
|
||||
}
|
||||
return LicenseInfo{}, false
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
97
internal/spdxlicense/license_url_test.go
Normal file
97
internal/spdxlicense/license_url_test.go
Normal file
@ -0,0 +1,97 @@
|
||||
package spdxlicense
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLicenseByURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
wantID string
|
||||
wantFound bool
|
||||
}{
|
||||
{
|
||||
name: "MIT license URL (https)",
|
||||
url: "https://opensource.org/license/mit/",
|
||||
wantID: "MIT",
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "MIT license URL (http)",
|
||||
url: "http://opensource.org/licenses/MIT",
|
||||
wantID: "MIT",
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "Apache 2.0 license URL",
|
||||
url: "https://www.apache.org/licenses/LICENSE-2.0",
|
||||
wantID: "Apache-2.0",
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "GPL 3.0 or later URL",
|
||||
url: "https://www.gnu.org/licenses/gpl-3.0-standalone.html",
|
||||
wantID: "GPL-3.0-or-later",
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "BSD 3-Clause URL",
|
||||
url: "https://opensource.org/licenses/BSD-3-Clause",
|
||||
wantID: "BSD-3-Clause",
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "URL with trailing whitespace",
|
||||
url: " http://opensource.org/licenses/MIT ",
|
||||
wantID: "MIT",
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "Unknown URL",
|
||||
url: "https://example.com/unknown-license",
|
||||
wantID: "",
|
||||
wantFound: false,
|
||||
},
|
||||
{
|
||||
name: "Empty URL",
|
||||
url: "",
|
||||
wantID: "",
|
||||
wantFound: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
info, found := LicenseByURL(tt.url)
|
||||
if found != tt.wantFound {
|
||||
t.Errorf("LicenseByURL() found = %v, want %v", found, tt.wantFound)
|
||||
}
|
||||
if found {
|
||||
if info.ID != tt.wantID {
|
||||
t.Errorf("LicenseByURL() ID = %v, want %v", info.ID, tt.wantID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLicenseByURL_DeprecatedLicenses(t *testing.T) {
|
||||
// Test that deprecated license URLs map to their replacement licenses
|
||||
// For example, GPL-2.0+ should map to GPL-2.0-or-later
|
||||
|
||||
// This test needs actual URLs from deprecated licenses
|
||||
// We can verify by checking if a deprecated license URL maps to a non-deprecated ID
|
||||
url := "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html"
|
||||
info, found := LicenseByURL(url)
|
||||
|
||||
if found {
|
||||
// Check that we got a valid non-deprecated license ID
|
||||
if info.ID == "" {
|
||||
t.Error("Got empty license ID for deprecated license URL")
|
||||
}
|
||||
// The ID should be the replacement (GPL-2.0-only or GPL-2.0-or-later)
|
||||
// depending on the URL
|
||||
t.Logf("Deprecated license URL mapped to: ID=%s", info.ID)
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
elementFormDefault="qualified"
|
||||
targetNamespace="http://cyclonedx.org/schema/spdx"
|
||||
version="1.0-3.24.0">
|
||||
version="1.0-3.26.0">
|
||||
|
||||
<xs:simpleType name="licenseId">
|
||||
<xs:restriction base="xs:string">
|
||||
@ -162,6 +162,11 @@
|
||||
<xs:documentation>Any OSI License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="any-OSI-perl-modules">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Any OSI License - Perl Modules</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Apache-1.0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Apache License 1.0</xs:documentation>
|
||||
@ -307,6 +312,11 @@
|
||||
<xs:documentation>Boehm-Demers-Weiser GC License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Boehm-GC-without-fee">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Boehm-Demers-Weiser GC License (without fee)</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Borceux">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Borceux license</xs:documentation>
|
||||
@ -812,6 +822,16 @@
|
||||
<xs:documentation>Creative Commons Public Domain Dedication and Certification</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="CC-PDM-1.0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Creative Commons Public Domain Mark 1.0 Universal</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="CC-SA-1.0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Creative Commons Share Alike 1.0 Generic</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="CC0-1.0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Creative Commons Zero v1.0 Universal</xs:documentation>
|
||||
@ -1062,6 +1082,21 @@
|
||||
<xs:documentation>DOC License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="DocBook-Schema">
|
||||
<xs:annotation>
|
||||
<xs:documentation>DocBook Schema License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="DocBook-Stylesheet">
|
||||
<xs:annotation>
|
||||
<xs:documentation>DocBook Stylesheet License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="DocBook-XML">
|
||||
<xs:annotation>
|
||||
<xs:documentation>DocBook XML License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Dotseqn">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Dotseqn License</xs:documentation>
|
||||
@ -1267,6 +1302,11 @@
|
||||
<xs:documentation>GD License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="generic-xts">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Generic XTS License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="GFDL-1.1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>GNU Free Documentation License v1.1</xs:documentation>
|
||||
@ -1527,6 +1567,11 @@
|
||||
<xs:documentation>hdparm License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="HIDAPI">
|
||||
<xs:annotation>
|
||||
<xs:documentation>HIDAPI License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Hippocratic-2.1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Hippocratic License 2.1</xs:documentation>
|
||||
@ -1617,6 +1662,11 @@
|
||||
<xs:documentation>Historical Permission Notice and Disclaimer with MIT disclaimer</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="HPND-Netrek">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Historical Permission Notice and Disclaimer - Netrek variant</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="HPND-Pbmplus">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Historical Permission Notice and Disclaimer - Pbmplus variant</xs:documentation>
|
||||
@ -1712,6 +1762,11 @@
|
||||
<xs:documentation>Inner Net License v2.0</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="InnoSetup">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Inno Setup License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Intel">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Intel Open Source License</xs:documentation>
|
||||
@ -2052,6 +2107,11 @@
|
||||
<xs:documentation>Minpack License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="MIPS">
|
||||
<xs:annotation>
|
||||
<xs:documentation>MIPS License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="MirOS">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The MirOS Licence</xs:documentation>
|
||||
@ -2072,6 +2132,11 @@
|
||||
<xs:documentation>Enlightenment License (e16)</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="MIT-Click">
|
||||
<xs:annotation>
|
||||
<xs:documentation>MIT Click License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="MIT-CMU">
|
||||
<xs:annotation>
|
||||
<xs:documentation>CMU License</xs:documentation>
|
||||
@ -2772,6 +2837,11 @@
|
||||
<xs:documentation>Ruby License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Ruby-pty">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Ruby pty extension license</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="SAX-PD">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Sax Public Domain Notice</xs:documentation>
|
||||
@ -2807,6 +2877,11 @@
|
||||
<xs:documentation>Sendmail License 8.23</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Sendmail-Open-Source-1.1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Sendmail Open Source License v1.1</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="SGI-B-1.0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>SGI Free Software License B v1.0</xs:documentation>
|
||||
@ -2867,6 +2942,11 @@
|
||||
<xs:documentation>Sleepycat License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="SMAIL-GPL">
|
||||
<xs:annotation>
|
||||
<xs:documentation>SMAIL General Public License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="SMLNJ">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Standard ML of New Jersey License</xs:documentation>
|
||||
@ -3007,6 +3087,11 @@
|
||||
<xs:documentation>Transitive Grace Period Public Licence 1.0</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="ThirdEye">
|
||||
<xs:annotation>
|
||||
<xs:documentation>ThirdEye License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="threeparttable">
|
||||
<xs:annotation>
|
||||
<xs:documentation>threeparttable License</xs:documentation>
|
||||
@ -3037,6 +3122,11 @@
|
||||
<xs:documentation>THOR Public License 1.0</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="TrustedQSL">
|
||||
<xs:annotation>
|
||||
<xs:documentation>TrustedQSL License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="TTWL">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Text-Tabs+Wrap License</xs:documentation>
|
||||
@ -3057,6 +3147,11 @@
|
||||
<xs:documentation>Technische Universitaet Berlin License 2.0</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Ubuntu-font-1.0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Ubuntu Font Licence v1.0</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="UCAR">
|
||||
<xs:annotation>
|
||||
<xs:documentation>UCAR License</xs:documentation>
|
||||
@ -3172,6 +3267,11 @@
|
||||
<xs:documentation>Do What The F*ck You Want To Public License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="wwl">
|
||||
<xs:annotation>
|
||||
<xs:documentation>WWL License</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="wxWindows">
|
||||
<xs:annotation>
|
||||
<xs:documentation>wxWindows Library License</xs:documentation>
|
||||
@ -3187,6 +3287,11 @@
|
||||
<xs:documentation>X11 License Distribution Modification Variant</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="X11-swapped">
|
||||
<xs:annotation>
|
||||
<xs:documentation>X11 swapped final paragraphs</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Xdebug-1.03">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Xdebug License v 1.03</xs:documentation>
|
||||
@ -3358,6 +3463,11 @@
|
||||
<xs:documentation>Bootloader Distribution Exception</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="CGAL-linking-exception">
|
||||
<xs:annotation>
|
||||
<xs:documentation>CGAL Linking Exception</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Classpath-exception-2.0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Classpath exception 2.0</xs:documentation>
|
||||
@ -3383,6 +3493,11 @@
|
||||
<xs:documentation>eCos exception 2.0</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="erlang-otp-linking-exception">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Erlang/OTP Linking Exception</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Fawkes-Runtime-exception">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Fawkes Runtime Exception</xs:documentation>
|
||||
@ -3425,7 +3540,7 @@
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Gmsh-exception">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Gmsh exception></xs:documentation>
|
||||
<xs:documentation>Gmsh exception</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="GNAT-exception">
|
||||
@ -3448,6 +3563,11 @@
|
||||
<xs:documentation>GNU JavaMail exception</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="GPL-3.0-389-ds-base-exception">
|
||||
<xs:annotation>
|
||||
<xs:documentation>GPL-3.0 389 DS Base Exception</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="GPL-3.0-interface-exception">
|
||||
<xs:annotation>
|
||||
<xs:documentation>GPL-3.0 Interface Exception</xs:documentation>
|
||||
@ -3478,11 +3598,21 @@
|
||||
<xs:documentation>GStreamer Exception (2008)</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="harbour-exception">
|
||||
<xs:annotation>
|
||||
<xs:documentation>harbour exception</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="i2p-gpl-java-exception">
|
||||
<xs:annotation>
|
||||
<xs:documentation>i2p GPL+Java Exception</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Independent-modules-exception">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Independent Module Linking exception</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="KiCad-libraries-exception">
|
||||
<xs:annotation>
|
||||
<xs:documentation>KiCad Libraries Exception</xs:documentation>
|
||||
@ -3528,6 +3658,11 @@
|
||||
<xs:documentation>Macros and Inline Functions Exception</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="mxml-exception">
|
||||
<xs:annotation>
|
||||
<xs:documentation>mxml Exception</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Nokia-Qt-exception-1.1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Nokia Qt LGPL exception 1.1</xs:documentation>
|
||||
@ -3583,6 +3718,11 @@
|
||||
<xs:documentation>Qwt exception 1.0</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="romic-exception">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Romic Exception</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="RRDtool-FLOSS-exception-2.0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>RRDtool FLOSS exception 2.0</xs:documentation>
|
||||
|
||||
@ -62,6 +62,9 @@ func decodeLicenses(c *cyclonedx.Component) []pkg.License {
|
||||
licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), l.License.ID, l.License.URL))
|
||||
case l.License != nil && l.License.Name != "":
|
||||
licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), l.License.Name, l.License.URL))
|
||||
case l.License != nil && l.License.URL != "":
|
||||
// Try to enrich license from URL when ID and Name are empty
|
||||
licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), "", l.License.URL))
|
||||
case l.Expression != "":
|
||||
licenses = append(licenses, pkg.NewLicenseWithContext(context.TODO(), l.Expression))
|
||||
default:
|
||||
@ -163,10 +166,18 @@ func processCustomLicense(l pkg.License) cyclonedx.Licenses {
|
||||
func processLicenseURLs(l pkg.License, spdxID string, populate *cyclonedx.Licenses) {
|
||||
for _, url := range l.URLs {
|
||||
if spdxID == "" {
|
||||
// CycloneDX requires either an id or name to be present for a license
|
||||
// If l.Value is empty, use the URL as the name to ensure schema compliance
|
||||
// at this point we've already tried to enrich the license we just don't want the format
|
||||
// conversion to be lossy here
|
||||
name := l.Value
|
||||
if name == "" {
|
||||
name = url
|
||||
}
|
||||
*populate = append(*populate, cyclonedx.LicenseChoice{
|
||||
License: &cyclonedx.License{
|
||||
URL: url,
|
||||
Name: l.Value,
|
||||
Name: name,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
|
||||
@ -121,6 +121,22 @@ func Test_encodeLicense(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with URL only and no name or SPDX ID",
|
||||
input: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicenseFromURLsWithContext(ctx, "", "http://jaxen.codehaus.org/license.html"),
|
||||
),
|
||||
},
|
||||
expected: &cyclonedx.Licenses{
|
||||
{
|
||||
License: &cyclonedx.License{
|
||||
Name: "http://jaxen.codehaus.org/license.html",
|
||||
URL: "http://jaxen.codehaus.org/license.html",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with multiple values licenses are deduplicated",
|
||||
input: pkg.Package{
|
||||
|
||||
@ -4,6 +4,7 @@ package license
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/github/go-spdx/v2/spdxexp"
|
||||
|
||||
@ -17,6 +18,18 @@ const (
|
||||
Concluded Type = "concluded"
|
||||
)
|
||||
|
||||
// trimFileSuffix removes common file extensions from the end of a string
|
||||
func trimFileSuffix(s string) string {
|
||||
suffixes := []string{".txt", ".pdf", ".html", ".htm", ".md", ".markdown", ".rst", ".doc", ".docx", ".rtf", ".tex", ".xml", ".json"}
|
||||
lower := strings.ToLower(s)
|
||||
for _, suffix := range suffixes {
|
||||
if strings.HasSuffix(lower, suffix) {
|
||||
return s[:len(s)-len(suffix)]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func ParseExpression(expression string) (ex string, err error) {
|
||||
// https://github.com/anchore/syft/issues/1837
|
||||
// The current spdx library can panic when parsing some expressions
|
||||
@ -28,10 +41,26 @@ func ParseExpression(expression string) (ex string, err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
// Try with the original expression first
|
||||
licenseID, exists := spdxlicense.ID(expression)
|
||||
if exists {
|
||||
return licenseID, nil
|
||||
}
|
||||
|
||||
// Check if the expression is a URL and try to look it up
|
||||
if info, found := spdxlicense.LicenseByURL(expression); found {
|
||||
return info.ID, nil
|
||||
}
|
||||
|
||||
// Try with trimmed file suffix
|
||||
trimmed := trimFileSuffix(expression)
|
||||
if trimmed != expression {
|
||||
// Try as a URL with the trimmed version
|
||||
if info, found := spdxlicense.LicenseByURL(trimmed); found {
|
||||
return info.ID, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If it doesn't exist initially in the SPDX list it might be a more complex expression
|
||||
// ignored variable is any invalid expressions
|
||||
// TODO: contribute to spdxexp to expose deprecated license IDs
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/anchore/syft/internal/licenses"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/license"
|
||||
@ -132,6 +133,13 @@ func stripUnwantedCharacters(rawURL string) (string, error) {
|
||||
}
|
||||
|
||||
func NewLicenseFromFieldsWithContext(ctx context.Context, value, url string, location *file.Location) License {
|
||||
// If value is empty but URL is provided, try to enrich from SPDX database
|
||||
if value == "" && url != "" {
|
||||
if info, found := spdxlicense.LicenseByURL(url); found {
|
||||
value = info.ID
|
||||
}
|
||||
}
|
||||
|
||||
lics := newLicenseBuilder().WithValues(value).WithURLs(url).WithOptionalLocation(location).Build(ctx).ToSlice()
|
||||
if len(lics) > 0 {
|
||||
return lics[0]
|
||||
@ -294,11 +302,22 @@ func (b *licenseBuilder) Build(ctx context.Context) LicenseSet {
|
||||
|
||||
if set.Empty() && len(b.urls) > 0 {
|
||||
// if we have no values or contents, but we do have URLs, let's make a license with the URLs
|
||||
set.Add(License{
|
||||
// try to enrich the license by looking up the URL in the SPDX database
|
||||
license := License{
|
||||
Type: b.tp,
|
||||
URLs: b.urls,
|
||||
Locations: locations,
|
||||
})
|
||||
}
|
||||
|
||||
// attempt to fill in missing license information from the first URL
|
||||
if len(b.urls) > 0 {
|
||||
if info, found := spdxlicense.LicenseByURL(b.urls[0]); found {
|
||||
license.Value = info.ID
|
||||
license.SPDXExpression = info.ID
|
||||
}
|
||||
}
|
||||
|
||||
set.Add(license)
|
||||
}
|
||||
|
||||
return set
|
||||
|
||||
180
syft/pkg/license_url_enrichment_test.go
Normal file
180
syft/pkg/license_url_enrichment_test.go
Normal file
@ -0,0 +1,180 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewLicenseFromFieldsWithContext_URLEnrichment(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
url string
|
||||
wantValue string
|
||||
wantHasURL bool
|
||||
}{
|
||||
{
|
||||
name: "Empty value with MIT URL should enrich",
|
||||
value: "",
|
||||
url: "http://opensource.org/licenses/MIT",
|
||||
wantValue: "MIT",
|
||||
wantHasURL: true,
|
||||
},
|
||||
{
|
||||
name: "Empty value with Apache URL should enrich",
|
||||
value: "",
|
||||
url: "https://www.apache.org/licenses/LICENSE-2.0",
|
||||
wantValue: "Apache-2.0",
|
||||
wantHasURL: true,
|
||||
},
|
||||
{
|
||||
name: "Non-empty value should not be overridden",
|
||||
value: "Custom-License",
|
||||
url: "http://opensource.org/licenses/MIT",
|
||||
wantValue: "Custom-License",
|
||||
wantHasURL: true,
|
||||
},
|
||||
{
|
||||
name: "Unknown URL should not enrich",
|
||||
value: "",
|
||||
url: "https://example.com/unknown-license",
|
||||
wantValue: "",
|
||||
wantHasURL: true,
|
||||
},
|
||||
{
|
||||
name: "Empty value and empty URL",
|
||||
value: "",
|
||||
url: "",
|
||||
wantValue: "",
|
||||
wantHasURL: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
license := NewLicenseFromFieldsWithContext(ctx, tt.value, tt.url, nil)
|
||||
|
||||
if license.Value != tt.wantValue {
|
||||
t.Errorf("NewLicenseFromFieldsWithContext() Value = %v, want %v", license.Value, tt.wantValue)
|
||||
}
|
||||
|
||||
hasURL := len(license.URLs) > 0
|
||||
if hasURL != tt.wantHasURL {
|
||||
t.Errorf("NewLicenseFromFieldsWithContext() has URL = %v, want %v", hasURL, tt.wantHasURL)
|
||||
}
|
||||
|
||||
if tt.wantHasURL && tt.url != "" && license.URLs[0] != tt.url {
|
||||
t.Errorf("NewLicenseFromFieldsWithContext() URL = %v, want %v", license.URLs[0], tt.url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLicenseBuilder_URLOnlyEnrichment(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
urls []string
|
||||
wantValue string
|
||||
wantSPDX string
|
||||
wantHasURL bool
|
||||
}{
|
||||
{
|
||||
name: "MIT URL only should enrich",
|
||||
urls: []string{"http://opensource.org/licenses/MIT"},
|
||||
wantValue: "MIT",
|
||||
wantSPDX: "MIT",
|
||||
wantHasURL: true,
|
||||
},
|
||||
{
|
||||
name: "Apache URL only should enrich",
|
||||
urls: []string{"https://www.apache.org/licenses/LICENSE-2.0"},
|
||||
wantValue: "Apache-2.0",
|
||||
wantSPDX: "Apache-2.0",
|
||||
wantHasURL: true,
|
||||
},
|
||||
{
|
||||
name: "Multiple URLs should use first",
|
||||
urls: []string{"http://opensource.org/licenses/MIT", "https://example.com/other"},
|
||||
wantValue: "MIT",
|
||||
wantSPDX: "MIT",
|
||||
wantHasURL: true,
|
||||
},
|
||||
{
|
||||
name: "Unknown URL should not enrich",
|
||||
urls: []string{"https://example.com/unknown-license"},
|
||||
wantValue: "",
|
||||
wantSPDX: "",
|
||||
wantHasURL: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
builder := newLicenseBuilder().WithURLs(tt.urls...)
|
||||
licenses := builder.Build(ctx).ToSlice()
|
||||
|
||||
if len(licenses) == 0 {
|
||||
t.Fatal("Expected at least one license")
|
||||
}
|
||||
|
||||
license := licenses[0]
|
||||
|
||||
if license.Value != tt.wantValue {
|
||||
t.Errorf("License Value = %v, want %v", license.Value, tt.wantValue)
|
||||
}
|
||||
|
||||
if license.SPDXExpression != tt.wantSPDX {
|
||||
t.Errorf("License SPDXExpression = %v, want %v", license.SPDXExpression, tt.wantSPDX)
|
||||
}
|
||||
|
||||
hasURL := len(license.URLs) > 0
|
||||
if hasURL != tt.wantHasURL {
|
||||
t.Errorf("License has URL = %v, want %v", hasURL, tt.wantHasURL)
|
||||
}
|
||||
|
||||
if tt.wantHasURL && len(tt.urls) > 0 {
|
||||
if len(license.URLs) != len(tt.urls) {
|
||||
t.Errorf("License URL count = %v, want %v", len(license.URLs), len(tt.urls))
|
||||
}
|
||||
if license.URLs[0] != tt.urls[0] {
|
||||
t.Errorf("License first URL = %v, want %v", license.URLs[0], tt.urls[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLicenseFromURLsWithContext_URLEnrichment(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
urls []string
|
||||
wantValue string
|
||||
}{
|
||||
{
|
||||
name: "Empty value with MIT URL should enrich via builder",
|
||||
value: "",
|
||||
urls: []string{"http://opensource.org/licenses/MIT"},
|
||||
wantValue: "MIT",
|
||||
},
|
||||
{
|
||||
name: "Non-empty value should not be changed",
|
||||
value: "Custom-License",
|
||||
urls: []string{"http://opensource.org/licenses/MIT"},
|
||||
wantValue: "Custom-License",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
license := NewLicenseFromURLsWithContext(ctx, tt.value, tt.urls...)
|
||||
|
||||
if license.Value != tt.wantValue {
|
||||
t.Errorf("NewLicenseFromURLsWithContext() Value = %v, want %v", license.Value, tt.wantValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user