From 6272fdd510276993578d6298f86547c53ed5f0b4 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 10 Dec 2024 19:04:24 +0600 Subject: [PATCH 01/18] feat: add spdx licenses and exceptions + validation --- pkg/licensing/expression/category.go | 134 +++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/pkg/licensing/expression/category.go b/pkg/licensing/expression/category.go index c32f228c07d8..9db5ee8c5af1 100644 --- a/pkg/licensing/expression/category.go +++ b/pkg/licensing/expression/category.go @@ -1,5 +1,12 @@ package expression +import ( + "slices" + "strings" + + "github.com/samber/lo" +) + // Canonical names of the licenses. // ported from https://github.com/google/licenseclassifier/blob/7c62d6fe8d3aa2f39c4affb58c9781d9dc951a2d/license_type.go#L24-L177 const ( @@ -358,4 +365,131 @@ var ( Unlicense, ZeroBSD, } + + // SpdxLicenseExceptions contains all supported SPDX Exceptions + // cf. https://github.com/spdx/license-list-data/blob/592c2dcb8497c6fe829eea604045f77d3bce770b/json/exceptions.json + // used `awk -F'"' '/"licenseExceptionId":/ {print toupper("\"" $4 "\"," )}' exceptions.json ` command + spdxLicenseExceptions = []string{ + "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", + "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-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", + "HARBOUR-EXCEPTION", //nolint:misspell + "I2P-GPL-JAVA-EXCEPTION", + "INDEPENDENT-MODULES-EXCEPTION", + "KICAD-LIBRARIES-EXCEPTION", + "LGPL-3.0-LINKING-EXCEPTION", + "LIBPRI-OPENH323-EXCEPTION", + "LIBTOOL-EXCEPTION", + "LINUX-SYSCALL-NOTE", + "LLGPL", + "LLVM-EXCEPTION", + "LZMA-EXCEPTION", + "MIF-EXCEPTION", + "MXML-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", + } ) + +var spdxLicenses map[string]struct{} + +func initSpdxLicenses() { + 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{}{} + } + +} + +// ValidSpdxLicense returns true if SPDX license lists contain licenseID and license exception (if exists) +func ValidSpdxLicense(license string) bool { + if spdxLicenses == nil { + initSpdxLicenses() + } + + id, exception, ok := strings.Cut(license, " WITH ") + if _, licenseIdFound := spdxLicenses[id]; licenseIdFound && (!ok || slices.Contains(spdxLicenseExceptions, strings.ToUpper(exception))) { + return true + } + return false +} From eb30b492705681433815d65aa5c5cee6daf525e2 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 10 Dec 2024 19:07:49 +0600 Subject: [PATCH 02/18] feat: use otherLicense for non-spdx licenses --- pkg/sbom/spdx/marshal.go | 147 ++++++++++++++++++++++++++++++--------- 1 file changed, 113 insertions(+), 34 deletions(-) diff --git a/pkg/sbom/spdx/marshal.go b/pkg/sbom/spdx/marshal.go index 51f9144f682d..4413c53e55f7 100644 --- a/pkg/sbom/spdx/marshal.go +++ b/pkg/sbom/spdx/marshal.go @@ -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("SPDX"), } 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) + 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,90 @@ 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 && 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 + + // Save text licenses as OtherLicense + for i, license := range licenses { + if strings.HasPrefix(license, licensing.LicenseTextPrefix) { + otherLicense := m.newOtherLicense(strings.TrimPrefix(license, licensing.LicenseTextPrefix), true) + otherLicenses[otherLicense.LicenseIdentifier] = otherLicense + licenses[i] = otherLicense.LicenseIdentifier + } + } + + 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 ") + + normalizedLicense, err := expression.Normalize(license, licensing.NormalizeLicense, expression.NormalizeForSPDX) + if err != nil { + // Not fail on the invalid license + m.logger.Warn("Unable to marshal SPDX licenses", log.String("license", license)) + return "", nil + } + + license, others := m.processNonSpdxLicenses(normalizedLicense) + allOtherLicenses := lo.Assign(otherLicenses, others) + + return license, lo.Ternary(len(allOtherLicenses) > 0, lo.Values(allOtherLicenses), 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.ExtractedText = license + } else { + otherLicense.LicenseName = license + } + licenseID, err := calcSPDXID(m.hasher, otherLicense) + if err != nil { + m.logger.Warn("Unable to calculate SPDX licenses ID", log.String("license", license), log.Err(err)) + return nil + } + otherLicense.LicenseIdentifier = LicenseRefPrefix + "-" + licenseID + + return &otherLicense +} + +// processNonSpdxLicenses detects licenses that are not on the SPDX list and creates OtherLicense for them +func (m *Marshaler) processNonSpdxLicenses(license string) (string, map[string]*spdx.OtherLicense) { + otherLicenses := make(map[string]*spdx.OtherLicense) + var andLicenses []string + for _, andLicense := range strings.Split(license, " AND ") { + var orLicenses []string + for _, orLicense := range strings.Split(andLicense, " OR ") { + // Handle brackets - e.g. GPL-2.0-or-later AND (LGPL-2.0-only OR LGPL-2.1-only) + startRune := lo.Ternary(strings.HasPrefix(orLicense, "("), "(", "") + endRune := lo.Ternary(strings.HasSuffix(orLicense, ")"), ")", "") + trimmedLicense := strings.TrimPrefix(strings.TrimSuffix(orLicense, ")"), "(") + // This is license from SPDX list of text license (already processed) + if strings.HasPrefix(trimmedLicense, LicenseRefPrefix) || expression.ValidSpdxLicense(trimmedLicense) { + orLicenses = append(orLicenses, orLicense) + continue + } + + // Save this license as OtherLicense + otherLicense := m.newOtherLicense(trimmedLicense, false) + otherLicenses[otherLicense.LicenseIdentifier] = otherLicense + orLicenses = append(orLicenses, startRune+otherLicense.LicenseIdentifier+endRune) + } + andLicenses = append(andLicenses, strings.Join(orLicenses, " OR ")) } - return NormalizeLicense(c.Licenses) + + return strings.Join(andLicenses, " AND "), otherLicenses } func (m *Marshaler) spdxChecksums(digests []digest.Digest) []common.Checksum { @@ -435,7 +525,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 +595,12 @@ func sortFiles(files []*spdx.File) { }) } +func sortOtherLicenses(licenses []*spdx.OtherLicense) { + sort.Slice(licenses, func(i, j int) bool { + return licenses[i].LicenseIdentifier < licenses[j].LicenseIdentifier + }) +} + func elementID(elementType, pkgID string) spdx.ElementID { return spdx.ElementID(fmt.Sprintf("%s-%s", elementType, pkgID)) } @@ -518,13 +614,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 +646,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 -} From b1081e20887c2066e067c7be1271de565c2af29b Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 10 Dec 2024 19:07:58 +0600 Subject: [PATCH 03/18] test: add/update unit tests --- pkg/sbom/spdx/marshal_private_test.go | 122 ++++++++++++++++ pkg/sbom/spdx/marshal_test.go | 191 +++++++++++++++++++------- 2 files changed, 260 insertions(+), 53 deletions(-) create mode 100644 pkg/sbom/spdx/marshal_private_test.go diff --git a/pkg/sbom/spdx/marshal_private_test.go b/pkg/sbom/spdx/marshal_private_test.go new file mode 100644 index 000000000000..3ed549a2a0c2 --- /dev/null +++ b/pkg/sbom/spdx/marshal_private_test.go @@ -0,0 +1,122 @@ +package spdx + +import ( + "sort" + "testing" + + "github.com/spdx/tools-golang/spdx" + "github.com/stretchr/testify/require" +) + +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+", + }, + wantLicenseName: "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", + }, + 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-3a64a1cb4bc51d5d OR LicenseRef-398e59dafb7d221c)", + wantOtherLicenses: []*spdx.OtherLicense{ + { + LicenseIdentifier: "LicenseRef-398e59dafb7d221c", + LicenseName: "unknown-license", + }, + { + LicenseIdentifier: "LicenseRef-3a64a1cb4bc51d5d", + LicenseName: "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-64ec018384f0fde7", + wantOtherLicenses: []*spdx.OtherLicense{ + { + LicenseIdentifier: "LicenseRef-64ec018384f0fde7", + LicenseName: "AFL-3.0 WITH wrong-exceptions", + }, + }, + }, + { + name: "happy path with text of license", + input: []string{ + "text://unknown-license", + "AFL 2.0", + "unknown-license", + }, + wantLicenseName: "LicenseRef-d94457d3705e6c77 AND AFL-2.0 AND LicenseRef-398e59dafb7d221c", + wantOtherLicenses: []*spdx.OtherLicense{ + { + LicenseIdentifier: "LicenseRef-398e59dafb7d221c", + LicenseName: "unknown-license", + }, + { + LicenseIdentifier: "LicenseRef-d94457d3705e6c77", + ExtractedText: "unknown-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 + }) + require.Equal(t, tt.wantLicenseName, gotLicenseName) + require.Equal(t, tt.wantOtherLicenses, gotOtherLicenses) + }) + } +} diff --git a/pkg/sbom/spdx/marshal_test.go b/pkg/sbom/spdx/marshal_test.go index 4d9d33c013a7..43edfde10386 100644 --- a/pkg/sbom/spdx/marshal_test.go +++ b/pkg/sbom/spdx/marshal_test.go @@ -842,6 +842,142 @@ 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-e9cd3c91226a1faf AND (BSD-4-Clause OR LGPL-2.0-only) AND LicenseRef-7f368222b66b338c AND AFL-3.0 WITH Autoconf-exception-3.0 AND LicenseRef-c9e3ba90af6fce7b AND LicenseRef-43ec15da050d7849", + PackageLicenseDeclared: "LicenseRef-e9cd3c91226a1faf AND (BSD-4-Clause OR LGPL-2.0-only) AND LicenseRef-7f368222b66b338c AND AFL-3.0 WITH Autoconf-exception-3.0 AND LicenseRef-c9e3ba90af6fce7b AND LicenseRef-43ec15da050d7849", + 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-43ec15da050d7849", + LicenseName: "UNKNOWN", + }, + { + LicenseIdentifier: "LicenseRef-7f368222b66b338c", + LicenseName: "AFL-3.0 WITH wrong-exceptions", + }, + { + LicenseIdentifier: "LicenseRef-c9e3ba90af6fce7b", + ExtractedText: "UNKNOWN", + }, + { + LicenseIdentifier: "LicenseRef-e9cd3c91226a1faf", + ExtractedText: "BSD-4-clause", + }, + }, + }, + }, { name: "happy path with vulnerability", inputReport: types.Report{ @@ -1324,6 +1460,8 @@ func TestMarshaler_Marshal(t *testing.T) { for _, f := range vv.Files { str += f.Path } + case spdx.OtherLicense: + str = "text: " + vv.ExtractedText + "name: " + vv.LicenseName case string: str = vv default: @@ -1349,56 +1487,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)) - }) - } -} From 420c15e3ad3dbecddd5484faf58fde561ef95e3f Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 10 Dec 2024 19:39:14 +0600 Subject: [PATCH 04/18] refactor: remove duplicates of otherLicenses --- pkg/sbom/spdx/marshal.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/sbom/spdx/marshal.go b/pkg/sbom/spdx/marshal.go index 4413c53e55f7..c93bb77af403 100644 --- a/pkg/sbom/spdx/marshal.go +++ b/pkg/sbom/spdx/marshal.go @@ -216,7 +216,7 @@ func (m *Marshaler) Marshal(ctx context.Context, bom *core.BOM) (*spdx.Document, sortPackages(packages) sortRelationships(relationShips) sortFiles(files) - sortOtherLicenses(otherLicenses) + otherLicenses = sortOtherLicenses(otherLicenses) return &spdx.Document{ SPDXVersion: spdx.Version, @@ -402,7 +402,10 @@ func (m *Marshaler) spdxAnnotations(c *core.Component, timeNow string) []spdx.An func (m *Marshaler) spdxLicense(c *core.Component) (string, []*spdx.OtherLicense) { // Only library components contain licenses - if c.Type == core.TypeLibrary && len(c.Licenses) == 0 { + if c.Type != core.TypeLibrary { + return "", nil + } + if len(c.Licenses) == 0 { return noAssertionField, nil } return m.normalizeLicenses(c.Licenses) @@ -595,10 +598,15 @@ func sortFiles(files []*spdx.File) { }) } -func sortOtherLicenses(licenses []*spdx.OtherLicense) { +// sortOtherLicenses removes duplicates and sorts result slice +func sortOtherLicenses(licenses []*spdx.OtherLicense) []*spdx.OtherLicense { + 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 { From b6c1d28dab44bcefb14d843ca970774434421f97 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 10 Dec 2024 19:47:47 +0600 Subject: [PATCH 05/18] refactor: always fill ExtractedText and LicenseName fields --- pkg/sbom/spdx/marshal.go | 8 +++++++- pkg/sbom/spdx/marshal_test.go | 26 +++++++++++++++----------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/pkg/sbom/spdx/marshal.go b/pkg/sbom/spdx/marshal.go index c93bb77af403..55d0383001ea 100644 --- a/pkg/sbom/spdx/marshal.go +++ b/pkg/sbom/spdx/marshal.go @@ -445,7 +445,10 @@ func (m *Marshaler) normalizeLicenses(licenses []string) (string, []*spdx.OtherL // 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{} + otherLicense := spdx.OtherLicense{ + ExtractedText: noAssertionField, + LicenseName: noAssertionField, + } if text { otherLicense.ExtractedText = license } else { @@ -600,6 +603,9 @@ 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 }) diff --git a/pkg/sbom/spdx/marshal_test.go b/pkg/sbom/spdx/marshal_test.go index 43edfde10386..9ed524a75e71 100644 --- a/pkg/sbom/spdx/marshal_test.go +++ b/pkg/sbom/spdx/marshal_test.go @@ -914,8 +914,8 @@ func TestMarshaler_Marshal(t *testing.T) { PackageDownloadLocation: "NONE", PackageName: "com.example:example", PackageVersion: "1.0.0", - PackageLicenseConcluded: "LicenseRef-e9cd3c91226a1faf AND (BSD-4-Clause OR LGPL-2.0-only) AND LicenseRef-7f368222b66b338c AND AFL-3.0 WITH Autoconf-exception-3.0 AND LicenseRef-c9e3ba90af6fce7b AND LicenseRef-43ec15da050d7849", - PackageLicenseDeclared: "LicenseRef-e9cd3c91226a1faf AND (BSD-4-Clause OR LGPL-2.0-only) AND LicenseRef-7f368222b66b338c AND AFL-3.0 WITH Autoconf-exception-3.0 AND LicenseRef-c9e3ba90af6fce7b AND LicenseRef-43ec15da050d7849", + PackageLicenseConcluded: "LicenseRef-14b1606fb243e2b6 AND (BSD-4-Clause OR LGPL-2.0-only) AND LicenseRef-73d50f1d1ecbdb3b AND AFL-3.0 WITH Autoconf-exception-3.0 AND LicenseRef-229659393343e160 AND LicenseRef-94441301bfddf506", + PackageLicenseDeclared: "LicenseRef-14b1606fb243e2b6 AND (BSD-4-Clause OR LGPL-2.0-only) AND LicenseRef-73d50f1d1ecbdb3b AND AFL-3.0 WITH Autoconf-exception-3.0 AND LicenseRef-229659393343e160 AND LicenseRef-94441301bfddf506", PackageExternalReferences: []*spdx.PackageExternalReference{ { Category: tspdx.CategoryPackageManager, @@ -960,20 +960,24 @@ func TestMarshaler_Marshal(t *testing.T) { }, OtherLicenses: []*spdx.OtherLicense{ { - LicenseIdentifier: "LicenseRef-43ec15da050d7849", - LicenseName: "UNKNOWN", + LicenseIdentifier: "LicenseRef-14b1606fb243e2b6", + LicenseName: "NOASSERTION", + ExtractedText: "BSD-4-clause", }, { - LicenseIdentifier: "LicenseRef-7f368222b66b338c", - LicenseName: "AFL-3.0 WITH wrong-exceptions", + LicenseIdentifier: "LicenseRef-229659393343e160", + LicenseName: "NOASSERTION", + ExtractedText: "UNKNOWN", }, { - LicenseIdentifier: "LicenseRef-c9e3ba90af6fce7b", - ExtractedText: "UNKNOWN", + LicenseIdentifier: "LicenseRef-73d50f1d1ecbdb3b", + LicenseName: "AFL-3.0 WITH wrong-exceptions", + ExtractedText: "NOASSERTION", }, { - LicenseIdentifier: "LicenseRef-e9cd3c91226a1faf", - ExtractedText: "BSD-4-clause", + LicenseIdentifier: "LicenseRef-94441301bfddf506", + LicenseName: "UNKNOWN", + ExtractedText: "NOASSERTION", }, }, }, @@ -1461,7 +1465,7 @@ func TestMarshaler_Marshal(t *testing.T) { str += f.Path } case spdx.OtherLicense: - str = "text: " + vv.ExtractedText + "name: " + vv.LicenseName + str = vv.ExtractedText + vv.LicenseName case string: str = vv default: From 1bac2cafcdccfe0c9fd32f8c66a8c37acf07a986 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Wed, 11 Dec 2024 12:33:00 +0600 Subject: [PATCH 06/18] fix: tests --- pkg/sbom/spdx/marshal_private_test.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/pkg/sbom/spdx/marshal_private_test.go b/pkg/sbom/spdx/marshal_private_test.go index 3ed549a2a0c2..ff98efd05768 100644 --- a/pkg/sbom/spdx/marshal_private_test.go +++ b/pkg/sbom/spdx/marshal_private_test.go @@ -44,15 +44,17 @@ func TestMarshaler_normalizeLicenses(t *testing.T) { "GPLv2+", "wrong-license or unknown-license", }, - wantLicenseName: "GPL-2.0-or-later AND (LicenseRef-3a64a1cb4bc51d5d OR LicenseRef-398e59dafb7d221c)", + wantLicenseName: "GPL-2.0-or-later AND (LicenseRef-a84be91b438c5e83 OR LicenseRef-8960e1168859663e)", wantOtherLicenses: []*spdx.OtherLicense{ { - LicenseIdentifier: "LicenseRef-398e59dafb7d221c", + LicenseIdentifier: "LicenseRef-8960e1168859663e", LicenseName: "unknown-license", + ExtractedText: "NOASSERTION", }, { - LicenseIdentifier: "LicenseRef-3a64a1cb4bc51d5d", + LicenseIdentifier: "LicenseRef-a84be91b438c5e83", LicenseName: "wrong-license", + ExtractedText: "NOASSERTION", }, }, }, @@ -78,11 +80,12 @@ func TestMarshaler_normalizeLicenses(t *testing.T) { "AFL 2.0", "AFL 3.0 with wrong-exceptions", }, - wantLicenseName: "AFL-2.0 AND LicenseRef-64ec018384f0fde7", + wantLicenseName: "AFL-2.0 AND LicenseRef-fb68abbeae80aaed", wantOtherLicenses: []*spdx.OtherLicense{ { - LicenseIdentifier: "LicenseRef-64ec018384f0fde7", + LicenseIdentifier: "LicenseRef-fb68abbeae80aaed", LicenseName: "AFL-3.0 WITH wrong-exceptions", + ExtractedText: "NOASSERTION", }, }, }, @@ -93,14 +96,16 @@ func TestMarshaler_normalizeLicenses(t *testing.T) { "AFL 2.0", "unknown-license", }, - wantLicenseName: "LicenseRef-d94457d3705e6c77 AND AFL-2.0 AND LicenseRef-398e59dafb7d221c", + wantLicenseName: "LicenseRef-c5e3a1aeaab71db AND AFL-2.0 AND LicenseRef-8960e1168859663e", wantOtherLicenses: []*spdx.OtherLicense{ { - LicenseIdentifier: "LicenseRef-398e59dafb7d221c", + LicenseIdentifier: "LicenseRef-8960e1168859663e", LicenseName: "unknown-license", + ExtractedText: "NOASSERTION", }, { - LicenseIdentifier: "LicenseRef-d94457d3705e6c77", + LicenseIdentifier: "LicenseRef-c5e3a1aeaab71db", + LicenseName: "NOASSERTION", ExtractedText: "unknown-license", }, }, From f851f9bb18411db838fc65e0c6c4351b04953f8f Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Wed, 11 Dec 2024 12:46:55 +0600 Subject: [PATCH 07/18] add comment --- pkg/sbom/spdx/marshal.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/sbom/spdx/marshal.go b/pkg/sbom/spdx/marshal.go index 55d0383001ea..47f181a5f638 100644 --- a/pkg/sbom/spdx/marshal.go +++ b/pkg/sbom/spdx/marshal.go @@ -446,6 +446,7 @@ func (m *Marshaler) normalizeLicenses(licenses []string) (string, []*spdx.OtherL // 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{ + // ExtractedText and LicenseName are mandatory fields ExtractedText: noAssertionField, LicenseName: noAssertionField, } From 041ab214c353a9d27dcdd4ef9672555840ec5cca Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Thu, 12 Dec 2024 13:26:43 +0600 Subject: [PATCH 08/18] feat: add `LicenseComment` field --- pkg/sbom/spdx/marshal.go | 1 + pkg/sbom/spdx/marshal_private_test.go | 5 +++-- pkg/sbom/spdx/marshal_test.go | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/sbom/spdx/marshal.go b/pkg/sbom/spdx/marshal.go index 47f181a5f638..3efa5f55bb5d 100644 --- a/pkg/sbom/spdx/marshal.go +++ b/pkg/sbom/spdx/marshal.go @@ -452,6 +452,7 @@ func (m *Marshaler) newOtherLicense(license string, text bool) *spdx.OtherLicens } if text { 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 } diff --git a/pkg/sbom/spdx/marshal_private_test.go b/pkg/sbom/spdx/marshal_private_test.go index ff98efd05768..83748024b238 100644 --- a/pkg/sbom/spdx/marshal_private_test.go +++ b/pkg/sbom/spdx/marshal_private_test.go @@ -96,7 +96,7 @@ func TestMarshaler_normalizeLicenses(t *testing.T) { "AFL 2.0", "unknown-license", }, - wantLicenseName: "LicenseRef-c5e3a1aeaab71db AND AFL-2.0 AND LicenseRef-8960e1168859663e", + wantLicenseName: "LicenseRef-ffca10435cadded4 AND AFL-2.0 AND LicenseRef-8960e1168859663e", wantOtherLicenses: []*spdx.OtherLicense{ { LicenseIdentifier: "LicenseRef-8960e1168859663e", @@ -104,9 +104,10 @@ func TestMarshaler_normalizeLicenses(t *testing.T) { ExtractedText: "NOASSERTION", }, { - LicenseIdentifier: "LicenseRef-c5e3a1aeaab71db", + 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", }, }, }, diff --git a/pkg/sbom/spdx/marshal_test.go b/pkg/sbom/spdx/marshal_test.go index 9ed524a75e71..ba51d52c4dc7 100644 --- a/pkg/sbom/spdx/marshal_test.go +++ b/pkg/sbom/spdx/marshal_test.go @@ -963,11 +963,13 @@ func TestMarshaler_Marshal(t *testing.T) { 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-73d50f1d1ecbdb3b", From 659f992f9eccbfec55ea65fe7b2bdc973d892c8e Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Fri, 13 Dec 2024 12:07:02 +0600 Subject: [PATCH 09/18] refactor: ExtractedText field for license with name --- pkg/sbom/spdx/marshal.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/sbom/spdx/marshal.go b/pkg/sbom/spdx/marshal.go index 3efa5f55bb5d..886ddfe9fdf8 100644 --- a/pkg/sbom/spdx/marshal.go +++ b/pkg/sbom/spdx/marshal.go @@ -445,16 +445,14 @@ func (m *Marshaler) normalizeLicenses(licenses []string) (string, []*spdx.OtherL // 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{ - // ExtractedText and LicenseName are mandatory fields - ExtractedText: noAssertionField, - LicenseName: noAssertionField, - } + 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 %s", license) } licenseID, err := calcSPDXID(m.hasher, otherLicense) if err != nil { From c25d8408ed050720ba1d24bd0f736b29ef16ad91 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Fri, 13 Dec 2024 12:09:02 +0600 Subject: [PATCH 10/18] refactor: use exception list from spdx.org site --- pkg/licensing/expression/category.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pkg/licensing/expression/category.go b/pkg/licensing/expression/category.go index 9db5ee8c5af1..b509ba9300bb 100644 --- a/pkg/licensing/expression/category.go +++ b/pkg/licensing/expression/category.go @@ -367,7 +367,7 @@ var ( } // SpdxLicenseExceptions contains all supported SPDX Exceptions - // cf. https://github.com/spdx/license-list-data/blob/592c2dcb8497c6fe829eea604045f77d3bce770b/json/exceptions.json + // cf. https://spdx.org/licenses/exceptions.json // used `awk -F'"' '/"licenseExceptionId":/ {print toupper("\"" $4 "\"," )}' exceptions.json ` command spdxLicenseExceptions = []string{ "389-EXCEPTION", @@ -381,7 +381,6 @@ var ( "BISON-EXCEPTION-1.24", "BISON-EXCEPTION-2.2", "BOOTLOADER-EXCEPTION", - "CGAL-LINKING-EXCEPTION", "CLASSPATH-EXCEPTION-2.0", "CLISP-EXCEPTION-2.0", "CRYPTSETUP-OPENSSL-EXCEPTION", @@ -401,16 +400,13 @@ var ( "GNOME-EXAMPLES-EXCEPTION", "GNU-COMPILER-EXCEPTION", "GNU-JAVAMAIL-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", - "HARBOUR-EXCEPTION", //nolint:misspell "I2P-GPL-JAVA-EXCEPTION", - "INDEPENDENT-MODULES-EXCEPTION", "KICAD-LIBRARIES-EXCEPTION", "LGPL-3.0-LINKING-EXCEPTION", "LIBPRI-OPENH323-EXCEPTION", @@ -420,7 +416,6 @@ var ( "LLVM-EXCEPTION", "LZMA-EXCEPTION", "MIF-EXCEPTION", - "MXML-EXCEPTION", "NOKIA-QT-EXCEPTION-1.1", "OCAML-LGPL-LINKING-EXCEPTION", "OCCT-EXCEPTION-1.0", From 20275c39238ce996505f5ea671916b80197094fe Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Fri, 13 Dec 2024 12:18:07 +0600 Subject: [PATCH 11/18] test: update tests --- pkg/sbom/spdx/marshal.go | 2 +- pkg/sbom/spdx/marshal_private_test.go | 28 +++++++++++++-------------- pkg/sbom/spdx/marshal_test.go | 12 ++++++------ 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/pkg/sbom/spdx/marshal.go b/pkg/sbom/spdx/marshal.go index 886ddfe9fdf8..e1f7ae54b804 100644 --- a/pkg/sbom/spdx/marshal.go +++ b/pkg/sbom/spdx/marshal.go @@ -452,7 +452,7 @@ func (m *Marshaler) newOtherLicense(license string, text bool) *spdx.OtherLicens 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 %s", license) + otherLicense.ExtractedText = fmt.Sprintf("This component is licensed under %q", license) } licenseID, err := calcSPDXID(m.hasher, otherLicense) if err != nil { diff --git a/pkg/sbom/spdx/marshal_private_test.go b/pkg/sbom/spdx/marshal_private_test.go index 83748024b238..ec6db8afa812 100644 --- a/pkg/sbom/spdx/marshal_private_test.go +++ b/pkg/sbom/spdx/marshal_private_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/spdx/tools-golang/spdx" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" ) func TestMarshaler_normalizeLicenses(t *testing.T) { @@ -44,17 +44,17 @@ func TestMarshaler_normalizeLicenses(t *testing.T) { "GPLv2+", "wrong-license or unknown-license", }, - wantLicenseName: "GPL-2.0-or-later AND (LicenseRef-a84be91b438c5e83 OR LicenseRef-8960e1168859663e)", + wantLicenseName: "GPL-2.0-or-later AND (LicenseRef-c581e42fe705aa48 OR LicenseRef-a0bb0951a6dfbdbe)", wantOtherLicenses: []*spdx.OtherLicense{ { - LicenseIdentifier: "LicenseRef-8960e1168859663e", + LicenseIdentifier: "LicenseRef-a0bb0951a6dfbdbe", LicenseName: "unknown-license", - ExtractedText: "NOASSERTION", + ExtractedText: `This component is licensed under "unknown-license"`, }, { - LicenseIdentifier: "LicenseRef-a84be91b438c5e83", + LicenseIdentifier: "LicenseRef-c581e42fe705aa48", LicenseName: "wrong-license", - ExtractedText: "NOASSERTION", + ExtractedText: `This component is licensed under "wrong-license"`, }, }, }, @@ -80,12 +80,12 @@ func TestMarshaler_normalizeLicenses(t *testing.T) { "AFL 2.0", "AFL 3.0 with wrong-exceptions", }, - wantLicenseName: "AFL-2.0 AND LicenseRef-fb68abbeae80aaed", + wantLicenseName: "AFL-2.0 AND LicenseRef-51373b28fab165e9", wantOtherLicenses: []*spdx.OtherLicense{ { - LicenseIdentifier: "LicenseRef-fb68abbeae80aaed", + LicenseIdentifier: "LicenseRef-51373b28fab165e9", LicenseName: "AFL-3.0 WITH wrong-exceptions", - ExtractedText: "NOASSERTION", + ExtractedText: `This component is licensed under "AFL-3.0 WITH wrong-exceptions"`, }, }, }, @@ -96,12 +96,12 @@ func TestMarshaler_normalizeLicenses(t *testing.T) { "AFL 2.0", "unknown-license", }, - wantLicenseName: "LicenseRef-ffca10435cadded4 AND AFL-2.0 AND LicenseRef-8960e1168859663e", + wantLicenseName: "LicenseRef-ffca10435cadded4 AND AFL-2.0 AND LicenseRef-a0bb0951a6dfbdbe", wantOtherLicenses: []*spdx.OtherLicense{ { - LicenseIdentifier: "LicenseRef-8960e1168859663e", + LicenseIdentifier: "LicenseRef-a0bb0951a6dfbdbe", LicenseName: "unknown-license", - ExtractedText: "NOASSERTION", + ExtractedText: `This component is licensed under "unknown-license"`, }, { LicenseIdentifier: "LicenseRef-ffca10435cadded4", @@ -121,8 +121,8 @@ func TestMarshaler_normalizeLicenses(t *testing.T) { sort.Slice(gotOtherLicenses, func(i, j int) bool { return gotOtherLicenses[i].LicenseIdentifier < gotOtherLicenses[j].LicenseIdentifier }) - require.Equal(t, tt.wantLicenseName, gotLicenseName) - require.Equal(t, tt.wantOtherLicenses, gotOtherLicenses) + assert.Equal(t, tt.wantLicenseName, gotLicenseName) + assert.Equal(t, tt.wantOtherLicenses, gotOtherLicenses) }) } } diff --git a/pkg/sbom/spdx/marshal_test.go b/pkg/sbom/spdx/marshal_test.go index ba51d52c4dc7..5c8e3ab4902e 100644 --- a/pkg/sbom/spdx/marshal_test.go +++ b/pkg/sbom/spdx/marshal_test.go @@ -914,8 +914,8 @@ func TestMarshaler_Marshal(t *testing.T) { PackageDownloadLocation: "NONE", PackageName: "com.example:example", PackageVersion: "1.0.0", - PackageLicenseConcluded: "LicenseRef-14b1606fb243e2b6 AND (BSD-4-Clause OR LGPL-2.0-only) AND LicenseRef-73d50f1d1ecbdb3b AND AFL-3.0 WITH Autoconf-exception-3.0 AND LicenseRef-229659393343e160 AND LicenseRef-94441301bfddf506", - PackageLicenseDeclared: "LicenseRef-14b1606fb243e2b6 AND (BSD-4-Clause OR LGPL-2.0-only) AND LicenseRef-73d50f1d1ecbdb3b AND AFL-3.0 WITH Autoconf-exception-3.0 AND LicenseRef-229659393343e160 AND LicenseRef-94441301bfddf506", + 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, @@ -972,14 +972,14 @@ func TestMarshaler_Marshal(t *testing.T) { LicenseComment: "The license text represents text found in package metadata and may not represent the full text of the license", }, { - LicenseIdentifier: "LicenseRef-73d50f1d1ecbdb3b", + LicenseIdentifier: "LicenseRef-77bdf77d8292ce5b", LicenseName: "AFL-3.0 WITH wrong-exceptions", - ExtractedText: "NOASSERTION", + ExtractedText: `This component is licensed under "AFL-3.0 WITH wrong-exceptions"`, }, { - LicenseIdentifier: "LicenseRef-94441301bfddf506", + LicenseIdentifier: "LicenseRef-a8d01765900624d3", LicenseName: "UNKNOWN", - ExtractedText: "NOASSERTION", + ExtractedText: `This component is licensed under "UNKNOWN"`, }, }, }, From c89c4e30134914f00e646d830e174fa3aa3fa7d9 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 17 Dec 2024 16:34:58 +0600 Subject: [PATCH 12/18] refactor: use `exceptions.json` file --- pkg/licensing/expression/category.go | 123 +++++++++------------------ 1 file changed, 40 insertions(+), 83 deletions(-) diff --git a/pkg/licensing/expression/category.go b/pkg/licensing/expression/category.go index b509ba9300bb..9e5e6f91f419 100644 --- a/pkg/licensing/expression/category.go +++ b/pkg/licensing/expression/category.go @@ -1,9 +1,12 @@ package expression import ( - "slices" + _ "embed" + "encoding/json" "strings" + "sync" + "github.com/aquasecurity/trivy/pkg/log" "github.com/samber/lo" ) @@ -365,89 +368,15 @@ var ( Unlicense, ZeroBSD, } - - // SpdxLicenseExceptions contains all supported SPDX Exceptions - // cf. https://spdx.org/licenses/exceptions.json - // used `awk -F'"' '/"licenseExceptionId":/ {print toupper("\"" $4 "\"," )}' exceptions.json ` command - spdxLicenseExceptions = []string{ - "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", - } ) var spdxLicenses map[string]struct{} -func initSpdxLicenses() { +var initSpdxLicenses = sync.OnceFunc(func() { + if len(spdxLicenses) > 0 { + return + } + licenseSlices := [][]string{ ForbiddenLicenses, RestrictedLicenses, @@ -473,18 +402,46 @@ func initSpdxLicenses() { license.HasPlus = true spdxLicenses[license.String()] = struct{}{} } +}) -} +//go:embed exceptions.json +var exceptions []byte + +var spdxExceptions map[string]struct{} + +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)) + 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 { if spdxLicenses == nil { initSpdxLicenses() } + if spdxExceptions == nil { + initSpdxExceptions() + } id, exception, ok := strings.Cut(license, " WITH ") - if _, licenseIdFound := spdxLicenses[id]; licenseIdFound && (!ok || slices.Contains(spdxLicenseExceptions, strings.ToUpper(exception))) { - return true + if _, licenseIdFound := spdxLicenses[id]; licenseIdFound { + if !ok { + return true + } + if _, exceptionFound := spdxExceptions[strings.ToUpper(exception)]; exceptionFound { + return true + } } return false } From 784db9e3c7eb70a3a48aeb28b8a1772271e6bbde Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 17 Dec 2024 16:35:09 +0600 Subject: [PATCH 13/18] feat(mage): add command to create exceptions.json file --- magefiles/magefile.go | 7 ++++ magefiles/spdx.go | 79 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 magefiles/spdx.go diff --git a/magefiles/magefile.go b/magefiles/magefile.go index 6adff2d92864..900528b514dc 100644 --- a/magefiles/magefile.go +++ b/magefiles/magefile.go @@ -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") +} diff --git a/magefiles/spdx.go b/magefiles/spdx.go new file mode 100644 index 000000000000..9cf3b5873d27 --- /dev/null +++ b/magefiles/spdx.go @@ -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.josn" + 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 +} From ffb506778a016cd9dfd2ba786b9617629b29849b Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 17 Dec 2024 16:43:52 +0600 Subject: [PATCH 14/18] feat(licensing): add exceptions.json file --- pkg/licensing/expression/exceptions.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 pkg/licensing/expression/exceptions.json diff --git a/pkg/licensing/expression/exceptions.json b/pkg/licensing/expression/exceptions.json new file mode 100644 index 000000000000..c3e85cbde825 --- /dev/null +++ b/pkg/licensing/expression/exceptions.json @@ -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"] \ No newline at end of file From 9f0f7bfa6808b771a1c26f2cd3f6516ef55b4957 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 17 Dec 2024 16:44:28 +0600 Subject: [PATCH 15/18] fix(mage): fix typo --- magefiles/spdx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magefiles/spdx.go b/magefiles/spdx.go index 9cf3b5873d27..f169add48684 100644 --- a/magefiles/spdx.go +++ b/magefiles/spdx.go @@ -18,7 +18,7 @@ import ( ) const ( - exceptionFile = "exceptions.josn" + exceptionFile = "exceptions.json" exceptionDir = "./pkg/licensing/expression" exceptionURL = "https://spdx.org/licenses/exceptions.json" ) From c5563f57c313725b8759699c7814732e4354ea65 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 17 Dec 2024 16:48:24 +0600 Subject: [PATCH 16/18] ci: add spdx-cron --- .github/workflows/spdx-cron.yaml | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/spdx-cron.yaml diff --git a/.github/workflows/spdx-cron.yaml b/.github/workflows/spdx-cron.yaml new file mode 100644 index 000000000000..0cdd86dffa85 --- /dev/null +++ b/.github/workflows/spdx-cron.yaml @@ -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) }} \ No newline at end of file From 0d1310827189855d5f1896e00955ace95b2d8af1 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 17 Dec 2024 16:59:11 +0600 Subject: [PATCH 17/18] ci: add aqua-installer --- .github/workflows/spdx-cron.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/spdx-cron.yaml b/.github/workflows/spdx-cron.yaml index 0cdd86dffa85..c8081db1b49e 100644 --- a/.github/workflows/spdx-cron.yaml +++ b/.github/workflows/spdx-cron.yaml @@ -19,6 +19,14 @@ jobs: 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 From a2ebc32f7172961f8081af2a99f1467d67e568ec Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 17 Dec 2024 17:11:29 +0600 Subject: [PATCH 18/18] fix: linter error --- pkg/licensing/expression/category.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/licensing/expression/category.go b/pkg/licensing/expression/category.go index 9e5e6f91f419..f81d10cec7f2 100644 --- a/pkg/licensing/expression/category.go +++ b/pkg/licensing/expression/category.go @@ -1,13 +1,15 @@ package expression import ( - _ "embed" "encoding/json" "strings" "sync" - "github.com/aquasecurity/trivy/pkg/log" "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/log" + + _ "embed" ) // Canonical names of the licenses.