mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +01:00
--------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
241 lines
6.5 KiB
Go
241 lines
6.5 KiB
Go
package helpers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"strings"
|
|
|
|
"github.com/CycloneDX/cyclonedx-go"
|
|
|
|
"github.com/anchore/syft/internal/licenses"
|
|
"github.com/anchore/syft/internal/spdxlicense"
|
|
"github.com/anchore/syft/syft/pkg"
|
|
)
|
|
|
|
// This should be a function that just surfaces licenses already validated in the package struct
|
|
func encodeLicenses(p pkg.Package) *cyclonedx.Licenses {
|
|
spdx, other, ex := separateLicenses(p)
|
|
out := spdx
|
|
out = append(out, other...)
|
|
|
|
if len(other) > 0 || len(spdx) > 0 {
|
|
// found non spdx related licenses
|
|
// build individual license choices for each
|
|
// complex expressions are not combined and set as NAME fields
|
|
for _, e := range ex {
|
|
if e == "" {
|
|
continue
|
|
}
|
|
out = append(out, cyclonedx.LicenseChoice{
|
|
License: &cyclonedx.License{
|
|
Name: e,
|
|
},
|
|
})
|
|
}
|
|
} else if len(ex) > 0 {
|
|
// only expressions found
|
|
e := mergeSPDX(ex)
|
|
if e != "" {
|
|
out = append(out, cyclonedx.LicenseChoice{
|
|
Expression: e,
|
|
})
|
|
}
|
|
}
|
|
|
|
if len(out) > 0 {
|
|
return &out
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func decodeLicenses(c *cyclonedx.Component) []pkg.License {
|
|
licenses := make([]pkg.License, 0)
|
|
if c == nil || c.Licenses == nil {
|
|
return licenses
|
|
}
|
|
|
|
for _, l := range *c.Licenses {
|
|
// these fields are mutually exclusive in the spec
|
|
switch {
|
|
case l.License != nil && l.License.ID != "":
|
|
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:
|
|
}
|
|
}
|
|
|
|
return licenses
|
|
}
|
|
|
|
func separateLicenses(p pkg.Package) (spdx, other cyclonedx.Licenses, expressions []string) {
|
|
ex := make([]string, 0)
|
|
spdxc := cyclonedx.Licenses{}
|
|
otherc := cyclonedx.Licenses{}
|
|
/*
|
|
pkg.License can be a couple of things: see above declarations
|
|
- Complex SPDX expression
|
|
- Some other Valid license ID
|
|
- Some non-standard non spdx license
|
|
|
|
To determine if an expression is a singular ID we first run it against the SPDX license list.
|
|
|
|
The weird case we run into is if there is a package with a license that is not a valid SPDX expression
|
|
and a license that is a valid complex expression. In this case we will surface the valid complex expression
|
|
as a license choice and the invalid expression as a license string.
|
|
|
|
*/
|
|
seen := make(map[string]bool)
|
|
for _, l := range p.Licenses.ToSlice() {
|
|
// singular expression case
|
|
// only ID field here since we guarantee that the license is valid
|
|
if value, exists := spdxlicense.ID(l.SPDXExpression); exists {
|
|
if len(l.URLs) > 0 {
|
|
processLicenseURLs(l, value, &spdxc)
|
|
continue
|
|
}
|
|
|
|
if _, exists := seen[value]; exists {
|
|
continue
|
|
}
|
|
// try making set of license choices to avoid duplicates
|
|
// only update if the license has more information
|
|
spdxc = append(spdxc, cyclonedx.LicenseChoice{
|
|
License: &cyclonedx.License{
|
|
ID: value,
|
|
},
|
|
})
|
|
seen[value] = true
|
|
// we have added the license to the SPDX license list check next license
|
|
continue
|
|
}
|
|
|
|
if l.SPDXExpression != "" && !strings.HasPrefix(l.SPDXExpression, licenses.UnknownLicensePrefix) {
|
|
// COMPLEX EXPRESSION CASE
|
|
ex = append(ex, l.SPDXExpression)
|
|
continue
|
|
}
|
|
|
|
// license string that are not valid spdx expressions or ids
|
|
// we only use license Name here since we cannot guarantee that the license is a valid SPDX expression
|
|
if len(l.URLs) > 0 && !strings.HasPrefix(l.SPDXExpression, licenses.UnknownLicensePrefix) {
|
|
processLicenseURLs(l, "", &otherc)
|
|
continue
|
|
}
|
|
|
|
otherc = append(otherc, processCustomLicense(l)...)
|
|
}
|
|
return spdxc, otherc, ex
|
|
}
|
|
|
|
func processCustomLicense(l pkg.License) cyclonedx.Licenses {
|
|
result := cyclonedx.Licenses{}
|
|
if strings.HasPrefix(l.SPDXExpression, licenses.UnknownLicensePrefix) {
|
|
cyclonedxLicense := &cyclonedx.License{
|
|
Name: l.SPDXExpression,
|
|
}
|
|
if len(l.URLs) > 0 {
|
|
cyclonedxLicense.URL = l.URLs[0]
|
|
}
|
|
if len(l.Contents) > 0 {
|
|
cyclonedxLicense.Text = &cyclonedx.AttachedText{
|
|
Content: base64.StdEncoding.EncodeToString([]byte(l.Contents)),
|
|
}
|
|
cyclonedxLicense.Text.ContentType = "text/plain"
|
|
cyclonedxLicense.Text.Encoding = "base64"
|
|
}
|
|
result = append(result, cyclonedx.LicenseChoice{
|
|
License: cyclonedxLicense,
|
|
})
|
|
} else {
|
|
result = append(result, cyclonedx.LicenseChoice{
|
|
License: &cyclonedx.License{
|
|
Name: l.Value,
|
|
},
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
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: name,
|
|
},
|
|
})
|
|
} else {
|
|
*populate = append(*populate, cyclonedx.LicenseChoice{
|
|
License: &cyclonedx.License{
|
|
ID: spdxID,
|
|
URL: url,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func mergeSPDX(ex []string) string {
|
|
var candidate []string
|
|
for _, e := range ex {
|
|
// if the expression does not have balanced parens add them
|
|
if !strings.HasPrefix(e, "(") && !strings.HasSuffix(e, ")") {
|
|
e = "(" + e + ")"
|
|
}
|
|
candidate = append(candidate, e)
|
|
}
|
|
|
|
if len(candidate) == 1 {
|
|
return reduceOuter(candidate[0])
|
|
}
|
|
|
|
return reduceOuter(strings.Join(candidate, " AND "))
|
|
}
|
|
|
|
func reduceOuter(expression string) string {
|
|
expression = strings.TrimSpace(expression)
|
|
|
|
// If the entire expression is wrapped in parentheses, check if they are redundant.
|
|
if strings.HasPrefix(expression, "(") && strings.HasSuffix(expression, ")") {
|
|
trimmed := expression[1 : len(expression)-1]
|
|
if isBalanced(trimmed) {
|
|
return reduceOuter(trimmed) // Recursively reduce the trimmed expression.
|
|
}
|
|
}
|
|
|
|
return expression
|
|
}
|
|
|
|
func isBalanced(expression string) bool {
|
|
count := 0
|
|
for _, c := range expression {
|
|
switch c {
|
|
case '(':
|
|
count++
|
|
case ')':
|
|
count--
|
|
if count < 0 {
|
|
return false
|
|
}
|
|
default:
|
|
}
|
|
}
|
|
return count == 0
|
|
}
|