Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(spdx): use the hasExtractedLicensingInfos field for licenses that are not listed in the SPDX #8077

Merged
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6272fdd
feat: add spdx licenses and exceptions + validation
DmitriyLewen Dec 10, 2024
eb30b49
feat: use otherLicense for non-spdx licenses
DmitriyLewen Dec 10, 2024
b1081e2
test: add/update unit tests
DmitriyLewen Dec 10, 2024
420c15e
refactor: remove duplicates of otherLicenses
DmitriyLewen Dec 10, 2024
b6c1d28
refactor: always fill ExtractedText and LicenseName fields
DmitriyLewen Dec 10, 2024
1bac2ca
fix: tests
DmitriyLewen Dec 11, 2024
f851f9b
add comment
DmitriyLewen Dec 11, 2024
041ab21
feat: add `LicenseComment` field
DmitriyLewen Dec 12, 2024
659f992
refactor: ExtractedText field for license with name
DmitriyLewen Dec 13, 2024
c25d840
refactor: use exception list from spdx.org site
DmitriyLewen Dec 13, 2024
20275c3
test: update tests
DmitriyLewen Dec 13, 2024
c89c4e3
refactor: use `exceptions.json` file
DmitriyLewen Dec 17, 2024
784db9e
feat(mage): add command to create exceptions.json file
DmitriyLewen Dec 17, 2024
ffb5067
feat(licensing): add exceptions.json file
DmitriyLewen Dec 17, 2024
9f0f7bf
fix(mage): fix typo
DmitriyLewen Dec 17, 2024
c5563f5
ci: add spdx-cron
DmitriyLewen Dec 17, 2024
0d13108
ci: add aqua-installer
DmitriyLewen Dec 17, 2024
a2ebc32
fix: linter error
DmitriyLewen Dec 17, 2024
f9ea255
Merge branch 'main' of github.com:DmitriyLewen/trivy into fix/use-oth…
DmitriyLewen Jan 21, 2025
48a46b8
refactor: rename ValidSpdxLicense to ValidateSPDXLicense
DmitriyLewen Jan 21, 2025
ab86fd6
fix: remove duplicate step
DmitriyLewen Jan 21, 2025
8a96a8b
refactor: doesn't check `spdxLicenses` and `spdxExceptions` before init
DmitriyLewen Jan 21, 2025
82a52f1
refactor: use set.Set instead of map[string]struct{}
DmitriyLewen Jan 21, 2025
b38b6eb
refactor: use `spdx` lowercase prefix + use const
DmitriyLewen Jan 21, 2025
d8c4b83
refactor: logic for ValidateSPDXLicense
DmitriyLewen Jan 21, 2025
a8a85ad
refactor: add replaceOtherLicenses function
DmitriyLewen Jan 21, 2025
d4e67dc
refactor: use original spdx exception-id
DmitriyLewen Jan 22, 2025
5d0f7e1
refactor: normalize exceptions in NormalizeForSPDX
DmitriyLewen Jan 22, 2025
08ea0c8
fix: linter error
DmitriyLewen Jan 22, 2025
a9d0c64
refactor: validate exceptions for singleExpr
DmitriyLewen Jan 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/spdx-cron.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: SPDX licenses cron
on:
schedule:
- cron: '0 0 * * 0' # every Sunday at 00:00
workflow_dispatch:

jobs:
build:
name: Check if SPDX exceptions
runs-on: ubuntu-24.04
steps:
- name: Check out code
uses: actions/[email protected]

- name: Check if SPDX exceptions are up-to-date
run: |
mage spdx:updateLicenseExceptions
if [ -n "$(git status --porcelain)" ]; then
echo "Run 'mage spdx:updateLicenseExceptions' and push it"
exit 1
fi

- name: Check if SPDX exceptions are up-to-date
run: |
mage spdx:updateLicenseExceptions
if [ -n "$(git status --porcelain)" ]; then
echo "Run 'mage spdx:updateLicenseExceptions' and push it"
exit 1
fi

