Skip to content

Commit aec8885

Browse files
authored
fix(spdx): use the hasExtractedLicensingInfos field for licenses that are not listed in the SPDX (#8077)
1 parent 715575d commit aec8885

11 files changed

Lines changed: 637 additions & 104 deletions

File tree

.github/workflows/spdx-cron.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: SPDX licenses cron
2+
on:
3+
schedule:
4+
- cron: '0 0 * * 0' # every Sunday at 00:00
5+
workflow_dispatch:
6+
7+
jobs:
8+
build:
9+
name: Check if SPDX exceptions
10+
runs-on: ubuntu-24.04
11+
steps:
12+
- name: Check out code
13+
uses: actions/checkout@v4.1.6
14+
15+
- name: Check if SPDX exceptions are up-to-date
16+
run: |
17+
mage spdx:updateLicenseExceptions
18+
if [ -n "$(git status --porcelain)" ]; then
19+
echo "Run 'mage spdx:updateLicenseExceptions' and push it"
20+
exit 1
21+
fi
22+
23+
- name: Microsoft Teams Notification
24+
## Until the PR with the fix for the AdaptivCard version is merged yet
25+
## https://github.com/Skitionek/notify-microsoft-teams/pull/96
26+
## Use the aquasecurity fork
27+
uses: aquasecurity/notify-microsoft-teams@master
28+
if: failure()
29+
with:
30+
webhook_url: ${{ secrets.TRIVY_MSTEAMS_WEBHOOK }}
31+
needs: ${{ toJson(needs) }}
32+
job: ${{ toJson(job) }}
33+
steps: ${{ toJson(steps) }}

magefiles/magefile.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,3 +533,10 @@ type Helm mg.Namespace
533533
func (Helm) UpdateVersion() error {
534534
return sh.RunWith(ENV, "go", "run", "-tags=mage_helm", "./magefiles")
535535
}
536+
537+
type SPDX mg.Namespace
538+
539+
// UpdateLicenseExceptions updates 'exception.json' with SPDX license exceptions
540+
func (SPDX) UpdateLicenseExceptions() error {
541+
return sh.RunWith(ENV, "go", "run", "-tags=mage_spdx", "./magefiles/spdx.go")
542+
}

magefiles/spdx.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//go:build mage_spdx
2+
3+
package main
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"os"
9+
"path/filepath"
10+
"sort"
11+
12+
"github.com/samber/lo"
13+
"golang.org/x/xerrors"
14+
15+
"github.com/aquasecurity/trivy/pkg/downloader"
16+
"github.com/aquasecurity/trivy/pkg/log"
17+
)
18+
19+
const (
20+
exceptionFileName = "exceptions.json"
21+
exceptionDir = "./pkg/licensing/expression"
22+
exceptionURL = "https://spdx.org/licenses/exceptions.json"
23+
)
24+
25+
type Exceptions struct {
26+
Exceptions []Exception `json:"exceptions"`
27+
}
28+
29+
type Exception struct {
30+
ID string `json:"licenseExceptionId"`
31+
}
32+
33+
func main() {
34+
if err := run(); err != nil {
35+
log.Fatal("Fatal error", log.Err(err))
36+
}
37+
38+
}
39+
40+
// run downloads exceptions.json file, takes only IDs and saves into `expression` package.
41+
func run() error {
42+
tmpDir, err := downloader.DownloadToTempDir(context.Background(), exceptionURL, downloader.Options{})
43+
if err != nil {
44+
return xerrors.Errorf("unable to download exceptions.json file: %w", err)
45+
}
46+
tmpFile, err := os.ReadFile(filepath.Join(tmpDir, exceptionFileName))
47+
if err != nil {
48+
return xerrors.Errorf("unable to read exceptions.json file: %w", err)
49+
}
50+
51+
exceptions := Exceptions{}
52+
if err = json.Unmarshal(tmpFile, &exceptions); err != nil {
53+
return xerrors.Errorf("unable to unmarshal exceptions.json file: %w", err)
54+
}
55+
56+
exs := lo.Map(exceptions.Exceptions, func(ex Exception, _ int) string {
57+
return ex.ID
58+
})
59+
sort.Strings(exs)
60+
61+
exceptionFile := filepath.Join(exceptionDir, exceptionFileName)
62+
f, err := os.Create(exceptionFile)
63+
if err != nil {
64+
return xerrors.Errorf("unable to create file %s: %w", exceptionFile, err)
65+
}
66+
defer f.Close()
67+
68+
e, err := json.Marshal(exs)
69+
if err != nil {
70+
return xerrors.Errorf("unable to marshal exceptions list: %w", err)
71+
}
72+
73+
if _, err = f.Write(e); err != nil {
74+
return xerrors.Errorf("unable to write exceptions list: %w", err)
75+
}
76+
77+
return nil
78+
}

