feat(sbom): use SPDX license IDs list to validate SPDX IDs (#9569)

This commit is contained in:
DmitriyLewen
2025-10-14 12:58:55 +06:00
committed by GitHub
parent b885d3a369
commit 35db88c81c
5 changed files with 89 additions and 60 deletions

View File

@@ -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

View File

@@ -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")
} }

View File

@@ -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
} }

View File

@@ -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

File diff suppressed because one or more lines are too long