- name: Microsoft Teams Notification
## Until the PR with the fix for the AdaptivCard version is merged yet
## https://github.com/Skitionek/notify-microsoft-teams/pull/96
## Use the aquasecurity fork
uses: aquasecurity/notify-microsoft-teams@master
if: failure()
with:
webhook_url: ${{ secrets.TRIVY_MSTEAMS_WEBHOOK }}
needs: ${{ toJson(needs) }}
job: ${{ toJson(job) }}
steps: ${{ toJson(steps) }}
7 changes: 7 additions & 0 deletions magefiles/magefile.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,3 +540,10 @@ type Helm mg.Namespace
func (Helm) UpdateVersion() error {
return sh.RunWith(ENV, "go", "run", "-tags=mage_helm", "./magefiles")
}

type SPDX mg.Namespace

// UpdateLicenseExceptions updates 'exception.json' with SPDX license exceptions
func (SPDX) UpdateLicenseExceptions() error {
return sh.RunWith(ENV, "go", "run", "-tags=mage_spdx", "./magefiles/spdx.go")
}
79 changes: 79 additions & 0 deletions magefiles/spdx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//go:build mage_spdx

package main

import (
"context"
"encoding/json"
"os"
"path/filepath"
"sort"
"strings"

"github.com/samber/lo"
"golang.org/x/xerrors"

"github.com/aquasecurity/trivy/pkg/downloader"
"github.com/aquasecurity/trivy/pkg/log"
)

const (
exceptionFile = "exceptions.json"
exceptionDir = "./pkg/licensing/expression"
exceptionURL = "https://spdx.org/licenses/exceptions.json"
)

type Exceptions struct {
Exceptions []Exception `json:"exceptions"`
}

type Exception struct {
ID string `json:"licenseExceptionId"`
}

func main() {
if err := run(); err != nil {
log.Fatal("Fatal error", log.Err(err))
}

}

// run downloads exceptions.json file, takes only IDs and saves into `expression` package.
func run() error {
tmpDir, err := downloader.DownloadToTempDir(context.Background(), exceptionURL, downloader.Options{})
if err != nil {
return xerrors.Errorf("unable to download exceptions.json file: %w", err)
}
tmpFile, err := os.ReadFile(filepath.Join(tmpDir, exceptionFile))
if err != 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 strings.ToUpper(ex.ID)
})
sort.Strings(exs)

exceptionFile := filepath.Join(exceptionDir, exceptionFile)
f, err := os.Create(exceptionFile)
if err != nil {
return xerrors.Errorf("unable to create file %s: %w", exceptionFile, err)
}
defer f.Close()

e, err := json.Marshal(exs)
if err != nil {
return xerrors.Errorf("unable to marshal exceptions list: %w", err)
}

if _, err = f.Write(e); err != nil {
return xerrors.Errorf("unable to write exceptions list: %w", err)
}

return nil
}
88 changes: 88 additions & 0 deletions pkg/licensing/expression/category.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
package expression

import (
"encoding/json"
"strings"
"sync"

"github.com/samber/lo"

"github.com/aquasecurity/trivy/pkg/log"

_ "embed"
)

// Canonical names of the licenses.
// ported from https://github.com/google/licenseclassifier/blob/7c62d6fe8d3aa2f39c4affb58c9781d9dc951a2d/license_type.go#L24-L177
const (
Expand Down Expand Up @@ -359,3 +371,79 @@ var (
ZeroBSD,
}
)

var spdxLicenses map[string]struct{}
knqyf263 marked this conversation as resolved.
Show resolved Hide resolved

var initSpdxLicenses = sync.OnceFunc(func() {
if len(spdxLicenses) > 0 {
return
}

licenseSlices := [][]string{
ForbiddenLicenses,
RestrictedLicenses,
ReciprocalLicenses,
NoticeLicenses,
PermissiveLicenses,
UnencumberedLicenses,
}

for _, licenseSlice := range licenseSlices {
spdxLicenses = lo.Assign(spdxLicenses, lo.SliceToMap(licenseSlice, func(l string) (string, struct{}) {
return l, struct{}{}
}))
}

// Save GNU licenses with "-or-later" and `"-only" suffixes
for _, l := range GnuLicenses {
license := SimpleExpr{
License: l,
}
spdxLicenses[license.String()] = struct{}{}

license.HasPlus = true
spdxLicenses[license.String()] = struct{}{}
}
})

//go:embed exceptions.json
var exceptions []byte

