anchore-actions-token-generator[bot] f11377fe30
chore(deps): update tools to latest versions (#3775)
---------
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Co-authored-by: spiffcs <32073428+spiffcs@users.noreply.github.com>
2025-04-03 17:35:26 +00:00

229 lines
5.9 KiB
Go

package helpers
import (
"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.NewLicenseFromURLs(l.License.ID, l.License.URL))
case l.License != nil && l.License.Name != "":
licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.Name, l.License.URL))
case l.Expression != "":
licenses = append(licenses, pkg.NewLicense(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 == "" {
*populate = append(*populate, cyclonedx.LicenseChoice{
License: &cyclonedx.License{
URL: url,
Name: l.Value,
},
})
} 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
}