mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 07:40:48 -08:00
feat(sbom): use SPDX license IDs list to validate SPDX IDs (#9569)
This commit is contained in:
6
.github/workflows/spdx-cron.yaml
vendored
6
.github/workflows/spdx-cron.yaml
vendored
@@ -20,12 +20,12 @@ jobs:
|
|||||||
- name: Install Go tools
|
- name: Install Go tools
|
||||||
run: go install tool # GOBIN is added to the PATH by the setup-go action
|
run: go install tool # GOBIN is added to the PATH by the setup-go action
|
||||||
|
|
||||||
- name: Check if SPDX exceptions are up-to-date
|
- name: Check if SPDX license IDs and exceptions are up-to-date
|
||||||
id: exceptions_check
|
id: exceptions_check
|
||||||
run: |
|
run: |
|
||||||
mage spdx:updateLicenseExceptions
|
mage spdx:updateLicenseEntries
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
echo "Run 'mage spdx:updateLicenseExceptions' and push it"
|
echo "Run 'mage spdx:updateLicenseEntries' and push it"
|
||||||
echo "send_notify=true" >> $GITHUB_OUTPUT
|
echo "send_notify=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -506,7 +506,7 @@ func (Helm) UpdateVersion() error {
|
|||||||
|
|
||||||
type SPDX mg.Namespace
|
type SPDX mg.Namespace
|
||||||
|
|
||||||
// UpdateLicenseExceptions updates 'exception.json' with SPDX license exceptions
|
// UpdateLicenseEntries updates both SPDX license IDs and exceptions
|
||||||
func (SPDX) UpdateLicenseExceptions() error {
|
func (SPDX) UpdateLicenseEntries() error {
|
||||||
return sh.RunWith(ENV, "go", "run", "-tags=mage_spdx", "./magefiles/spdx.go")
|
return sh.RunWith(ENV, "go", "run", "-tags=mage_spdx", "./magefiles/spdx.go")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
expressionDir = "./pkg/licensing/expression"
|
||||||
|
|
||||||
exceptionFileName = "exceptions.json"
|
exceptionFileName = "exceptions.json"
|
||||||
exceptionDir = "./pkg/licensing/expression"
|
|
||||||
exceptionURL = "https://spdx.org/licenses/exceptions.json"
|
exceptionURL = "https://spdx.org/licenses/exceptions.json"
|
||||||
|
|
||||||
|
licenseFileName = "licenses.json"
|
||||||
|
licenseURL = "https://spdx.org/licenses/licenses.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
log.Fatal("Fatal error", log.Err(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
type Exceptions struct {
|
type Exceptions struct {
|
||||||
Exceptions []Exception `json:"exceptions"`
|
Exceptions []Exception `json:"exceptions"`
|
||||||
}
|
}
|
||||||
@@ -30,49 +41,76 @@ type Exception struct {
|
|||||||
ID string `json:"licenseExceptionId"`
|
ID string `json:"licenseExceptionId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
// run downloads SPDX licenses and exceptions, extracts only IDs and writes flat arrays into the `expression` package.
|
||||||
if err := run(); err != nil {
|
func run() error {
|
||||||
log.Fatal("Fatal error", log.Err(err))
|
if err := updateLicenses(); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
return updateExceptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
// run downloads exceptions.json file, takes only IDs and saves into `expression` package.
|
func updateExceptions() error {
|
||||||
func run() error {
|
return fetchAndWrite(exceptionURL, exceptionFileName, filepath.Join(expressionDir, exceptionFileName), func(b []byte) ([]string, error) {
|
||||||
tmpDir, err := downloader.DownloadToTempDir(context.Background(), exceptionURL, downloader.Options{})
|
var exceptions Exceptions
|
||||||
if err != nil {
|
if err := json.Unmarshal(b, &exceptions); err != nil {
|
||||||
return xerrors.Errorf("unable to download exceptions.json file: %w", err)
|
return nil, xerrors.Errorf("unable to unmarshal exceptions.json file: %w", err)
|
||||||
}
|
}
|
||||||
tmpFile, err := os.ReadFile(filepath.Join(tmpDir, exceptionFileName))
|
exs := lo.Map(exceptions.Exceptions, func(ex Exception, _ int) string { return ex.ID })
|
||||||
if err != nil {
|
return exs, nil
|
||||||
return xerrors.Errorf("unable to read exceptions.json file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
exceptions := Exceptions{}
|
|
||||||
if err = json.Unmarshal(tmpFile, &exceptions); err != nil {
|
|
||||||
return xerrors.Errorf("unable to unmarshal exceptions.json file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
exs := lo.Map(exceptions.Exceptions, func(ex Exception, _ int) string {
|
|
||||||
return ex.ID
|
|
||||||
})
|
})
|
||||||
sort.Strings(exs)
|
}
|
||||||
|
|
||||||
exceptionFile := filepath.Join(exceptionDir, exceptionFileName)
|
type Licenses struct {
|
||||||
f, err := os.Create(exceptionFile)
|
Licenses []License `json:"licenses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type License struct {
|
||||||
|
ID string `json:"licenseId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLicenses() error {
|
||||||
|
return fetchAndWrite(licenseURL, licenseFileName, filepath.Join(expressionDir, licenseFileName), func(b []byte) ([]string, error) {
|
||||||
|
var licenses Licenses
|
||||||
|
if err := json.Unmarshal(b, &licenses); err != nil {
|
||||||
|
return nil, xerrors.Errorf("unable to unmarshal licenses.json file: %w", err)
|
||||||
|
}
|
||||||
|
ids := lo.Map(licenses.Licenses, func(l License, _ int) string { return l.ID })
|
||||||
|
return ids, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchAndWrite downloads a SPDX index file, extracts IDs using extractor, sorts and writes them to destPath
|
||||||
|
func fetchAndWrite(url, tmpFileName, destPath string, extractor func([]byte) ([]string, error)) error {
|
||||||
|
tmpDir, err := downloader.DownloadToTempDir(context.Background(), url, downloader.Options{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("unable to create file %s: %w", exceptionFile, err)
|
return xerrors.Errorf("unable to download %s: %w", tmpFileName, err)
|
||||||
|
}
|
||||||
|
tmpFile, err := os.ReadFile(filepath.Join(tmpDir, tmpFileName))
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("unable to read %s: %w", tmpFileName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, err := extractor(tmpFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sort.Strings(ids)
|
||||||
|
return writeIDs(destPath, ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeIDs(path string, ids []string) error {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("unable to create file %s: %w", path, err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
e, err := json.Marshal(exs)
|
b, err := json.Marshal(ids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("unable to marshal exceptions list: %w", err)
|
return xerrors.Errorf("unable to marshal id list: %w", err)
|
||||||
}
|
}
|
||||||
|
if _, err = f.Write(b); err != nil {
|
||||||
if _, err = f.Write(e); err != nil {
|
return xerrors.Errorf("unable to write id list: %w", err)
|
||||||
return xerrors.Errorf("unable to write exceptions list: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -375,34 +375,24 @@ var (
|
|||||||
|
|
||||||
var spdxLicenses = set.New[string]()
|
var spdxLicenses = set.New[string]()
|
||||||
|
|
||||||
|
//go:embed licenses.json
|
||||||
|
var licenses []byte
|
||||||
|
|
||||||
var initSpdxLicenses = sync.OnceFunc(func() {
|
var initSpdxLicenses = sync.OnceFunc(func() {
|
||||||
if spdxLicenses.Size() > 0 {
|
if spdxLicenses.Size() > 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
licenseSlices := [][]string{
|
var lics []string
|
||||||
ForbiddenLicenses,
|
if err := json.Unmarshal(licenses, &lics); err != nil {
|
||||||
RestrictedLicenses,
|
log.WithPrefix(log.PrefixSPDX).Warn("Unable to parse SPDX license file", log.Err(err))
|
||||||
ReciprocalLicenses,
|
return
|
||||||
NoticeLicenses,
|
|
||||||
PermissiveLicenses,
|
|
||||||
UnencumberedLicenses,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, licenseSlice := range licenseSlices {
|
// SPDX license list is case-insensitive. Store in upper case for simplicity.
|
||||||
spdxLicenses.Append(licenseSlice...)
|
spdxLicenses.Append(lo.Map(lics, func(l string, _ int) string {
|
||||||
}
|
return strings.ToUpper(l)
|
||||||
|
})...)
|
||||||
// Save GNU licenses with "-or-later" and `"-only" suffixes
|
|
||||||
for _, l := range GnuLicenses {
|
|
||||||
license := SimpleExpr{
|
|
||||||
License: l,
|
|
||||||
}
|
|
||||||
spdxLicenses.Append(license.String())
|
|
||||||
|
|
||||||
license.HasPlus = true
|
|
||||||
spdxLicenses.Append(license.String())
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
//go:embed exceptions.json
|
//go:embed exceptions.json
|
||||||
@@ -429,7 +419,7 @@ var initSpdxExceptions = sync.OnceFunc(func() {
|
|||||||
func ValidateSPDXLicense(license string) bool {
|
func ValidateSPDXLicense(license string) bool {
|
||||||
initSpdxLicenses()
|
initSpdxLicenses()
|
||||||
|
|
||||||
return spdxLicenses.Contains(license)
|
return spdxLicenses.Contains(strings.ToUpper(license))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateSPDXException returns true if SPDX exception list contain exceptionID
|
// ValidateSPDXException returns true if SPDX exception list contain exceptionID
|
||||||
|
|||||||
1
pkg/licensing/expression/licenses.json
Normal file
1
pkg/licensing/expression/licenses.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user