pkg/licensing/expression/category.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
package expression
22

3+
import (
4+
"encoding/json"
5+
"strings"
6+
"sync"
7+
8+
"github.com/samber/lo"
9+
10+
"github.com/aquasecurity/trivy/pkg/log"
11+
"github.com/aquasecurity/trivy/pkg/set"
12+
13+
_ "embed"
14+
)
15+
316
// Canonical names of the licenses.
417
// ported from https://github.com/google/licenseclassifier/blob/7c62d6fe8d3aa2f39c4affb58c9781d9dc951a2d/license_type.go#L24-L177
518
const (
@@ -359,3 +372,70 @@ var (
359372
ZeroBSD,
360373
}
361374
)
375+
376+
var spdxLicenses = set.New[string]()
377+
378+
var initSpdxLicenses = sync.OnceFunc(func() {
379+
if spdxLicenses.Size() > 0 {
380+
return
381+
}
382+
383+
licenseSlices := [][]string{
384+
ForbiddenLicenses,
385+
RestrictedLicenses,
386+
ReciprocalLicenses,
387+
NoticeLicenses,
388+
PermissiveLicenses,
389+
UnencumberedLicenses,
390+
}
391+
392+
for _, licenseSlice := range licenseSlices {
393+
spdxLicenses.Append(licenseSlice...)
394+
}
395+
396+
// Save GNU licenses with "-or-later" and `"-only" suffixes
397+
for _, l := range GnuLicenses {
398+
license := SimpleExpr{
399+
License: l,
400+
}
401+
spdxLicenses.Append(license.String())
402+
403+
license.HasPlus = true
404+
spdxLicenses.Append(license.String())
405+
}
406+
})
407+
408+
//go:embed exceptions.json
409+
var exceptions []byte
410+
411+
var spdxExceptions map[string]SimpleExpr
412+
413+
var initSpdxExceptions = sync.OnceFunc(func() {
414+
if len(spdxExceptions) > 0 {
415+
return
416+
}
417+
418+
var exs []string
419+
if err := json.Unmarshal(exceptions, &exs); err != nil {
420+
log.WithPrefix(log.PrefixSPDX).Warn("Unable to parse SPDX exception file", log.Err(err))
421+
return
422+
}
423+
spdxExceptions = lo.SliceToMap(exs, func(exception string) (string, SimpleExpr) {
424+
return strings.ToUpper(exception), SimpleExpr{License: exception}
425+
})
426+
})
427+
428+
// ValidateSPDXLicense returns true if SPDX license list contain licenseID
429+
func ValidateSPDXLicense(license string) bool {
430+
initSpdxLicenses()
431+
432+
return spdxLicenses.Contains(license)
433+
}
434+
435+
// ValidateSPDXException returns true if SPDX exception list contain exceptionID
436+
func ValidateSPDXException(exception string) bool {
437+
initSpdxExceptions()
438+
439+
_, ok := spdxExceptions[strings.ToUpper(exception)]
440+
return ok
441+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
["389-exception","Asterisk-exception","Asterisk-linking-protocols-exception","Autoconf-exception-2.0","Autoconf-exception-3.0","Autoconf-exception-generic","Autoconf-exception-generic-3.0","Autoconf-exception-macro","Bison-exception-1.24","Bison-exception-2.2","Bootloader-exception","CGAL-linking-exception","CLISP-exception-2.0","Classpath-exception-2.0","DigiRule-FOSS-exception","FLTK-exception","Fawkes-Runtime-exception","Font-exception-2.0","GCC-exception-2.0","GCC-exception-2.0-note","GCC-exception-3.1","GNAT-exception","GNOME-examples-exception","GNU-compiler-exception","GPL-3.0-389-ds-base-exception","GPL-3.0-interface-exception","GPL-3.0-linking-exception","GPL-3.0-linking-source-exception","GPL-CC-1.0","GStreamer-exception-2005","GStreamer-exception-2008","Gmsh-exception","Independent-modules-exception","KiCad-libraries-exception","LGPL-3.0-linking-exception","LLGPL","LLVM-exception","LZMA-exception","Libtool-exception","Linux-syscall-note","Nokia-Qt-exception-1.1","OCCT-exception-1.0","OCaml-LGPL-linking-exception","OpenJDK-assembly-exception-1.0","PCRE2-exception","PS-or-PDF-font-exception-20170817","QPL-1.0-INRIA-2004-exception","Qt-GPL-exception-1.0","Qt-LGPL-exception-1.1","Qwt-exception-1.0","RRDtool-FLOSS-exception-2.0","SANE-exception","SHL-2.0","SHL-2.1","SWI-exception","Swift-exception","Texinfo-exception","UBDL-exception","Universal-FOSS-exception-1.0","WxWindows-exception-3.1","cryptsetup-OpenSSL-exception","eCos-exception-2.0","erlang-otp-linking-exception","fmt-exception","freertos-exception-2.0","gnu-javamail-exception","harbour-exception","i2p-gpl-java-exception","libpri-OpenH323-exception","mif-exception","mxml-exception","openvpn-openssl-exception","romic-exception","stunnel-exception","u-boot-exception-2.0","vsftpd-openssl-exception","x11vnc-openssl-exception"]

pkg/licensing/expression/expression.go

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,26 +59,33 @@ func normalize(expr Expression, fn NormalizeFunc) Expression {
5959
// There MUST be white space on either side of the operator "WITH".
6060
// ref: https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions
6161
func NormalizeForSPDX(expr Expression) Expression {
62-
e, ok := expr.(SimpleExpr)
63-
if !ok {
64-
return expr // do not normalize compound expressions
65-
}
66-
67-
var b strings.Builder
68-
for _, c := range e.License {
69-
switch {
70-
// spec: idstring = 1*(ALPHA / DIGIT / "-" / "." )
71-
case isAlphabet(c) || unicode.IsNumber(c) || c == '-' || c == '.':
72-
_, _ = b.WriteRune(c)
73-
case c == ':':
74-
// TODO: Support DocumentRef
75-
_, _ = b.WriteRune(c)
76-
default:
77-
// Replace invalid characters with '-'
78-
_, _ = b.WriteRune('-')
62+
switch e := expr.(type) {
63+
case SimpleExpr:
64+
var b strings.Builder
65+
for _, c := range e.License {
66+
switch {
67+
// spec: idstring = 1*(ALPHA / DIGIT / "-" / "." )
68+
case isAlphabet(c) || unicode.IsNumber(c) || c == '-' || c == '.':
69+
_, _ = b.WriteRune(c)
70+
case c == ':':
71+
// TODO: Support DocumentRef
72+
_, _ = b.WriteRune(c)
73+
default:
74+
// Replace invalid characters with '-'
75+
_, _ = b.WriteRune('-')
76+
}
77+
}
78+
return SimpleExpr{License: b.String(), HasPlus: e.HasPlus}
79+
case CompoundExpr:
80+
if e.Conjunction() == TokenWith {
81+
initSpdxExceptions()
82+
// Use correct SPDX exceptionID
83+
if exc, ok := spdxExceptions[strings.ToUpper(e.Right().String())]; ok {
84+
return NewCompoundExpr(e.Left(), e.Conjunction(), exc)
85+
}
7986
}
8087
}
81-
return SimpleExpr{License: b.String(), HasPlus: e.HasPlus}
88+
return expr
8289
}
8390

8491
func isAlphabet(r rune) bool {

pkg/licensing/expression/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ func (c CompoundExpr) Conjunction() Token {
5656
return c.conjunction
5757
}
5858

59+
func (c CompoundExpr) Left() Expression {
60+
return c.left
61+
}
62+
63+
func (c CompoundExpr) Right() Expression {
64+
return c.right
65+
}
66+
5967
func (c CompoundExpr) String() string {
6068
left := c.left.String()
6169
if l, ok := c.left.(CompoundExpr); ok {

pkg/log/logger.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const (
2525
PrefixLicense = "license"
2626
PrefixVulnerabilityDB = "vulndb"
2727
PrefixJavaDB = "javadb"
28+
PrefixSPDX = "spdx"
2829
)
2930

3031
// Logger is an alias of slog.Logger

0 commit comments

Comments
 (0)