var spdxExceptions map[string]struct{}
knqyf263 marked this conversation as resolved.
Show resolved Hide resolved

var initSpdxExceptions = sync.OnceFunc(func() {
if len(spdxExceptions) > 0 {
return
}

var exs []string
if err := json.Unmarshal(exceptions, &exs); err != nil {
log.WithPrefix("SPDX").Warn("Unable to parse SPDX exception file", log.Err(err))
knqyf263 marked this conversation as resolved.
Show resolved Hide resolved
return
}

spdxExceptions = lo.SliceToMap(exs, func(e string) (string, struct{}) {
return e, struct{}{}
})
})

// ValidSpdxLicense returns true if SPDX license lists contain licenseID and license exception (if exists)
func ValidSpdxLicense(license string) bool {
knqyf263 marked this conversation as resolved.
Show resolved Hide resolved
if spdxLicenses == nil {
initSpdxLicenses()
}
knqyf263 marked this conversation as resolved.
Show resolved Hide resolved
if spdxExceptions == nil {
initSpdxExceptions()
}
knqyf263 marked this conversation as resolved.
Show resolved Hide resolved

id, exception, ok := strings.Cut(license, " WITH ")
if _, licenseIdFound := spdxLicenses[id]; licenseIdFound {
if !ok {
return true
}
if _, exceptionFound := spdxExceptions[strings.ToUpper(exception)]; exceptionFound {
return true
}
}
return false
knqyf263 marked this conversation as resolved.
Show resolved Hide resolved
}
1 change: 1 addition & 0 deletions pkg/licensing/expression/exceptions.json
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I save and check exceptions in uppercase because we don't have normalization for exceptions like we do for licenses.

I suggest waiting for user feedback.
Maybe we need to update this logic (e.g. check in uppercase but save in original (from this file) case).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we need to update this logic (e.g. check in uppercase but save in original (from this file) case).

In the SPDX tools I maintain, I take the approach of saving in the original case but comparing ignoring case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @goneall

i updated logic in d4e67dc + 5d0f7e1

Original file line number Diff line number Diff line change
@@ -0,0 +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","CLASSPATH-EXCEPTION-2.0","CLISP-EXCEPTION-2.0","CRYPTSETUP-OPENSSL-EXCEPTION","DIGIRULE-FOSS-EXCEPTION","ECOS-EXCEPTION-2.0","ERLANG-OTP-LINKING-EXCEPTION","FAWKES-RUNTIME-EXCEPTION","FLTK-EXCEPTION","FMT-EXCEPTION","FONT-EXCEPTION-2.0","FREERTOS-EXCEPTION-2.0","GCC-EXCEPTION-2.0","GCC-EXCEPTION-2.0-NOTE","GCC-EXCEPTION-3.1","GMSH-EXCEPTION","GNAT-EXCEPTION","GNOME-EXAMPLES-EXCEPTION","GNU-COMPILER-EXCEPTION","GNU-JAVAMAIL-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","I2P-GPL-JAVA-EXCEPTION","KICAD-LIBRARIES-EXCEPTION","LGPL-3.0-LINKING-EXCEPTION","LIBPRI-OPENH323-EXCEPTION","LIBTOOL-EXCEPTION","LINUX-SYSCALL-NOTE","LLGPL","LLVM-EXCEPTION","LZMA-EXCEPTION","MIF-EXCEPTION","NOKIA-QT-EXCEPTION-1.1","OCAML-LGPL-LINKING-EXCEPTION","OCCT-EXCEPTION-1.0","OPENJDK-ASSEMBLY-EXCEPTION-1.0","OPENVPN-OPENSSL-EXCEPTION","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","ROMIC-EXCEPTION","RRDTOOL-FLOSS-EXCEPTION-2.0","SANE-EXCEPTION","SHL-2.0","SHL-2.1","STUNNEL-EXCEPTION","SWI-EXCEPTION","SWIFT-EXCEPTION","TEXINFO-EXCEPTION","U-BOOT-EXCEPTION-2.0","UBDL-EXCEPTION","UNIVERSAL-FOSS-EXCEPTION-1.0","VSFTPD-OPENSSL-EXCEPTION","WXWINDOWS-EXCEPTION-3.1","X11VNC-OPENSSL-EXCEPTION"]
Loading
Loading