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

This commit is contained in:
DmitriyLewen
2025-01-27 13:28:59 +06:00
committed by GitHub
parent 715575d731
commit aec8885bc7
11 changed files with 638 additions and 105 deletions

33
.github/workflows/spdx-cron.yaml vendored Normal file
View File

@@ -0,0 +1,33 @@
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/checkout@v4.1.6
- 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) }}

View File

@@ -533,3 +533,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")
}

78
magefiles/spdx.go Normal file
View File

@@ -0,0 +1,78 @@
//go:build mage_spdx
package main
import (
"context"
"encoding/json"
"os"
"path/filepath"
"sort"
"github.com/samber/lo"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/downloader"
"github.com/aquasecurity/trivy/pkg/log"
)
const (
exceptionFileName = "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, exceptionFileName))
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 ex.ID
})
sort.Strings(exs)
exceptionFile := filepath.Join(exceptionDir, exceptionFileName)
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
}

View File

@@ -1,5 +1,18 @@
package expression
import (
"encoding/json"
"strings"
"sync"
"github.com/samber/lo"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/set"
_ "embed"
)
// Canonical names of the licenses.
// ported from https://github.com/google/licenseclassifier/blob/7c62d6fe8d3aa2f39c4affb58c9781d9dc951a2d/license_type.go#L24-L177
const (
@@ -359,3 +372,70 @@ var (
ZeroBSD,
}
)
var spdxLicenses = set.New[string]()
var initSpdxLicenses = sync.OnceFunc(func() {
if spdxLicenses.Size() > 0 {
return
}
licenseSlices := [][]string{
ForbiddenLicenses,
RestrictedLicenses,
ReciprocalLicenses,
NoticeLicenses,
PermissiveLicenses,
UnencumberedLicenses,
}
for _, licenseSlice := range licenseSlices {
spdxLicenses.Append(licenseSlice...)
}
// 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
var exceptions []byte
var spdxExceptions map[string]SimpleExpr
var initSpdxExceptions = sync.OnceFunc(func() {
if len(spdxExceptions) > 0 {
return
}
var exs []string
if err := json.Unmarshal(exceptions, &exs); err != nil {
log.WithPrefix(log.PrefixSPDX).Warn("Unable to parse SPDX exception file", log.Err(err))
return
}
spdxExceptions = lo.SliceToMap(exs, func(exception string) (string, SimpleExpr) {
return strings.ToUpper(exception), SimpleExpr{License: exception}
})
})
// ValidateSPDXLicense returns true if SPDX license list contain licenseID
func ValidateSPDXLicense(license string) bool {
initSpdxLicenses()
return spdxLicenses.Contains(license)
}
// ValidateSPDXException returns true if SPDX exception list contain exceptionID
func ValidateSPDXException(exception string) bool {
initSpdxExceptions()
_, ok := spdxExceptions[strings.ToUpper(exception)]
return ok
}

View File

@@ -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","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"]

View File

@@ -59,26 +59,33 @@ func normalize(expr Expression, fn NormalizeFunc) Expression {
// There MUST be white space on either side of the operator "WITH".
// ref: https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions
func NormalizeForSPDX(expr Expression) Expression {
e, ok := expr.(SimpleExpr)
if !ok {
return expr // do not normalize compound expressions
}
var b strings.Builder
for _, c := range e.License {
switch {
// spec: idstring = 1*(ALPHA / DIGIT / "-" / "." )
case isAlphabet(c) || unicode.IsNumber(c) || c == '-' || c == '.':
_, _ = b.WriteRune(c)
case c == ':':
// TODO: Support DocumentRef
_, _ = b.WriteRune(c)
default:
// Replace invalid characters with '-'
_, _ = b.WriteRune('-')
switch e := expr.(type) {
case SimpleExpr:
var b strings.Builder
for _, c := range e.License {
switch {
// spec: idstring = 1*(ALPHA / DIGIT / "-" / "." )
case isAlphabet(c) || unicode.IsNumber(c) || c == '-' || c == '.':
_, _ = b.WriteRune(c)
case c == ':':
// TODO: Support DocumentRef
_, _ = b.WriteRune(c)
default:
// Replace invalid characters with '-'
_, _ = b.WriteRune('-')
}
}
return SimpleExpr{License: b.String(), HasPlus: e.HasPlus}
case CompoundExpr:
if e.Conjunction() == TokenWith {
initSpdxExceptions()
// Use correct SPDX exceptionID
if exc, ok := spdxExceptions[strings.ToUpper(e.Right().String())]; ok {
return NewCompoundExpr(e.Left(), e.Conjunction(), exc)
}
}
}
return SimpleExpr{License: b.String(), HasPlus: e.HasPlus}
return expr
}
func isAlphabet(r rune) bool {

View File

@@ -56,6 +56,14 @@ func (c CompoundExpr) Conjunction() Token {
return c.conjunction
}
func (c CompoundExpr) Left() Expression {
return c.left
}
func (c CompoundExpr) Right() Expression {
return c.right
}
func (c CompoundExpr) String() string {
left := c.left.String()
if l, ok := c.left.(CompoundExpr); ok {

View File

@@ -25,6 +25,7 @@ const (
PrefixLicense = "license"
PrefixVulnerabilityDB = "vulndb"
PrefixJavaDB = "javadb"
PrefixSPDX = "spdx"
)
// Logger is an alias of slog.Logger

View File

@@ -60,6 +60,8 @@ const (
ElementApplication = "Application"
ElementPackage = "Package"
ElementFile = "File"
LicenseRefPrefix = "LicenseRef"
)
var (
@@ -82,6 +84,7 @@ type Marshaler struct {
format spdx.Document
hasher Hash
appVersion string // Trivy version. It needed for `creator` field
logger *log.Logger
}
type Hash func(v any, format hashstructure.Format, opts *hashstructure.HashOptions) (uint64, error)
@@ -99,6 +102,7 @@ func NewMarshaler(version string, opts ...marshalOption) *Marshaler {
format: spdx.Document{},
hasher: hashstructure.Hash,
appVersion: version,
logger: log.WithPrefix(log.PrefixSPDX),
}
for _, opt := range opts {
@@ -145,6 +149,7 @@ func (m *Marshaler) Marshal(ctx context.Context, bom *core.BOM) (*spdx.Document,
packageIDs[root.ID()] = rootPkg.PackageSPDXIdentifier
var files []*spdx.File
var otherLicenses []*spdx.OtherLicense
for _, c := range bom.Components() {
if c.Root {
continue
@@ -165,6 +170,14 @@ func (m *Marshaler) Marshal(ctx context.Context, bom *core.BOM) (*spdx.Document,
packages = append(packages, &spdxPackage)
packageIDs[c.ID()] = spdxPackage.PackageSPDXIdentifier
// Fill licenses
license, others := m.spdxLicense(c)
// The Declared License is what the authors of a project believe govern the package
spdxPackage.PackageLicenseConcluded = license
// The Concluded License field is the license the SPDX file creator believes governs the package
spdxPackage.PackageLicenseDeclared = license
otherLicenses = append(otherLicenses, others...)
spdxFiles, err := m.spdxFiles(c)
if err != nil {
return nil, xerrors.Errorf("spdx files error: %w", err)
@@ -203,6 +216,7 @@ func (m *Marshaler) Marshal(ctx context.Context, bom *core.BOM) (*spdx.Document,
sortPackages(packages)
sortRelationships(relationShips)
sortFiles(files)
otherLicenses = sortOtherLicenses(otherLicenses)
return &spdx.Document{
SPDXVersion: spdx.Version,
@@ -226,6 +240,7 @@ func (m *Marshaler) Marshal(ctx context.Context, bom *core.BOM) (*spdx.Document,
Packages: packages,
Relationships: relationShips,
Files: files,
OtherLicenses: otherLicenses,
}, nil
}
@@ -249,7 +264,7 @@ func (m *Marshaler) rootSPDXPackage(root *core.Component, timeNow, pkgDownloadLo
externalReferences = append(externalReferences, m.purlExternalReference(root.PkgIdentifier.PURL.String()))
}
pkgID, err := calcPkgID(m.hasher, fmt.Sprintf("%s-%s", root.Name, root.Type))
pkgID, err := calcSPDXID(m.hasher, fmt.Sprintf("%s-%s", root.Name, root.Type))
if err != nil {
return nil, xerrors.Errorf("failed to get %s package ID: %w", pkgID, err)
}
@@ -301,12 +316,12 @@ func (m *Marshaler) advisoryExternalReference(primaryURL string) *spdx.PackageEx
}
func (m *Marshaler) spdxPackage(c *core.Component, timeNow, pkgDownloadLocation string) (spdx.Package, error) {
pkgID, err := calcPkgID(m.hasher, c)
pkgID, err := calcSPDXID(m.hasher, c)
if err != nil {
return spdx.Package{}, xerrors.Errorf("failed to get os metadata package ID: %w", err)
}
var elementType, purpose, license, sourceInfo string
var elementType, purpose, sourceInfo string
var supplier *spdx.Supplier
switch c.Type {
case core.TypeOS:
@@ -318,7 +333,9 @@ func (m *Marshaler) spdxPackage(c *core.Component, timeNow, pkgDownloadLocation
case core.TypeLibrary:
elementType = ElementPackage
purpose = PackagePurposeLibrary
license = m.spdxLicense(c)
// We need to create a new `LicesenRef-*` component for licenses that are not in the SPDX license list
// So we will fill licenses later
if c.SrcName != "" {
sourceInfo = fmt.Sprintf("%s: %s %s", SourcePackagePrefix, c.SrcName, c.SrcVersion)
@@ -360,12 +377,6 @@ func (m *Marshaler) spdxPackage(c *core.Component, timeNow, pkgDownloadLocation
PackageSourceInfo: sourceInfo,
PackageSupplier: supplier,
PackageChecksums: m.spdxChecksums(digests),
// The Declared License is what the authors of a project believe govern the package
PackageLicenseConcluded: license,
// The Concluded License field is the license the SPDX file creator believes governs the package
PackageLicenseDeclared: license,
}, nil
}
@@ -389,11 +400,94 @@ func (m *Marshaler) spdxAnnotations(c *core.Component, timeNow string) []spdx.An
return annotations
}
func (m *Marshaler) spdxLicense(c *core.Component) string {
if len(c.Licenses) == 0 {
return noAssertionField
func (m *Marshaler) spdxLicense(c *core.Component) (string, []*spdx.OtherLicense) {
// Only library components contain licenses
if c.Type != core.TypeLibrary {
return "", nil
}
return NormalizeLicense(c.Licenses)
if len(c.Licenses) == 0 {
return noAssertionField, nil
}
return m.normalizeLicenses(c.Licenses)
}
func (m *Marshaler) normalizeLicenses(licenses []string) (string, []*spdx.OtherLicense) {
var otherLicenses = make(map[string]*spdx.OtherLicense) // licenseID -> OtherLicense
license := strings.Join(lo.Map(licenses, func(license string, index int) string {
// e.g. GPL-3.0-with-autoconf-exception
license = strings.ReplaceAll(license, "-with-", " WITH ")
license = strings.ReplaceAll(license, "-WITH-", " WITH ")
return fmt.Sprintf("(%s)", license)
}), " AND ")
replaceOtherLicenses := func(expr expression.Expression) expression.Expression {
var licenseName string
var textLicense bool
switch e := expr.(type) {
case expression.SimpleExpr:
// Trim `text:--` prefix (expression.NormalizeForSPDX normalized `text://` prefix)
if strings.HasPrefix(e.License, "text:--") {
textLicense = true
e.License = strings.TrimPrefix(e.License, "text:--")
}
if expression.ValidateSPDXLicense(e.License) || expression.ValidateSPDXException(e.License) {
return e
}
licenseName = e.License
case expression.CompoundExpr:
// Check only CompoundExpr with `WITH` token as one license
if e.Conjunction() != expression.TokenWith {
return expr
}
// Check that license and exception are valid
if expression.ValidateSPDXLicense(e.Left().String()) && expression.ValidateSPDXException(e.Right().String()) {
// Use SimpleExpr for a valid SPDX license with an exception,
// to avoid parsing the license and exception separately.
return e
}
licenseName = e.String()
}
l := m.newOtherLicense(licenseName, textLicense)
otherLicenses[l.LicenseIdentifier] = l
return expression.SimpleExpr{License: l.LicenseIdentifier}
}
normalizedLicense, err := expression.Normalize(license, licensing.NormalizeLicense, expression.NormalizeForSPDX, replaceOtherLicenses)
if err != nil {
// Not fail on the invalid license
m.logger.Warn("Unable to marshal SPDX licenses", log.String("license", license))
return "", nil
}
return normalizedLicense, lo.Ternary(len(otherLicenses) > 0, lo.Values(otherLicenses), nil)
}
// newOtherLicense create new OtherLicense for license not included in the SPDX license list
func (m *Marshaler) newOtherLicense(license string, text bool) *spdx.OtherLicense {
otherLicense := spdx.OtherLicense{}
if text {
otherLicense.LicenseName = noAssertionField
otherLicense.ExtractedText = license
otherLicense.LicenseComment = "The license text represents text found in package metadata and may not represent the full text of the license"
} else {
otherLicense.LicenseName = license
otherLicense.ExtractedText = fmt.Sprintf("This component is licensed under %q", license)
}
licenseID, err := calcSPDXID(m.hasher, otherLicense)
if err != nil {
// This must be an unattainable case.
m.logger.Warn("Unable to calculate SPDX licenses ID", log.String("license", license), log.Err(err))
licenseID = license
}
otherLicense.LicenseIdentifier = LicenseRefPrefix + "-" + licenseID
return &otherLicense
}
func (m *Marshaler) spdxChecksums(digests []digest.Digest) []common.Checksum {
@@ -435,7 +529,7 @@ func (m *Marshaler) spdxFiles(c *core.Component) ([]*spdx.File, error) {
}
func (m *Marshaler) spdxFile(filePath string, digests []digest.Digest) (*spdx.File, error) {
pkgID, err := calcPkgID(m.hasher, filePath)
pkgID, err := calcSPDXID(m.hasher, filePath)
if err != nil {
return nil, xerrors.Errorf("failed to get %s package ID: %w", filePath, err)
}
@@ -505,6 +599,20 @@ func sortFiles(files []*spdx.File) {
})
}
// sortOtherLicenses removes duplicates and sorts result slice
func sortOtherLicenses(licenses []*spdx.OtherLicense) []*spdx.OtherLicense {
if len(licenses) == 0 {
return nil
}
licenses = lo.UniqBy(licenses, func(license *spdx.OtherLicense) string {
return license.LicenseIdentifier
})
sort.Slice(licenses, func(i, j int) bool {
return licenses[i].LicenseIdentifier < licenses[j].LicenseIdentifier
})
return licenses
}
func elementID(elementType, pkgID string) spdx.ElementID {
return spdx.ElementID(fmt.Sprintf("%s-%s", elementType, pkgID))
}
@@ -518,13 +626,13 @@ func getDocumentNamespace(root *core.Component) string {
)
}
func calcPkgID(h Hash, v any) (string, error) {
func calcSPDXID(h Hash, v any) (string, error) {
f, err := h(v, hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: true,
SlicesAsSets: true,
})
if err != nil {
return "", xerrors.Errorf("could not build package ID for %+v: %w", v, err)
return "", xerrors.Errorf("could not build component ID for %+v: %w", v, err)
}
return fmt.Sprintf("%x", f), nil
@@ -550,20 +658,3 @@ func camelCase(inputUnderScoreStr string) (camelCase string) {
}
return
}
func NormalizeLicense(licenses []string) string {
license := strings.Join(lo.Map(licenses, func(license string, index int) string {
// e.g. GPL-3.0-with-autoconf-exception
license = strings.ReplaceAll(license, "-with-", " WITH ")
license = strings.ReplaceAll(license, "-WITH-", " WITH ")
return fmt.Sprintf("(%s)", license)
}), " AND ")
s, err := expression.Normalize(license, licensing.NormalizeLicense, expression.NormalizeForSPDX)
if err != nil {
// Not fail on the invalid license
log.Warn("Unable to marshal SPDX licenses", log.String("license", license))
return ""
}
return s
}

View File

@@ -0,0 +1,136 @@
package spdx
import (
"sort"
"testing"
"github.com/spdx/tools-golang/spdx"
"github.com/stretchr/testify/assert"
)
func TestMarshaler_normalizeLicenses(t *testing.T) {
tests := []struct {
name string
input []string
wantLicenseName string
wantOtherLicenses []*spdx.OtherLicense
}{
{
name: "happy path",
input: []string{
"GPLv2+",
},
wantLicenseName: "GPL-2.0-or-later",
},
{
name: "happy path with multi license",
input: []string{
"GPLv2+",
"GPLv3+",
"BSD-4-Clause",
},
wantLicenseName: "GPL-2.0-or-later AND GPL-3.0-or-later AND BSD-4-Clause",
},
{
name: "happy path with OR operator",
input: []string{
"GPLv2+",
"LGPL 2.0 or GNU LESSER",
},
wantLicenseName: "GPL-2.0-or-later AND (LGPL-2.0-only OR LGPL-2.1-only)",
},
{
name: "happy path with OR operator with non-SPDX license",
input: []string{
"GPLv2+",
"wrong-license or unknown-license",
},
wantLicenseName: "GPL-2.0-or-later AND (LicenseRef-c581e42fe705aa48 OR LicenseRef-a0bb0951a6dfbdbe)",
wantOtherLicenses: []*spdx.OtherLicense{
{
LicenseIdentifier: "LicenseRef-a0bb0951a6dfbdbe",
LicenseName: "unknown-license",
ExtractedText: `This component is licensed under "unknown-license"`,
},
{
LicenseIdentifier: "LicenseRef-c581e42fe705aa48",
LicenseName: "wrong-license",
ExtractedText: `This component is licensed under "wrong-license"`,
},
},
},
{
name: "happy path with AND operator",
input: []string{
"GPLv2+",
"LGPL 2.0 and GNU LESSER",
},
wantLicenseName: "GPL-2.0-or-later AND LGPL-2.0-only AND LGPL-2.1-only",
},
{
name: "happy path with WITH operator",
input: []string{
"AFL 2.0",
"AFL 3.0 with Autoconf-exception-3.0",
},
wantLicenseName: "AFL-2.0 AND AFL-3.0 WITH Autoconf-exception-3.0",
},
{
name: "happy path with non-SPDX exception",
input: []string{
"AFL 2.0",
"AFL 3.0 with wrong-exceptions",
},
wantLicenseName: "AFL-2.0 AND LicenseRef-51373b28fab165e9",
wantOtherLicenses: []*spdx.OtherLicense{
{
LicenseIdentifier: "LicenseRef-51373b28fab165e9",
LicenseName: "AFL-3.0 WITH wrong-exceptions",
ExtractedText: `This component is licensed under "AFL-3.0 WITH wrong-exceptions"`,
},
},
},
{
name: "happy path with incorrect cases for license and exception",
input: []string{
"afl 3.0 with autoCONF-exception-3.0",
},
wantLicenseName: "AFL-3.0 WITH Autoconf-exception-3.0",
},
{
name: "happy path with text of license",
input: []string{
"text://unknown-license",
"AFL 2.0",
"unknown-license",
},
wantLicenseName: "LicenseRef-ffca10435cadded4 AND AFL-2.0 AND LicenseRef-a0bb0951a6dfbdbe",
wantOtherLicenses: []*spdx.OtherLicense{
{
LicenseIdentifier: "LicenseRef-a0bb0951a6dfbdbe",
LicenseName: "unknown-license",
ExtractedText: `This component is licensed under "unknown-license"`,
},
{
LicenseIdentifier: "LicenseRef-ffca10435cadded4",
LicenseName: "NOASSERTION",
ExtractedText: "unknown-license",
LicenseComment: "The license text represents text found in package metadata and may not represent the full text of the license",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewMarshaler("")
gotLicenseName, gotOtherLicenses := m.normalizeLicenses(tt.input)
// We will sort all OtherLicenses for SPDX document
// So we need to sort OtherLicenses for this test
sort.Slice(gotOtherLicenses, func(i, j int) bool {
return gotOtherLicenses[i].LicenseIdentifier < gotOtherLicenses[j].LicenseIdentifier
})
assert.Equal(t, tt.wantLicenseName, gotLicenseName)
assert.Equal(t, tt.wantOtherLicenses, gotOtherLicenses)
})
}
}

View File

@@ -842,6 +842,148 @@ func TestMarshaler_Marshal(t *testing.T) {
},
},
},
{
name: "happy path with various licenses",
inputReport: types.Report{
SchemaVersion: report.SchemaVersion,
ArtifactName: "pom.xml",
ArtifactType: artifact.TypeFilesystem,
Results: types.Results{
{
Target: "pom.xml",
Class: types.ClassLangPkg,
Type: ftypes.Pom,
Packages: []ftypes.Package{
{
ID: "com.example:example:1.0.0",
Name: "com.example:example",
Version: "1.0.0",
Identifier: ftypes.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeMaven,
Namespace: "com.example",
Name: "example",
Version: "1.0.0",
},
},
Licenses: []string{
"text://BSD-4-clause",
"BSD-4-clause or LGPL-2.0-only",
"AFL 3.0 with wrong-exceptions",
"AFL 3.0 with Autoconf-exception-3.0",
"text://UNKNOWN",
"UNKNOWN",
},
},
},
},
},
},
wantSBOM: &spdx.Document{
SPDXVersion: spdx.Version,
DataLicense: spdx.DataLicense,
SPDXIdentifier: "DOCUMENT",
DocumentName: "pom.xml",
DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/pom.xml-3ff14136-e09f-4df9-80ea-000000000004",
CreationInfo: &spdx.CreationInfo{
Creators: []common.Creator{
{
Creator: "aquasecurity",
CreatorType: "Organization",
},
{
Creator: "trivy-0.56.2",
CreatorType: "Tool",
},
},
Created: "2021-08-25T12:20:30Z",
},
Packages: []*spdx.Package{
{
PackageSPDXIdentifier: spdx.ElementID("Application-800d9e6e0f88ab3a"),
PackageDownloadLocation: "NONE",
PackageName: "pom.xml",
PrimaryPackagePurpose: tspdx.PackagePurposeApplication,
Annotations: []spdx.Annotation{
annotation(t, "Class: lang-pkgs"),
annotation(t, "Type: pom"),
},
},
{
PackageSPDXIdentifier: spdx.ElementID("Package-69cd7625c68537c7"),
PackageDownloadLocation: "NONE",
PackageName: "com.example:example",
PackageVersion: "1.0.0",
PackageLicenseConcluded: "LicenseRef-14b1606fb243e2b6 AND (BSD-4-Clause OR LGPL-2.0-only) AND LicenseRef-77bdf77d8292ce5b AND AFL-3.0 WITH Autoconf-exception-3.0 AND LicenseRef-229659393343e160 AND LicenseRef-a8d01765900624d3",
PackageLicenseDeclared: "LicenseRef-14b1606fb243e2b6 AND (BSD-4-Clause OR LGPL-2.0-only) AND LicenseRef-77bdf77d8292ce5b AND AFL-3.0 WITH Autoconf-exception-3.0 AND LicenseRef-229659393343e160 AND LicenseRef-a8d01765900624d3",
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: tspdx.CategoryPackageManager,
RefType: tspdx.RefTypePurl,
Locator: "pkg:maven/com.example/example@1.0.0",
},
},
PrimaryPackagePurpose: tspdx.PackagePurposeLibrary,
PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion},
PackageSourceInfo: "package found in: pom.xml",
Annotations: []spdx.Annotation{
annotation(t, "PkgID: com.example:example:1.0.0"),
annotation(t, "PkgType: pom"),
},
},
{
PackageSPDXIdentifier: spdx.ElementID("Filesystem-340a6f62df359d6a"),
PackageDownloadLocation: "NONE",
PackageName: "pom.xml",
Annotations: []spdx.Annotation{
annotation(t, "SchemaVersion: 2"),
},
PrimaryPackagePurpose: tspdx.PackagePurposeSource,
},
},
Relationships: []*spdx.Relationship{
{
RefA: spdx.DocElementID{ElementRefID: "Application-800d9e6e0f88ab3a"},
RefB: spdx.DocElementID{ElementRefID: "Package-69cd7625c68537c7"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"},
RefB: spdx.DocElementID{ElementRefID: "Filesystem-340a6f62df359d6a"},
Relationship: "DESCRIBES",
},
{
RefA: spdx.DocElementID{ElementRefID: "Filesystem-340a6f62df359d6a"},
RefB: spdx.DocElementID{ElementRefID: "Application-800d9e6e0f88ab3a"},
Relationship: "CONTAINS",
},
},
OtherLicenses: []*spdx.OtherLicense{
{
LicenseIdentifier: "LicenseRef-14b1606fb243e2b6",
LicenseName: "NOASSERTION",
ExtractedText: "BSD-4-clause",
LicenseComment: "The license text represents text found in package metadata and may not represent the full text of the license",
},
{
LicenseIdentifier: "LicenseRef-229659393343e160",
LicenseName: "NOASSERTION",
ExtractedText: "UNKNOWN",
LicenseComment: "The license text represents text found in package metadata and may not represent the full text of the license",
},
{
LicenseIdentifier: "LicenseRef-77bdf77d8292ce5b",
LicenseName: "AFL-3.0 WITH wrong-exceptions",
ExtractedText: `This component is licensed under "AFL-3.0 WITH wrong-exceptions"`,
},
{
LicenseIdentifier: "LicenseRef-a8d01765900624d3",
LicenseName: "UNKNOWN",
ExtractedText: `This component is licensed under "UNKNOWN"`,
},
},
},
},
{
name: "happy path with vulnerability",
inputReport: types.Report{
@@ -1324,6 +1466,8 @@ func TestMarshaler_Marshal(t *testing.T) {
for _, f := range vv.Files {
str += f.Path
}
case spdx.OtherLicense:
str = vv.ExtractedText + vv.LicenseName
case string:
str = vv
default:
@@ -1349,56 +1493,3 @@ func TestMarshaler_Marshal(t *testing.T) {
})
}
}
func Test_GetLicense(t *testing.T) {
tests := []struct {
name string
input []string
want string
}{
{
name: "happy path",
input: []string{
"GPLv2+",
},
want: "GPL-2.0-or-later",
},
{
name: "happy path with multi license",
input: []string{
"GPLv2+",
"GPLv3+",
},
want: "GPL-2.0-or-later AND GPL-3.0-or-later",
},
{
name: "happy path with OR operator",
input: []string{
"GPLv2+",
"LGPL 2.0 or GNU LESSER",
},
want: "GPL-2.0-or-later AND (LGPL-2.0-only OR LGPL-2.1-only)",
},
{
name: "happy path with AND operator",
input: []string{
"GPLv2+",
"LGPL 2.0 and GNU LESSER",
},
want: "GPL-2.0-or-later AND LGPL-2.0-only AND LGPL-2.1-only",
},
{
name: "happy path with WITH operator",
input: []string{
"AFL 2.0",
"AFL 3.0 with distribution exception",
},
want: "AFL-2.0 AND AFL-3.0 WITH distribution-exception",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tspdx.NormalizeLicense(tt.input))
})
}
}