Compare commits

..
Author SHA1 Message Date
Alex Goodman 47cc8b58a7 persist artifact ID as supplemental package data
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-07 21:51:58 -04:00
244 changed files with 1909 additions and 19996 deletions
+1 -1
View File
@@ -127,7 +127,7 @@ tools:
# used to upload test fixture cache
- name: yq
version:
want: v4.45.4
want: v4.45.2
method: github-release
with:
repo: mikefarah/yq
+1 -1
View File
@@ -29,7 +29,7 @@ runs:
using: "composite"
steps:
# note: go mod and build is automatically cached on default with v4+
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
- uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
if: inputs.go-version != ''
with:
go-version: ${{ inputs.go-version }}
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Install Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 #v5.5.0
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b #v5.4.0
with:
go-version-file: go.mod
+1 -1
View File
@@ -1,4 +1,4 @@
FROM gcr.io/distroless/static-debian12:debug@sha256:5c474275684bbf271ec40502ab50158b2f9826de5877d8feec27e22e8d6ee3d2 AS build
FROM gcr.io/distroless/static-debian11:debug@sha256:a0a404776dec98be120089ae42bbdfbe48c177921d856937d124d48eb8c0b951 AS build
FROM scratch
# needed for version check HTTPS request
+1 -1
View File
@@ -1,4 +1,4 @@
FROM gcr.io/distroless/static-debian12:debug@sha256:5c474275684bbf271ec40502ab50158b2f9826de5877d8feec27e22e8d6ee3d2
FROM gcr.io/distroless/static-debian11:debug@sha256:a0a404776dec98be120089ae42bbdfbe48c177921d856937d124d48eb8c0b951
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
+1 -2
View File
@@ -221,8 +221,7 @@ tasks:
cmds:
- "go test -v ./cmd/syft/internal/test/integration"
# exercise most of the CLI with the data race detector
# we use a larger image to ensure we're using multiple catalogers at a time
- "go run -race cmd/syft/main.go anchore/test_images:grype-quality-dotnet-69f15d2"
- "go run -race cmd/syft/main.go alpine:latest"
validate-cyclonedx-schema:
desc: Validate that Syft produces valid CycloneDX documents
+2 -2
View File
@@ -158,8 +158,8 @@ func (cfg Catalog) ToFilesConfig() filecataloging.Config {
func (cfg Catalog) ToLicenseConfig() cataloging.LicenseConfig {
return cataloging.LicenseConfig{
IncludeContent: cfg.License.Content,
Coverage: cfg.License.Coverage,
IncludeUnkownLicenseContent: cfg.License.IncludeUnknownLicenseContent,
Coverage: cfg.License.LicenseCoverage,
}
}
+7 -58
View File
@@ -1,22 +1,12 @@
package options
import (
"fmt"
"github.com/anchore/clio"
"github.com/anchore/syft/syft/cataloging"
)
type licenseConfig struct {
Content cataloging.LicenseContent `yaml:"content" json:"content" mapstructure:"content"`
// Deprecated: please use include-license-content instead
IncludeUnknownLicenseContent *bool `yaml:"-" json:"-" mapstructure:"include-unknown-license-content"`
Coverage float64 `yaml:"coverage" json:"coverage" mapstructure:"coverage"`
// Deprecated: please use coverage instead
LicenseCoverage *float64 `yaml:"license-coverage" json:"license-coverage" mapstructure:"license-coverage"`
AvailableLicenseContent []cataloging.LicenseContent `yaml:"-" json:"-" mapstructure:"-"`
IncludeUnknownLicenseContent bool `yaml:"include-unknown-license-content" json:"include-unknown-license-content" mapstructure:"include-unknown-license-content"`
LicenseCoverage float64 `yaml:"license-coverage" json:"license-coverage" mapstructure:"license-coverage"`
}
var _ interface {
@@ -24,56 +14,15 @@ var _ interface {
} = (*licenseConfig)(nil)
func (o *licenseConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.Content, fmt.Sprintf("include the content of licenses in the SBOM for a given syft scan; valid values are: %s", o.AvailableLicenseContent))
descriptions.Add(&o.IncludeUnknownLicenseContent, `deprecated: please use 'license-content' instead`)
descriptions.Add(&o.Coverage, `adjust the percent as a fraction of the total text, in normalized words, that
descriptions.Add(&o.IncludeUnknownLicenseContent, `include the content of a license in the SBOM when syft
cannot determine a valid SPDX ID for the given license`)
descriptions.Add(&o.LicenseCoverage, `adjust the percent as a fraction of the total text, in normalized words, that
matches any valid license for the given inputs, expressed as a percentage across all of the licenses matched.`)
descriptions.Add(&o.LicenseCoverage, `deprecated: please use 'coverage' instead`)
}
func (o *licenseConfig) PostLoad() error {
cfg := cataloging.DefaultLicenseConfig()
defaultContent := cfg.IncludeContent
defaultCoverage := cfg.Coverage
// if both legacy and new fields are specified, error out
if o.IncludeUnknownLicenseContent != nil && o.Content != defaultContent {
return fmt.Errorf("both 'include-unknown-license-content' and 'content' are set, please use only 'content'")
}
if o.LicenseCoverage != nil && o.Coverage != defaultCoverage {
return fmt.Errorf("both 'license-coverage' and 'coverage' are set, please use only 'coverage'")
}
// finalize the license content value
if o.IncludeUnknownLicenseContent != nil {
// convert 'include-unknown-license-content' -> 'license-content'
v := cataloging.LicenseContentExcludeAll
if *o.IncludeUnknownLicenseContent {
v = cataloging.LicenseContentIncludeUnknown
}
o.Content = v
}
// finalize the coverage value
if o.LicenseCoverage != nil {
// convert 'license-coverage' -> 'coverage'
o.Coverage = *o.LicenseCoverage
}
return nil
}
func defaultLicenseConfig() licenseConfig {
cfg := cataloging.DefaultLicenseConfig()
return licenseConfig{
Content: cfg.IncludeContent,
Coverage: cfg.Coverage,
AvailableLicenseContent: []cataloging.LicenseContent{
cataloging.LicenseContentIncludeAll,
cataloging.LicenseContentIncludeUnknown,
cataloging.LicenseContentExcludeAll,
},
IncludeUnknownLicenseContent: false,
LicenseCoverage: 75,
}
}
-2
View File
@@ -12,7 +12,6 @@ import (
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/cyclonedxxml"
"github.com/anchore/syft/syft/format/github"
"github.com/anchore/syft/syft/format/purls"
"github.com/anchore/syft/syft/format/spdxjson"
"github.com/anchore/syft/syft/format/spdxtagvalue"
"github.com/anchore/syft/syft/format/syftjson"
@@ -128,7 +127,6 @@ func supportedIDs() []sbom.FormatID {
table.ID,
text.ID,
template.ID,
purls.ID,
// encoders that support multiple versions
cyclonedxxml.ID,
@@ -501,13 +501,6 @@ var commonTestCases = []testCase{
"Akismet Anti-spam: Spam Protection": "5.3",
},
},
{
name: "find homebrew",
pkgType: pkg.HomebrewPkg,
pkgInfo: map[string]string{
"afflib": "1.2.3",
},
},
{
name: "find php pear/pecl package",
pkgType: pkg.PhpPearPkg,
@@ -1,2 +0,0 @@
desc "Advanced Forensic Format"
homepage "https://github.com/sshock/AFFLIBv3"
+1 -36
View File
@@ -1,12 +1,9 @@
package cmptest
import (
"strings"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/anchore/syft/internal/evidence"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
@@ -32,41 +29,9 @@ func BuildOptions(licenseCmp LicenseComparer, locationCmp LocationComparer) []cm
cmpopts.IgnoreFields(pkg.Package{}, "id"), // note: ID is not deterministic for test purposes
cmpopts.SortSlices(pkg.Less),
cmpopts.SortSlices(DefaultRelationshipComparer),
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](locationCmp, locationSorter)),
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](locationCmp)),
cmp.Comparer(buildSetComparer[pkg.License, pkg.LicenseSet](licenseCmp)),
cmp.Comparer(locationCmp),
cmp.Comparer(licenseCmp),
}
}
// LocationSorter always sorts by evidence annotations first, then by access path, then by real path.
// This intentionally does not consider layer details since some test fixtures have no layer information
// on the left side of the comparison (expected) and does on the right side (actual).
func locationSorter(a, b file.Location) int {
// compare by evidence annotations first...
aEvidence := a.Annotations[evidence.AnnotationKey]
bEvidence := b.Annotations[evidence.AnnotationKey]
if aEvidence != bEvidence {
if aEvidence == evidence.PrimaryAnnotation {
return -1
}
if bEvidence == evidence.PrimaryAnnotation {
return 1
}
if aEvidence > bEvidence {
return -1
}
if bEvidence > aEvidence {
return 1
}
}
// ...then by paths
if a.AccessPath != b.AccessPath {
return strings.Compare(a.AccessPath, b.AccessPath)
}
return strings.Compare(a.RealPath, b.RealPath)
}
+2 -2
View File
@@ -13,7 +13,7 @@ func DefaultLicenseComparer(x, y pkg.License) bool {
return cmp.Equal(
x, y,
cmp.Comparer(DefaultLocationComparer),
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](DefaultLocationComparer, locationSorter)),
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](DefaultLocationComparer)),
)
}
@@ -21,6 +21,6 @@ func LicenseComparerWithoutLocationLayer(x, y pkg.License) bool {
return cmp.Equal(
x, y,
cmp.Comparer(LocationComparerWithoutLayer),
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](LocationComparerWithoutLayer, locationSorter)),
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](LocationComparerWithoutLayer)),
)
}
+1 -2
View File
@@ -2,7 +2,6 @@ package cmptest
import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/anchore/syft/syft/file"
)
@@ -10,7 +9,7 @@ import (
type LocationComparer func(x, y file.Location) bool
func DefaultLocationComparer(x, y file.Location) bool {
return cmp.Equal(x.Coordinates, y.Coordinates, cmpopts.IgnoreUnexported(file.Coordinates{})) && cmp.Equal(x.AccessPath, y.AccessPath)
return cmp.Equal(x.Coordinates, y.Coordinates) && cmp.Equal(x.AccessPath, y.AccessPath)
}
func LocationComparerWithoutLayer(x, y file.Location) bool {
+4 -4
View File
@@ -1,13 +1,13 @@
package cmptest
type slicer[T any] interface {
ToSlice(sorter ...func(a, b T) int) []T
ToSlice() []T
}
func buildSetComparer[T any, S slicer[T]](l func(x, y T) bool, sorters ...func(a, b T) int) func(x, y S) bool {
func buildSetComparer[T any, S slicer[T]](l func(x, y T) bool) func(x, y S) bool {
return func(x, y S) bool {
xs := x.ToSlice(sorters...)
ys := y.ToSlice(sorters...)
xs := x.ToSlice()
ys := y.ToSlice()
if len(xs) != len(ys) {
return false
+1 -1
View File
@@ -3,5 +3,5 @@ package internal
const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "16.0.32"
JSONSchemaVersion = "16.0.28"
)
+3 -3
View File
@@ -8,7 +8,7 @@ import (
)
func TestSetContextLicenseScanner(t *testing.T) {
scanner := testScanner()
scanner := testScanner(true)
ctx := context.Background()
ctx = SetContextLicenseScanner(ctx, scanner)
@@ -20,7 +20,7 @@ func TestSetContextLicenseScanner(t *testing.T) {
}
func TestIsContextLicenseScannerSet(t *testing.T) {
scanner := testScanner()
scanner := testScanner(true)
ctx := context.Background()
require.False(t, IsContextLicenseScannerSet(ctx))
@@ -30,7 +30,7 @@ func TestIsContextLicenseScannerSet(t *testing.T) {
func TestContextLicenseScanner(t *testing.T) {
t.Run("with scanner", func(t *testing.T) {
scanner := testScanner()
scanner := testScanner(true)
ctx := SetContextLicenseScanner(context.Background(), scanner)
s, err := ContextLicenseScanner(ctx)
if err != nil || s != scanner {
-36
View File
@@ -1,36 +0,0 @@
package licenses
import (
"context"
"io"
)
func (s *scanner) FindEvidence(_ context.Context, reader io.Reader) (evidence []Evidence, content []byte, err error) {
if s.scanner == nil {
return nil, nil, nil
}
content, err = io.ReadAll(reader)
if err != nil {
return nil, nil, err
}
cov := s.scanner(content)
if cov.Percent < s.coverageThreshold {
// unknown or no licenses here
// => check return content to Search to process
return nil, content, nil
}
evidence = make([]Evidence, 0)
for _, m := range cov.Match {
evidence = append(evidence, Evidence{
ID: m.ID,
Type: m.Type,
Start: m.Start,
End: m.End,
IsURL: m.IsURL,
})
}
return evidence, content, nil
}
-81
View File
@@ -1,81 +0,0 @@
package licenses
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/google/licensecheck"
"github.com/stretchr/testify/require"
)
func TestDefaultScanner_FindEvidence(t *testing.T) {
testCases := []struct {
name string
fixture string
wantIDs []string // expected license IDs
minMatch int // minimum # of matches required
}{
{
name: "Single licenses are able to be recognized and returned Apache 2.0",
fixture: "test-fixtures/apache-license-2.0",
wantIDs: []string{"Apache-2.0"},
minMatch: 1,
},
{
name: "Multiple Licenses are returned as evidence with duplicates at different offset",
fixture: "test-fixtures/multi-license",
wantIDs: []string{
"MIT",
"MIT",
"NCSA",
"Apache-2.0",
"Zlib",
"Unlicense",
"BSD-2-Clause",
"BSD-2-Clause",
"BSD-3-Clause",
},
minMatch: 2,
},
}
scanner := testScanner()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
filePath := filepath.Clean(tc.fixture)
f, err := os.Open(filePath)
require.NoError(t, err)
defer f.Close()
evidence, content, err := scanner.FindEvidence(context.Background(), f)
require.NoError(t, err)
require.NotEmpty(t, content)
require.GreaterOrEqual(t, len(evidence), tc.minMatch, "expected at least %d matches", tc.minMatch)
var foundIDs []string
for _, ev := range evidence {
foundIDs = append(foundIDs, ev.ID)
}
require.ElementsMatch(t, tc.wantIDs, foundIDs, "expected license IDs %v, but got %v", tc.wantIDs, foundIDs)
})
}
}
func testScanner() Scanner {
return &scanner{
coverageThreshold: DefaultCoverageThreshold,
scanner: licensecheck.Scan,
}
}
func mustOpen(fixture string) []byte {
content, err := os.ReadFile(fixture)
if err != nil {
panic(err)
}
return content
}
+25 -22
View File
@@ -8,37 +8,33 @@ import (
"github.com/google/licensecheck"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
const (
UnknownLicensePrefix = unknownLicenseType + "_"
DefaultCoverageThreshold = 75 // determined by experimentation
unknownLicenseType = "UNKNOWN"
DefaultCoverageThreshold = 75 // determined by experimentation
DefaultIncludeLicenseContent = false
)
type Evidence struct {
ID string // License identifier. (See licenses/README.md.)
Type licensecheck.Type // The type of the license: BSD, MIT, etc.
Start int // Start offset of match in text; match is at text[Start:End].
End int // End offset of match in text.
IsURL bool // Whether match is a URL.
}
type Scanner interface {
FindEvidence(context.Context, io.Reader) ([]Evidence, []byte, error)
IdentifyLicenseIDs(context.Context, io.Reader) ([]string, []byte, error)
FileSearch(context.Context, file.LocationReadCloser) ([]file.License, error)
PkgSearch(context.Context, file.LocationReadCloser) ([]pkg.License, error)
}
var _ Scanner = (*scanner)(nil)
type scanner struct {
coverageThreshold float64 // between 0 and 100
scanner func([]byte) licensecheck.Coverage
coverageThreshold float64 // between 0 and 100
includeLicenseContent bool
scanner func([]byte) licensecheck.Coverage
}
type ScannerConfig struct {
CoverageThreshold float64
Scanner func([]byte) licensecheck.Coverage
CoverageThreshold float64
IncludeLicenseContent bool
Scanner func([]byte) licensecheck.Coverage
}
type Option func(*scanner)
@@ -49,6 +45,12 @@ func WithCoverage(coverage float64) Option {
}
}
func WithIncludeLicenseContent(includeLicenseContent bool) Option {
return func(s *scanner) {
s.includeLicenseContent = includeLicenseContent
}
}
// NewDefaultScanner returns a scanner that uses a new instance of the default licensecheck package scanner.
func NewDefaultScanner(o ...Option) (Scanner, error) {
s, err := licensecheck.NewScanner(licensecheck.BuiltinLicenses())
@@ -56,10 +58,10 @@ func NewDefaultScanner(o ...Option) (Scanner, error) {
log.WithFields("error", err).Trace("unable to create default license scanner")
return nil, fmt.Errorf("unable to create default license scanner: %w", err)
}
newScanner := &scanner{
coverageThreshold: DefaultCoverageThreshold,
scanner: s.Scan,
coverageThreshold: DefaultCoverageThreshold,
includeLicenseContent: DefaultIncludeLicenseContent,
scanner: s.Scan,
}
for _, opt := range o {
@@ -76,7 +78,8 @@ func NewScanner(c *ScannerConfig) (Scanner, error) {
}
return &scanner{
coverageThreshold: c.CoverageThreshold,
scanner: c.Scanner,
coverageThreshold: c.CoverageThreshold,
includeLicenseContent: c.IncludeLicenseContent,
scanner: c.Scanner,
}, nil
}
+84
View File
@@ -0,0 +1,84 @@
package licenses
import (
"bytes"
"context"
"os"
"testing"
"github.com/google/licensecheck"
"github.com/stretchr/testify/require"
)
func TestIdentifyLicenseIDs(t *testing.T) {
type expectation struct {
yieldError bool
ids []string
content []byte
}
tests := []struct {
name string
in string
expected expectation
}{
{
name: "apache license 2.0",
in: `test-fixtures/apache-license-2.0`,
expected: expectation{
yieldError: false,
ids: []string{"Apache-2.0"},
content: nil,
},
},
{
name: "custom license includes content for IdentifyLicenseIDs",
in: "test-fixtures/nvidia-software-and-cuda-supplement",
expected: expectation{
yieldError: false,
ids: []string{},
content: mustOpen("test-fixtures/nvidia-software-and-cuda-supplement"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
content, err := os.ReadFile(test.in)
require.NoError(t, err)
ids, content, err := testScanner(false).IdentifyLicenseIDs(context.TODO(), bytes.NewReader(content))
if test.expected.yieldError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Len(t, ids, len(test.expected.ids))
require.Len(t, content, len(test.expected.content))
if len(test.expected.ids) > 0 {
require.Equal(t, ids, test.expected.ids)
}
if len(test.expected.content) > 0 {
require.Equal(t, content, test.expected.content)
}
}
})
}
}
func testScanner(includeLicenseContent bool) Scanner {
return &scanner{
coverageThreshold: DefaultCoverageThreshold,
includeLicenseContent: includeLicenseContent,
scanner: licensecheck.Scan,
}
}
func mustOpen(fixture string) []byte {
content, err := os.ReadFile(fixture)
if err != nil {
panic(err)
}
return content
}
+123
View File
@@ -0,0 +1,123 @@
package licenses
import (
"context"
"crypto/sha256"
"fmt"
"io"
"strings"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg"
)
const (
unknownLicenseType = "UNKNOWN"
UnknownLicensePrefix = unknownLicenseType + "_"
)
func getCustomLicenseContentHash(contents []byte) string {
hash := sha256.Sum256(contents)
return fmt.Sprintf("%x", hash[:])
}
func (s *scanner) IdentifyLicenseIDs(_ context.Context, reader io.Reader) ([]string, []byte, error) {
if s.scanner == nil {
return nil, nil, nil
}
content, err := io.ReadAll(reader)
if err != nil {
return nil, nil, err
}
cov := s.scanner(content)
if cov.Percent < s.coverageThreshold {
// unknown or no licenses here
// => check return content to Search to process
return nil, content, nil
}
var ids []string
for _, m := range cov.Match {
ids = append(ids, m.ID)
}
return ids, nil, nil
}
// PkgSearch scans the contents of a license file to attempt to determine the type of license it is
func (s *scanner) PkgSearch(ctx context.Context, reader file.LocationReadCloser) (licenses []pkg.License, err error) {
licenses = make([]pkg.License, 0)
ids, content, err := s.IdentifyLicenseIDs(ctx, reader)
if err != nil {
return nil, err
}
// IdentifyLicenseIDs can only return a list of ID or content
// These return values are mutually exclusive.
// If the scanner threshold for matching scores < 75% then we return the license full content
if len(ids) > 0 {
for _, id := range ids {
lic := pkg.NewLicenseFromLocations(id, reader.Location)
lic.Type = license.Concluded
licenses = append(licenses, lic)
}
} else if len(content) > 0 {
// harmonize line endings to unix compatible first:
// 1. \r\n => \n (Windows => UNIX)
// 2. \r => \n (Macintosh => UNIX)
content = []byte(strings.ReplaceAll(strings.ReplaceAll(string(content), "\r\n", "\n"), "\r", "\n"))
lic := pkg.NewLicenseFromLocations(unknownLicenseType, reader.Location)
lic.SPDXExpression = UnknownLicensePrefix + getCustomLicenseContentHash(content)
if s.includeLicenseContent {
lic.Contents = string(content)
}
lic.Type = license.Declared
licenses = append(licenses, lic)
}
return licenses, nil
}
// FileSearch scans the contents of a license file to attempt to determine the type of license it is
func (s *scanner) FileSearch(ctx context.Context, reader file.LocationReadCloser) (licenses []file.License, err error) {
licenses = make([]file.License, 0)
ids, content, err := s.IdentifyLicenseIDs(ctx, reader)
if err != nil {
return nil, err
}
// IdentifyLicenseIDs can only return a list of ID or content
// These return values are mutually exclusive.
// If the scanner threshold for matching scores < 75% then we return the license full content
if len(ids) > 0 {
for _, id := range ids {
lic := file.NewLicense(id)
lic.Type = license.Concluded
licenses = append(licenses, lic)
}
} else if len(content) > 0 {
// harmonize line endings to unix compatible first:
// 1. \r\n => \n (Windows => UNIX)
// 2. \r => \n (Macintosh => UNIX)
content = []byte(strings.ReplaceAll(strings.ReplaceAll(string(content), "\r\n", "\n"), "\r", "\n"))
lic := file.NewLicense(unknownLicenseType)
lic.SPDXExpression = UnknownLicensePrefix + getCustomLicenseContentHash(content)
if s.includeLicenseContent {
lic.Contents = string(content)
}
lic.Type = license.Declared
licenses = append(licenses, lic)
}
return licenses, nil
}
+166
View File
@@ -0,0 +1,166 @@
package licenses
import (
"bytes"
"context"
"io"
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
type bytesReadCloser struct {
bytes.Buffer
}
func (brc *bytesReadCloser) Close() error {
return nil
}
func newBytesReadCloser(data []byte) *bytesReadCloser {
return &bytesReadCloser{
Buffer: *bytes.NewBuffer(data),
}
}
func TestSearchFileLicenses(t *testing.T) {
type expectation struct {
yieldError bool
licenses []file.License
}
tests := []struct {
name string
in string
includeUnkownLicenseContent bool
expected expectation
}{
{
name: "apache license 2.0",
in: "test-fixtures/apache-license-2.0",
expected: expectation{
yieldError: false,
licenses: []file.License{
{
Value: "Apache-2.0",
SPDXExpression: "Apache-2.0",
Type: "concluded",
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := context.TODO()
content, err := os.ReadFile(test.in)
require.NoError(t, err)
s := testScanner(false)
result, err := s.FileSearch(ctx, file.NewLocationReadCloser(file.NewLocation("LICENSE"), io.NopCloser(bytes.NewReader(content))))
if test.expected.yieldError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Len(t, result, len(test.expected.licenses))
if len(test.expected.licenses) > 0 {
require.Equal(t, test.expected.licenses, result)
}
}
})
}
}
func TestSearchPkgLicenses(t *testing.T) {
type expectation struct {
wantErr require.ErrorAssertionFunc
licenses []pkg.License
}
testLocation := file.NewLocation("LICENSE")
tests := []struct {
name string
in string
includeUnkownLicenseContent bool
expected expectation
}{
{
name: "apache license 2.0",
in: "test-fixtures/apache-license-2.0",
expected: expectation{
licenses: []pkg.License{
{
Value: "Apache-2.0",
SPDXExpression: "Apache-2.0",
Type: "concluded",
URLs: nil,
Locations: file.NewLocationSet(testLocation),
Contents: "",
},
},
wantErr: nil,
},
},
{
name: "custom license no content by default",
in: "test-fixtures/nvidia-software-and-cuda-supplement",
expected: expectation{
licenses: []pkg.License{
{
Value: "UNKNOWN",
SPDXExpression: "UNKNOWN_eebcea3ab1d1a28e671de90119ffcfb35fe86951e4af1b17af52b7a82fcf7d0a",
Type: "declared",
URLs: nil,
Locations: file.NewLocationSet(testLocation),
Contents: "",
},
},
wantErr: nil,
},
},
{
name: "custom license with content when scanner has content config",
in: "test-fixtures/nvidia-software-and-cuda-supplement",
includeUnkownLicenseContent: true,
expected: expectation{
licenses: []pkg.License{
{
Value: "UNKNOWN",
SPDXExpression: "UNKNOWN_eebcea3ab1d1a28e671de90119ffcfb35fe86951e4af1b17af52b7a82fcf7d0a",
Type: "declared",
URLs: nil,
Locations: file.NewLocationSet(testLocation),
Contents: string(mustOpen("test-fixtures/nvidia-software-and-cuda-supplement")),
},
},
wantErr: nil,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := context.TODO()
content, err := os.ReadFile(test.in)
require.NoError(t, err)
s := testScanner(test.includeUnkownLicenseContent)
result, err := s.PkgSearch(ctx, file.NewLocationReadCloser(file.NewLocation("LICENSE"), io.NopCloser(bytes.NewReader(content))))
if test.expected.wantErr != nil {
test.expected.wantErr(t, err)
}
require.NoError(t, err)
require.Len(t, result, len(test.expected.licenses))
if len(test.expected.licenses) > 0 {
require.Equal(t, test.expected.licenses, result)
}
})
}
}
@@ -1,445 +0,0 @@
Emscripten is available under 2 licenses, the MIT license and the
University of Illinois/NCSA Open Source License.
Both are permissive open source licenses, with little if any
practical difference between them.
The reason for offering both is that (1) the MIT license is
well-known, while (2) the University of Illinois/NCSA Open Source
License allows Emscripten's code to be integrated upstream into
LLVM, which uses that license, should the opportunity arise.
Additionally, the binaryen project is available under the Apache License
Version 2.0.
The full text of all three licenses follows.
==============================================================================
Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
==============================================================================
Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file.
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal with the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimers.
Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimers
in the documentation and/or other materials provided with the
distribution.
Neither the names of Mozilla,
nor the names of its contributors may be used to endorse
or promote products derived from this Software without specific prior
written permission.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE.
==============================================================================
This program uses portions of Node.js source code located in src/library_path.js,
in accordance with the terms of the MIT license. Node's license follows:
"""
Copyright Joyent, Inc. and other Node contributors. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
"""
==============================================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================
Simple DirectMedia Layer
Copyright (C) 1997-2014 Sam Lantinga <slouken@libsdl.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
==============================================================================
Files: tools/filelock.py
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org>
==============================================================================
Files: tools/eliminator/node_modules/uglify-js/... tools/node_modules/terser/...
Distributed under the BSD license:
Copyright 2012 (c) Mihai Bazon <mihai.bazon@gmail.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above
copyright notice, this list of conditions and the following
disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
==============================================================================
Files: system/include/webgpu/webgpu.h
BSD 3-Clause License
Copyright (c) 2019, "WebGPU native" developers
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
==============================================================================
Copyright (c) 2005-2011 David Schultz <das@FreeBSD.ORG>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
-31
View File
@@ -107,11 +107,6 @@ func finalizePkgCatalogerResults(cfg CatalogingFactoryConfig, resolver file.Path
}
}
// we want to know if the user wants to preserve license content or not in the final SBOM
// note: this looks incorrect, but pkg.License.Content is NOT used to compute the Package ID
// this does NOT change the reproducibility of the Package ID
applyLicenseContentRules(&p, cfg.LicenseConfig)
pkgs[i] = p
}
return pkgs, relationships
@@ -267,29 +262,3 @@ func packageFileOwnershipRelationships(p pkg.Package, resolver file.PathResolver
}
return relationships, nil
}
func applyLicenseContentRules(p *pkg.Package, cfg cataloging.LicenseConfig) {
if p.Licenses.Empty() {
return
}
licenses := p.Licenses.ToSlice()
for i := range licenses {
l := &licenses[i]
switch cfg.IncludeContent {
case cataloging.LicenseContentIncludeUnknown:
// we don't have an SPDX expression, which means we didn't find an SPDX license
// include the unknown licenses content in the final SBOM
if l.SPDXExpression != "" {
licenses[i].Contents = ""
}
case cataloging.LicenseContentExcludeAll:
// clear it all out
licenses[i].Contents = ""
case cataloging.LicenseContentIncludeAll:
// always include the content
}
}
p.Licenses = pkg.NewLicenseSet(licenses...)
}
-108
View File
@@ -117,114 +117,6 @@ func TestFilterNonCompliantPackages(t *testing.T) {
assert.Equal(t, p2, droppedPkgs[0])
}
func TestApplyLicenseContentRules(t *testing.T) {
licenseWithSPDX := pkg.License{
SPDXExpression: "MIT",
Contents: "MIT license content",
}
licenseWithoutSPDX := pkg.License{
Value: "License-Not-A-SPDX-Expression",
Contents: "Non-SPDX license content",
}
tests := []struct {
name string
inputLicenses []pkg.License
cfg cataloging.LicenseConfig
expectedLicenses []pkg.License
}{
{
name: "LicenseContentIncludeUnknown",
inputLicenses: []pkg.License{
licenseWithSPDX,
licenseWithoutSPDX,
},
cfg: cataloging.LicenseConfig{
IncludeContent: cataloging.LicenseContentIncludeUnknown,
},
expectedLicenses: []pkg.License{
{
SPDXExpression: "MIT",
Contents: "", // content cleared for SPDX license
},
{
Value: "License-Not-A-SPDX-Expression",
Contents: "Non-SPDX license content", // content preserved for non-SPDX
},
},
},
{
name: "LicenseContentExcludeAll",
inputLicenses: []pkg.License{
licenseWithSPDX,
licenseWithoutSPDX,
},
cfg: cataloging.LicenseConfig{
IncludeContent: cataloging.LicenseContentExcludeAll,
},
expectedLicenses: []pkg.License{
{
SPDXExpression: "MIT",
Contents: "", // content cleared
},
{
Value: "License-Not-A-SPDX-Expression",
Contents: "", // content cleared
},
},
},
{
name: "IncludeLicenseContentDefault",
inputLicenses: []pkg.License{
licenseWithSPDX,
licenseWithoutSPDX,
},
cfg: cataloging.LicenseConfig{
IncludeContent: cataloging.LicenseContentIncludeAll,
},
expectedLicenses: []pkg.License{
{
SPDXExpression: "MIT",
Contents: "MIT license content", // content preserved
},
{
Value: "License-Not-A-SPDX-Expression",
Contents: "Non-SPDX license content", // content preserved
},
},
},
{
name: "Empty licenses",
inputLicenses: []pkg.License{},
cfg: cataloging.LicenseConfig{
IncludeContent: cataloging.LicenseContentIncludeAll,
},
expectedLicenses: []pkg.License{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
inputPkg := &pkg.Package{
Licenses: pkg.NewLicenseSet(tt.inputLicenses...),
}
inputPkg.SetID()
originalID := inputPkg.ID()
applyLicenseContentRules(inputPkg, tt.cfg)
assert.Equal(t, originalID, inputPkg.ID(), "package ID changed unexpectedly")
actualLicenses := inputPkg.Licenses.ToSlice()
expectedLicenses := pkg.NewLicenseSet(tt.expectedLicenses...).ToSlice()
assert.Equal(t, expectedLicenses, actualLicenses, "license contents do not match expected values")
})
}
}
func TestApplyComplianceRules_DropAndStub(t *testing.T) {
p := pkg.Package{Name: "", Version: ""}
p.SetID()
+1 -3
View File
@@ -17,7 +17,6 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/githubactions"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/haskell"
"github.com/anchore/syft/syft/pkg/cataloger/homebrew"
"github.com/anchore/syft/syft/pkg/cataloger/java"
"github.com/anchore/syft/syft/pkg/cataloger/javascript"
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
@@ -77,7 +76,6 @@ func DefaultPackageTaskFactories() Factories {
// language-specific package declared catalogers ///////////////////////////////////////////////////////////////////////////
newSimplePackageTaskFactory(cpp.NewConanCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "cpp", "conan"),
newSimplePackageTaskFactory(dart.NewPubspecLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
newSimplePackageTaskFactory(dart.NewPubspecCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
newSimplePackageTaskFactory(elixir.NewMixLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "elixir"),
newSimplePackageTaskFactory(erlang.NewRebarLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "erlang"),
newSimplePackageTaskFactory(erlang.NewOTPCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "erlang", "otp"),
@@ -168,7 +166,6 @@ func DefaultPackageTaskFactories() Factories {
newSimplePackageTaskFactory(bitnamiSbomCataloger.NewCataloger, "bitnami", pkgcataloging.InstalledTag, pkgcataloging.ImageTag),
newSimplePackageTaskFactory(wordpress.NewWordpressPluginCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "wordpress"),
newSimplePackageTaskFactory(terraform.NewLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "terraform"),
newSimplePackageTaskFactory(homebrew.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "homebrew"),
// deprecated catalogers ////////////////////////////////////////
// these are catalogers that should not be selectable other than specific inclusion via name or "deprecated" tag (to remain backwards compatible)
@@ -176,5 +173,6 @@ func DefaultPackageTaskFactories() Factories {
newSimplePackageTaskFactory(dotnet.NewDotnetPortableExecutableCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
newSimplePackageTaskFactory(php.NewPeclCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
newSimplePackageTaskFactory(nix.NewStoreCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5 -67
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.0.32/document",
"$id": "anchore.io/schema/syft/json/16.0.28/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@@ -408,49 +408,6 @@
"path"
]
},
"DartPubspec": {
"properties": {
"homepage": {
"type": "string"
},
"repository": {
"type": "string"
},
"documentation": {
"type": "string"
},
"publish_to": {
"type": "string"
},
"environment": {
"$ref": "#/$defs/DartPubspecEnvironment"
},
"platforms": {
"items": {
"type": "string"
},
"type": "array"
},
"ignored_advisories": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"DartPubspecEnvironment": {
"properties": {
"sdk": {
"type": "string"
},
"flutter": {
"type": "string"
}
},
"type": "object"
},
"DartPubspecLockEntry": {
"properties": {
"name": {
@@ -1115,20 +1072,6 @@
},
"type": "object"
},
"HomebrewFormula": {
"properties": {
"tap": {
"type": "string"
},
"homepage": {
"type": "string"
},
"description": {
"type": "string"
}
},
"type": "object"
},
"IDLikes": {
"items": {
"type": "string"
@@ -1448,6 +1391,9 @@
"value": {
"type": "string"
},
"fullText": {
"type": "string"
},
"spdxExpression": {
"type": "string"
},
@@ -1473,6 +1419,7 @@
"type": "object",
"required": [
"value",
"fullText",
"spdxExpression",
"type",
"urls",
@@ -1890,9 +1837,6 @@
{
"$ref": "#/$defs/CocoaPodfileLockEntry"
},
{
"$ref": "#/$defs/DartPubspec"
},
{
"$ref": "#/$defs/DartPubspecLockEntry"
},
@@ -1935,9 +1879,6 @@
{
"$ref": "#/$defs/HaskellHackageStackLockEntry"
},
{
"$ref": "#/$defs/HomebrewFormula"
},
{
"$ref": "#/$defs/JavaArchive"
},
@@ -2325,9 +2266,6 @@
"installedSize": {
"type": "integer"
},
"licenses": {
"type": "string"
},
"files": {
"items": {
"$ref": "#/$defs/PortageFileRecord"
+4 -4
View File
@@ -1,4 +1,4 @@
(registered 2021-11-05, last updated 2025-13-05)
(registered 2021-11-05, last updated 2021-11-05)
Media type name: application
Media subtype name: vnd.syft+json
@@ -60,11 +60,11 @@ General Comments:
Person to contact for further information:
1. Name: Dan Nurmi
2. Email: opensource@anchore.com
1. Name: Dan Luhring
2. Email: dan.luhring@anchore.com
Intended usage: Common
The Syft SBOM format is an open-source software bill of materials specification. It is intended to be exchanged between different parties of the software supply chain.
Author/Change controller: Dan Nurmi, on behalf of Anchore
Author/Change controller: Dan Luhring, on behalf of Anchore
+5 -23
View File
@@ -1,33 +1,15 @@
package cataloging
import (
"github.com/anchore/syft/internal/licenses"
)
// LicenseContent controls when license content should be included in the SBOM.
type LicenseContent string
const (
LicenseContentIncludeAll LicenseContent = "all"
LicenseContentIncludeUnknown LicenseContent = "unknown"
LicenseContentExcludeAll LicenseContent = "none"
)
import "github.com/anchore/syft/internal/licenses"
type LicenseConfig struct {
// IncludeUnknownLicenseContent controls whether the content of a license should be included in the SBOM when the license ID cannot be determined.
// Deprecated: use IncludeContent instead
IncludeUnknownLicenseContent bool `json:"-" yaml:"-" mapstructure:"-"`
// IncludeContent controls whether license copy discovered should be included in the SBOM.
IncludeContent LicenseContent `json:"include-content" yaml:"include-content" mapstructure:"include-content"`
// Coverage is the percentage of text that must match a license for it to be considered a match.
Coverage float64 `json:"coverage" yaml:"coverage" mapstructure:"coverage"`
IncludeUnkownLicenseContent bool `json:"include-unknown-license-content" yaml:"include-unknown-license-content" mapstructure:"include-unknown-license-content"`
Coverage float64 `json:"coverage" yaml:"coverage" mapstructure:"coverage"`
}
func DefaultLicenseConfig() LicenseConfig {
return LicenseConfig{
IncludeContent: LicenseContentExcludeAll,
Coverage: licenses.DefaultCoverageThreshold,
IncludeUnkownLicenseContent: licenses.DefaultIncludeLicenseContent,
Coverage: licenses.DefaultCoverageThreshold,
}
}
+1
View File
@@ -109,6 +109,7 @@ func setupContext(ctx context.Context, cfg *CreateSBOMConfig) (context.Context,
func SetContextLicenseScanner(ctx context.Context, cfg cataloging.LicenseConfig) (context.Context, error) {
// inject a single license scanner and content config for all package cataloging tasks into context
licenseScanner, err := licenses.NewDefaultScanner(
licenses.WithIncludeLicenseContent(cfg.IncludeUnkownLicenseContent),
licenses.WithCoverage(cfg.Coverage),
)
if err != nil {
+7 -28
View File
@@ -59,34 +59,7 @@ func (s CoordinateSet) Paths() []string {
return pathSlice
}
func (s CoordinateSet) ToSlice(sorters ...func(a, b Coordinates) int) []Coordinates {
coordinates := s.ToUnorderedSlice()
var sorted bool
for _, sorter := range sorters {
if sorter == nil {
continue
}
sort.Slice(coordinates, func(i, j int) bool {
return sorter(coordinates[i], coordinates[j]) < 0
})
sorted = true
break
}
if !sorted {
sort.SliceStable(coordinates, func(i, j int) bool {
if coordinates[i].FileSystemID == coordinates[j].FileSystemID {
return coordinates[i].RealPath < coordinates[j].RealPath
}
return coordinates[i].FileSystemID < coordinates[j].FileSystemID
})
}
return coordinates
}
func (s CoordinateSet) ToUnorderedSlice() []Coordinates {
func (s CoordinateSet) ToSlice() []Coordinates {
if s.set == nil {
return nil
}
@@ -96,6 +69,12 @@ func (s CoordinateSet) ToUnorderedSlice() []Coordinates {
coordinates[idx] = v
idx++
}
sort.SliceStable(coordinates, func(i, j int) bool {
if coordinates[i].RealPath == coordinates[j].RealPath {
return coordinates[i].FileSystemID < coordinates[j].FileSystemID
}
return coordinates[i].RealPath < coordinates[j].RealPath
})
return coordinates
}
+2 -23
View File
@@ -1,7 +1,6 @@
package file
import (
"slices"
"sort"
"github.com/gohugoio/hashstructure"
@@ -55,28 +54,7 @@ func (s LocationSet) Contains(l Location) bool {
return ok
}
func (s LocationSet) ToSlice(sorters ...func(a, b Location) int) []Location {
locations := s.ToUnorderedSlice()
var sorted bool
for _, sorter := range sorters {
if sorter == nil {
continue
}
slices.SortFunc(locations, sorter)
sorted = true
break
}
if !sorted {
// though no sorter was passed, we need to guarantee a stable ordering between calls
sort.Sort(Locations(locations))
}
return locations
}
func (s LocationSet) ToUnorderedSlice() []Location {
func (s LocationSet) ToSlice() []Location {
if s.set == nil {
return nil
}
@@ -89,6 +67,7 @@ func (s LocationSet) ToUnorderedSlice() []Location {
}
idx++
}
sort.Sort(Locations(locations))
return locations
}
+2 -2
View File
@@ -89,8 +89,8 @@ func TestLocationSet_SortPaths(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := NewLocationSet(test.input...).ToSlice()
assert.Equal(t, test.expected, actual)
set := NewLocationSet(test.input...)
assert.Equal(t, test.expected, set.ToSlice())
})
}
}
+20 -107
View File
@@ -1,12 +1,6 @@
package file
import (
"strings"
"github.com/anchore/syft/internal/evidence"
)
var locationSorterWithoutLayers = LocationSorter(nil)
import "github.com/anchore/syft/internal/evidence"
type Locations []Location
@@ -15,108 +9,27 @@ func (l Locations) Len() int {
}
func (l Locations) Less(i, j int) bool {
return locationSorterWithoutLayers(l[i], l[j]) < 0
liEvidence := l[i].Annotations[evidence.AnnotationKey]
ljEvidence := l[j].Annotations[evidence.AnnotationKey]
if liEvidence == ljEvidence {
if l[i].RealPath == l[j].RealPath {
if l[i].AccessPath == l[j].AccessPath {
return l[i].FileSystemID < l[j].FileSystemID
}
return l[i].AccessPath < l[j].AccessPath
}
return l[i].RealPath < l[j].RealPath
}
if liEvidence == evidence.PrimaryAnnotation {
return true
}
if ljEvidence == evidence.PrimaryAnnotation {
return false
}
return liEvidence > ljEvidence
}
func (l Locations) Swap(i, j int) {
l[i], l[j] = l[j], l[i]
}
// LocationSorter creates a comparison function (slices.SortFunc) for Location objects based on layer order
func LocationSorter(layers []string) func(a, b Location) int { //nolint:gocognit
var layerOrderByDigest map[string]int
if len(layers) > 0 {
layerOrderByDigest = make(map[string]int)
for i, digest := range layers {
layerOrderByDigest[digest] = i
}
}
return func(a, b Location) int {
// compare by evidence annotations first...
aEvidence := a.Annotations[evidence.AnnotationKey]
bEvidence := b.Annotations[evidence.AnnotationKey]
if aEvidence != bEvidence {
if aEvidence == evidence.PrimaryAnnotation {
return -1
}
if bEvidence == evidence.PrimaryAnnotation {
return 1
}
if aEvidence > bEvidence {
return -1
}
if bEvidence > aEvidence {
return 1
}
}
// ...then by layer order
if layerOrderByDigest != nil {
// we're given layer order details
aLayerIdx, aExists := layerOrderByDigest[a.FileSystemID]
bLayerIdx, bExists := layerOrderByDigest[b.FileSystemID]
if aLayerIdx != bLayerIdx {
if !aExists && !bExists {
return strings.Compare(a.FileSystemID, b.FileSystemID)
}
if !aExists {
return 1
}
if !bExists {
return -1
}
return aLayerIdx - bLayerIdx
}
} else if a.FileSystemID != b.FileSystemID {
// no layer info given, legacy behavior is to sort lexicographically
return strings.Compare(a.FileSystemID, b.FileSystemID)
}
// ...then by paths
if a.AccessPath != b.AccessPath {
return strings.Compare(a.AccessPath, b.AccessPath)
}
return strings.Compare(a.RealPath, b.RealPath)
}
}
// CoordinatesSorter creates a comparison function (slices.SortFunc) for Coordinate objects based on layer order
func CoordinatesSorter(layers []string) func(a, b Coordinates) int {
var layerOrderByDigest map[string]int
if len(layers) > 0 {
layerOrderByDigest = make(map[string]int)
for i, digest := range layers {
layerOrderByDigest[digest] = i
}
}
return func(a, b Coordinates) int {
// ...then by layer order
if layerOrderByDigest != nil {
aLayerIdx, aExists := layerOrderByDigest[a.FileSystemID]
bLayerIdx, bExists := layerOrderByDigest[b.FileSystemID]
if aLayerIdx != bLayerIdx {
if !aExists && !bExists {
return strings.Compare(a.FileSystemID, b.FileSystemID)
}
if !aExists {
return 1
}
if !bExists {
return -1
}
return aLayerIdx - bLayerIdx
}
}
return strings.Compare(a.RealPath, b.RealPath)
}
}
-557
View File
@@ -1,557 +0,0 @@
package file
import (
"fmt"
"slices"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/anchore/syft/internal/evidence"
)
func TestLocationAndCoordinatesSorters(t *testing.T) {
tests := []struct {
name string
layers []string
locs []Location
coords []Coordinates
wantLocs []string
wantCoords []string
}{
{
name: "empty location slice",
layers: []string{"fsid-1"},
locs: []Location{},
coords: []Coordinates{},
wantLocs: []string{},
wantCoords: []string{},
},
{
name: "nil layer list",
layers: nil,
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
AccessPath: "/a",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/b",
FileSystemID: "fsid-2",
},
AccessPath: "/b",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{},
},
},
},
coords: []Coordinates{
{
RealPath: "/a",
FileSystemID: "fsid-1",
},
{
RealPath: "/b",
FileSystemID: "fsid-2",
},
},
wantLocs: []string{
"/a (/a) @ fsid-1 map[]",
"/b (/b) @ fsid-2 map[]",
},
wantCoords: []string{
"/a @ fsid-1",
"/b @ fsid-2",
},
},
{
name: "sort by evidence type only",
layers: []string{"fsid-1"},
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
AccessPath: "/a",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: "",
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
AccessPath: "/b",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.SupportingAnnotation,
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/c",
FileSystemID: "fsid-1",
},
AccessPath: "/c",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
},
coords: []Coordinates{
{
RealPath: "/a",
FileSystemID: "fsid-1",
},
{
RealPath: "/b",
FileSystemID: "fsid-1",
},
{
RealPath: "/c",
FileSystemID: "fsid-1",
},
},
wantLocs: []string{
"/c (/c) @ fsid-1 map[evidence:primary]",
"/b (/b) @ fsid-1 map[evidence:supporting]",
"/a (/a) @ fsid-1 map[evidence:]",
},
wantCoords: []string{
"/a @ fsid-1",
"/b @ fsid-1",
"/c @ fsid-1",
},
},
{
name: "same evidence type, sort by layer",
layers: []string{"fsid-1", "fsid-2", "fsid-3"},
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/a",
FileSystemID: "fsid-3",
},
AccessPath: "/a",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
AccessPath: "/b",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/c",
FileSystemID: "fsid-2",
},
AccessPath: "/c",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
},
coords: []Coordinates{
{
RealPath: "/a",
FileSystemID: "fsid-3",
},
{
RealPath: "/b",
FileSystemID: "fsid-1",
},
{
RealPath: "/c",
FileSystemID: "fsid-2",
},
},
wantLocs: []string{
"/b (/b) @ fsid-1 map[evidence:primary]",
"/c (/c) @ fsid-2 map[evidence:primary]",
"/a (/a) @ fsid-3 map[evidence:primary]",
},
wantCoords: []string{
"/b @ fsid-1",
"/c @ fsid-2",
"/a @ fsid-3",
},
},
{
name: "same evidence and layer, sort by access path",
layers: []string{"fsid-1"},
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/x",
FileSystemID: "fsid-1",
},
AccessPath: "/c",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/y",
FileSystemID: "fsid-1",
},
AccessPath: "/a",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/z",
FileSystemID: "fsid-1",
},
AccessPath: "/b",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
},
coords: []Coordinates{
{
RealPath: "/x",
FileSystemID: "fsid-1",
},
{
RealPath: "/y",
FileSystemID: "fsid-1",
},
{
RealPath: "/z",
FileSystemID: "fsid-1",
},
},
wantLocs: []string{
"/y (/a) @ fsid-1 map[evidence:primary]",
"/z (/b) @ fsid-1 map[evidence:primary]",
"/x (/c) @ fsid-1 map[evidence:primary]",
},
wantCoords: []string{
"/x @ fsid-1",
"/y @ fsid-1",
"/z @ fsid-1",
},
},
{
name: "same evidence, layer, and access path - sort by real path",
layers: []string{"fsid-1"},
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/c",
FileSystemID: "fsid-1",
},
AccessPath: "/same",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
AccessPath: "/same",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
AccessPath: "/same",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
},
coords: []Coordinates{
{
RealPath: "/c",
FileSystemID: "fsid-1",
},
{
RealPath: "/a",
FileSystemID: "fsid-1",
},
{
RealPath: "/b",
FileSystemID: "fsid-1",
},
},
wantLocs: []string{
"/a (/same) @ fsid-1 map[evidence:primary]",
"/b (/same) @ fsid-1 map[evidence:primary]",
"/c (/same) @ fsid-1 map[evidence:primary]",
},
wantCoords: []string{
"/a @ fsid-1",
"/b @ fsid-1",
"/c @ fsid-1",
},
},
{
name: "unknown layers",
layers: []string{"fsid-1", "fsid-2"},
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/a",
FileSystemID: "unknown-1",
},
AccessPath: "/a",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/b",
FileSystemID: "unknown-2",
},
AccessPath: "/b",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{},
},
},
},
coords: []Coordinates{
{
RealPath: "/a",
FileSystemID: "unknown-1",
},
{
RealPath: "/b",
FileSystemID: "unknown-2",
},
},
wantLocs: []string{
"/a (/a) @ unknown-1 map[]",
"/b (/b) @ unknown-2 map[]",
},
wantCoords: []string{
"/a @ unknown-1",
"/b @ unknown-2",
},
},
{
name: "mixed known and unknown layers",
layers: []string{"fsid-1", "fsid-2"},
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
AccessPath: "/a",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/b",
FileSystemID: "unknown",
},
AccessPath: "/b",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{},
},
},
},
coords: []Coordinates{
{
RealPath: "/a",
FileSystemID: "fsid-1",
},
{
RealPath: "/b",
FileSystemID: "unknown",
},
},
wantLocs: []string{
"/a (/a) @ fsid-1 map[]",
"/b (/b) @ unknown map[]",
},
wantCoords: []string{
"/a @ fsid-1",
"/b @ unknown",
},
},
{
name: "evidence comparison when one has none",
layers: []string{"fsid-1"},
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
AccessPath: "/a",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
// No evidence.AnnotationKey
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
AccessPath: "/b",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
},
coords: []Coordinates{
{
RealPath: "/a",
FileSystemID: "fsid-1",
},
{
RealPath: "/b",
FileSystemID: "fsid-1",
},
},
wantLocs: []string{
"/b (/b) @ fsid-1 map[evidence:primary]",
"/a (/a) @ fsid-1 map[]",
},
wantCoords: []string{
"/a @ fsid-1",
"/b @ fsid-1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Run("Location", func(t *testing.T) {
locs := make([]Location, len(tt.locs))
copy(locs, tt.locs)
slices.SortFunc(locs, LocationSorter(tt.layers))
got := make([]string, len(locs))
for i, loc := range locs {
got[i] = fmt.Sprintf("%s (%s) @ %s %s",
loc.RealPath,
loc.AccessPath,
loc.FileSystemID,
loc.LocationMetadata.Annotations)
}
if d := cmp.Diff(tt.wantLocs, got); d != "" {
t.Errorf("LocationSorter() mismatch (-want +got):\n%s", d)
}
})
t.Run("Coordinates", func(t *testing.T) {
coords := make([]Coordinates, len(tt.coords))
copy(coords, tt.coords)
slices.SortFunc(coords, CoordinatesSorter(tt.layers))
got := make([]string, len(coords))
for i, coord := range coords {
got[i] = fmt.Sprintf("%s @ %s",
coord.RealPath,
coord.FileSystemID)
}
if d := cmp.Diff(tt.wantCoords, got); d != "" {
t.Errorf("CoordinatesSorter() mismatch (-want +got):\n%s", d)
}
})
})
}
}
@@ -42,18 +42,17 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
cdxBOM.SerialNumber = uuid.New().URN()
cdxBOM.Metadata = toBomDescriptor(s.Descriptor.Name, s.Descriptor.Version, s.Source)
coordinates, locationSorter := getCoordinates(s)
// Packages
packages := s.Artifacts.Packages.Sorted()
components := make([]cyclonedx.Component, len(packages))
for i, p := range packages {
components[i] = helpers.EncodeComponent(p, locationSorter)
components[i] = helpers.EncodeComponent(p)
}
components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...)
// Files
artifacts := s.Artifacts
coordinates := s.AllCoordinates()
for _, coordinate := range coordinates {
var metadata *file.Metadata
// File Info
@@ -96,21 +95,6 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
return cdxBOM
}
func getCoordinates(s sbom.SBOM) ([]file.Coordinates, func(a, b file.Location) int) {
var layers []string
if m, ok := s.Source.Metadata.(source.ImageMetadata); ok {
for _, l := range m.Layers {
layers = append(layers, l.Digest)
}
}
coordSorter := file.CoordinatesSorter(layers)
coordinates := s.AllCoordinates()
slices.SortFunc(coordinates, coordSorter)
return coordinates, file.LocationSorter(layers)
}
func digestsToHashes(digests []file.Digest) []cyclonedx.Hash {
var hashes []cyclonedx.Hash
for _, digest := range digests {
@@ -5,6 +5,7 @@ import (
"crypto/sha1"
"fmt"
"path"
"regexp"
"slices"
"sort"
"strings"
@@ -14,13 +15,13 @@ import (
"github.com/spdx/tools-golang/spdx"
"github.com/anchore/packageurl-go"
internallicenses "github.com/anchore/syft/internal/licenses"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/mimetype"
"github.com/anchore/syft/internal/relationship"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
formatInternal "github.com/anchore/syft/syft/format/internal"
"github.com/anchore/syft/syft/format/internal/spdxutil/helpers"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
@@ -48,7 +49,7 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document {
name, namespace := helpers.DocumentNameAndNamespace(s.Source, s.Descriptor)
rels := relationship.NewIndex(s.Relationships...)
packages, otherLicenses := toPackages(rels, s.Artifacts.Packages, s)
packages := toPackages(rels, s.Artifacts.Packages, s)
allRelationships := toRelationships(rels.All())
@@ -154,7 +155,7 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document {
Packages: packages,
Files: toFiles(s),
Relationships: allRelationships,
OtherLicenses: convertOtherLicense(otherLicenses),
OtherLicenses: toOtherLicenses(s.Artifacts.Packages),
}
}
@@ -267,34 +268,21 @@ func toRootPackage(s source.Description) *spdx.Package {
}
func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
id := string(identifiable.ID())
if strings.HasPrefix(id, "SPDXRef-") {
// this is already an SPDX ID, no need to change it (except for the prefix)
return spdx.ElementID(helpers.SanitizeElementID(strings.TrimPrefix(id, "SPDXRef-")))
}
maxLen := 40
id := ""
switch it := identifiable.(type) {
case pkg.Package:
if strings.HasPrefix(id, "Package-") {
// this is already an SPDX ID, no need to change it
return spdx.ElementID(helpers.SanitizeElementID(id))
}
switch {
case it.Type != "" && it.Name != "":
id = fmt.Sprintf("Package-%s-%s-%s", it.Type, it.Name, id)
id = fmt.Sprintf("Package-%s-%s-%s", it.Type, it.Name, it.ID())
case it.Name != "":
id = fmt.Sprintf("Package-%s-%s", it.Name, id)
id = fmt.Sprintf("Package-%s-%s", it.Name, it.ID())
case it.Type != "":
id = fmt.Sprintf("Package-%s-%s", it.Type, id)
id = fmt.Sprintf("Package-%s-%s", it.Type, it.ID())
default:
id = fmt.Sprintf("Package-%s", id)
id = fmt.Sprintf("Package-%s", it.ID())
}
case file.Coordinates:
if strings.HasPrefix(id, "File-") {
// this is already an SPDX ID, no need to change it. Note: there is no way to reach this case today
// from within syft, however, this covers future cases where the ID can be overridden
return spdx.ElementID(helpers.SanitizeElementID(id))
}
p := ""
parts := strings.Split(it.RealPath, "/")
for i := len(parts); i > 0; i-- {
@@ -308,7 +296,9 @@ func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
}
p = path.Join(part, p)
}
id = fmt.Sprintf("File-%s-%s", p, id)
id = fmt.Sprintf("File-%s-%s", p, it.ID())
default:
id = string(identifiable.ID())
}
// NOTE: the spdx library prepend SPDXRef-, so we don't do it here
return spdx.ElementID(helpers.SanitizeElementID(id))
@@ -317,18 +307,16 @@ func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
// packages populates all Package Information from the package Collection (see https://spdx.github.io/spdx-spec/3-package-information/)
//
//nolint:funlen
func toPackages(rels *relationship.Index, catalog *pkg.Collection, sbom sbom.SBOM) (results []*spdx.Package, otherLicenses []spdx.OtherLicense) {
otherLicenseSet := helpers.NewSPDXOtherLicenseSet()
func toPackages(rels *relationship.Index, catalog *pkg.Collection, sbom sbom.SBOM) (results []*spdx.Package) {
for _, p := range catalog.Sorted() {
// name should be guaranteed to be unique but semantically useful and stable
// name should be guaranteed to be unique, but semantically useful and stable
id := toSPDXID(p)
// If the Concluded License is not the same as the Declared License, a written explanation should be provided
// in the Comments on License field (section 7.16). With respect to NOASSERTION, a written explanation in
// the Comments on License field (section 7.16) is preferred.
// extract these correctly to the spdx license format
concluded, declared, ol := helpers.License(p)
otherLicenseSet.Add(ol...)
concluded, declared := helpers.License(p)
// two ways to get filesAnalyzed == true:
// 1. syft has generated a sha1 digest for the package itself - usually in the java cataloger
@@ -487,7 +475,7 @@ func toPackages(rels *relationship.Index, catalog *pkg.Collection, sbom sbom.SBO
PackageAttributionTexts: nil,
})
}
return results, otherLicenseSet.ToSlice()
return results
}
func toPackageChecksums(p pkg.Package) ([]spdx.Checksum, bool) {
@@ -614,19 +602,14 @@ func lookupRelationship(ty artifact.RelationshipType) (bool, helpers.Relationshi
func toFiles(s sbom.SBOM) (results []*spdx.File) {
artifacts := s.Artifacts
_, coordinateSorter := formatInternal.GetLocationSorters(s)
coordinates := s.AllCoordinates()
slices.SortFunc(coordinates, coordinateSorter)
for _, c := range coordinates {
for _, coordinates := range s.AllCoordinates() {
var metadata *file.Metadata
if metadataForLocation, exists := artifacts.FileMetadata[c]; exists {
if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists {
metadata = &metadataForLocation
}
var digests []file.Digest
if digestsForLocation, exists := artifacts.FileDigests[c]; exists {
if digestsForLocation, exists := artifacts.FileDigests[coordinates]; exists {
digests = digestsForLocation
}
@@ -642,18 +625,18 @@ func toFiles(s sbom.SBOM) (results []*spdx.File) {
// TODO: add file classifications (?) and content as a snippet
var comment string
if c.FileSystemID != "" {
comment = fmt.Sprintf("layerID: %s", c.FileSystemID)
if coordinates.FileSystemID != "" {
comment = fmt.Sprintf("layerID: %s", coordinates.FileSystemID)
}
relativePath, err := convertAbsoluteToRelative(c.RealPath)
relativePath, err := convertAbsoluteToRelative(coordinates.RealPath)
if err != nil {
log.Debugf("unable to convert relative path '%s' to absolute path: %s", c.RealPath, err)
relativePath = c.RealPath
log.Debugf("unable to convert relative path '%s' to absolute path: %s", coordinates.RealPath, err)
relativePath = coordinates.RealPath
}
results = append(results, &spdx.File{
FileSPDXIdentifier: toSPDXID(c),
FileSPDXIdentifier: toSPDXID(coordinates),
FileComment: comment,
// required, no attempt made to determine license information
LicenseConcluded: noAssertion,
@@ -740,14 +723,81 @@ func toFileTypes(metadata *file.Metadata) (ty []string) {
return ty
}
// other licenses are for licenses from the pkg.Package that do not have a valid SPDX Expression
// OR are an expression that is a single `License-Ref-*`
func toOtherLicenses(catalog *pkg.Collection) []*spdx.OtherLicense {
licenses := map[string]helpers.SPDXLicense{}
for p := range catalog.Enumerate() {
declaredLicenses, concludedLicenses := helpers.ParseLicenses(p.Licenses.ToSlice())
for _, l := range declaredLicenses {
if l.Value != "" {
licenses[l.ID] = l
}
if l.ID != "" && isLicenseRef(l.ID) {
licenses[l.ID] = l
}
}
for _, l := range concludedLicenses {
if l.Value != "" {
licenses[l.ID] = l
}
if l.ID != "" && isLicenseRef(l.ID) {
licenses[l.ID] = l
}
}
}
var result []*spdx.OtherLicense
var ids []string
for licenseID := range licenses {
ids = append(ids, licenseID)
}
slices.Sort(ids)
for _, id := range ids {
license := licenses[id]
value := license.Value
fullText := license.FullText
// handle cases where LicenseRef needs to be included in hasExtractedLicensingInfos
if license.Value == "" {
value, _ = strings.CutPrefix(license.ID, "LicenseRef-")
}
other := &spdx.OtherLicense{
LicenseIdentifier: license.ID,
}
if fullText != "" {
other.ExtractedText = fullText
} else {
other.ExtractedText = value
}
customPrefix := spdxlicense.LicenseRefPrefix + helpers.SanitizeElementID(internallicenses.UnknownLicensePrefix)
if strings.HasPrefix(license.ID, customPrefix) {
other.LicenseName = strings.TrimPrefix(license.ID, customPrefix)
other.LicenseComment = strings.Trim(internallicenses.UnknownLicensePrefix, "-_")
}
result = append(result, other)
}
return result
}
var licenseRefRegEx = regexp.MustCompile(`^LicenseRef-[A-Za-z0-9_-]+$`)
// isSingularLicenseRef checks if the string is a singular LicenseRef-* identifier
func isLicenseRef(s string) bool {
// Match the input string against the regex
return licenseRefRegEx.MatchString(s)
}
// TODO: handle SPDX excludes file case
// f file is an "excludes" file, skip it /* exclude SPDX analysis file(s) */
// see: https://spdx.github.io/spdx-spec/v2.3/package-information/#79-package-verification-code-field
// the above link contains the SPDX algorithm for a package verification code
func newPackageVerificationCode(rels *relationship.Index, p pkg.Package, sbom sbom.SBOM) *spdx.PackageVerificationCode {
// key off of the spdx contains relationship;
// spdx validator will fail if a package claims to contain a file, but no sha1 provided
// if a sha1 for a file is provided, then the validator will fail if the package does not have
// key off of the contains relationship;
// spdx validator will fail if a package claims to contain a file but no sha1 provided
// if a sha1 for a file is provided then the validator will fail if the package does not have
// a package verification code
coordinates := rels.Coordinates(p, artifact.ContainsRelationship)
var digests []file.Digest
@@ -820,15 +870,3 @@ func convertAbsoluteToRelative(absPath string) (string, error) {
return relPath, nil
}
func convertOtherLicense(otherLicenses []spdx.OtherLicense) []*spdx.OtherLicense {
if len(otherLicenses) == 0 {
return nil
}
result := make([]*spdx.OtherLicense, 0, len(otherLicenses))
for i := range otherLicenses {
result = append(result, &otherLicenses[i])
}
return result
}
@@ -1,10 +1,8 @@
package spdxhelpers
import (
"context"
"fmt"
"regexp"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
@@ -738,7 +736,7 @@ func Test_H1Digest(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
catalog := pkg.NewCollection(test.pkg)
pkgs, _ := toPackages(relationship.NewIndex(), catalog, s)
pkgs := toPackages(relationship.NewIndex(), catalog, s)
require.Len(t, pkgs, 1)
for _, p := range pkgs {
if test.expectedDigest == "" {
@@ -755,31 +753,29 @@ func Test_H1Digest(t *testing.T) {
}
func Test_OtherLicenses(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
pkg pkg.Package
expected []spdx.OtherLicense
expected []*spdx.OtherLicense
}{
{
name: "no licenseRef",
pkg: pkg.Package{
Licenses: pkg.NewLicenseSet(),
},
expected: []spdx.OtherLicense{},
expected: nil,
},
{
name: "single licenseRef",
pkg: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "foobar"),
pkg.NewLicense("foobar"),
),
},
expected: []spdx.OtherLicense{
expected: []*spdx.OtherLicense{
{
LicenseIdentifier: "LicenseRef-foobar",
LicenseName: "foobar",
ExtractedText: "NOASSERTION",
ExtractedText: "foobar",
},
},
},
@@ -787,20 +783,18 @@ func Test_OtherLicenses(t *testing.T) {
name: "multiple licenseRef",
pkg: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "internal made up license name"),
pkg.NewLicenseWithContext(ctx, "new apple license 2.0"),
pkg.NewLicense("internal made up license name"),
pkg.NewLicense("new apple license 2.0"),
),
},
expected: []spdx.OtherLicense{
expected: []*spdx.OtherLicense{
{
LicenseIdentifier: "LicenseRef-internal-made-up-license-name",
ExtractedText: "NOASSERTION",
LicenseName: "internal made up license name",
ExtractedText: "internal made up license name",
},
{
LicenseIdentifier: "LicenseRef-new-apple-license-2.0",
ExtractedText: "NOASSERTION",
LicenseName: "new apple license 2.0",
ExtractedText: "new apple license 2.0",
},
},
},
@@ -808,27 +802,31 @@ func Test_OtherLicenses(t *testing.T) {
name: "LicenseRef as a valid spdx expression",
pkg: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "LicenseRef-Fedora-Public-Domain"),
pkg.NewLicense("LicenseRef-Fedora-Public-Domain"),
),
},
expected: []spdx.OtherLicense{},
expected: []*spdx.OtherLicense{
{
LicenseIdentifier: "LicenseRef-Fedora-Public-Domain",
ExtractedText: "Fedora-Public-Domain",
},
},
},
{
name: "LicenseRef as a valid spdx expression does not otherize compound spdx expressions",
pkg: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "(MIT AND LicenseRef-Fedora-Public-Domain)"),
pkg.NewLicense("(MIT AND LicenseRef-Fedora-Public-Domain)"),
),
},
expected: []spdx.OtherLicense{},
expected: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
catalog := pkg.NewCollection(test.pkg)
rels := relationship.NewIndex()
_, otherLicenses := toPackages(rels, catalog, sbom.SBOM{})
otherLicenses := toOtherLicenses(catalog)
require.Len(t, otherLicenses, len(test.expected))
require.Equal(t, test.expected, otherLicenses)
})
@@ -863,34 +861,6 @@ func Test_toSPDXID(t *testing.T) {
},
expected: "Package-npm-some-package",
},
{
name: "package with existing SPDX ID",
it: func() pkg.Package {
p := pkg.Package{
Type: pkg.NpmPkg,
Name: "some-package",
}
// SPDXRef- prefix is removed on decode (when everything is working as it should)
p.OverrideID("Package-npm-some-package-extra!")
return p
}(),
// note: we still sanitize out the "!" which is not allowed in SPDX IDs
expected: "Package-npm-some-package-extra",
},
{
name: "package with existing SPDX Ref",
it: func() pkg.Package {
p := pkg.Package{
Type: pkg.NpmPkg,
Name: "some-package",
}
// someone incorrectly added SPDXRef- prefix
p.OverrideID("SPDXRef-Package-npm-some-package-extra!")
return p
}(),
// note: we still sanitize out the "!" which is not allowed in SPDX IDs
expected: "Package-npm-some-package-extra",
},
}
for _, test := range tests {
@@ -904,19 +874,18 @@ func Test_toSPDXID(t *testing.T) {
}
func Test_otherLicenses(t *testing.T) {
ctx := context.TODO()
pkg1 := pkg.Package{
Name: "first-pkg",
Version: "1.1",
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "MIT"),
pkg.NewLicense("MIT"),
),
}
pkg2 := pkg.Package{
Name: "second-pkg",
Version: "2.2",
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "non spdx license"),
pkg.NewLicense("non spdx license"),
),
}
bigText := `
@@ -926,7 +895,7 @@ func Test_otherLicenses(t *testing.T) {
Name: "third-pkg",
Version: "3.3",
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, bigText),
pkg.NewLicense(bigText),
),
}
@@ -941,25 +910,22 @@ func Test_otherLicenses(t *testing.T) {
expected: nil,
},
{
name: "other licenses must include some original text",
name: "other licenses includes original text",
packages: []pkg.Package{pkg2},
expected: []*spdx.OtherLicense{
{
LicenseIdentifier: "LicenseRef-non-spdx-license",
LicenseName: "non spdx license",
ExtractedText: "NOASSERTION",
ExtractedText: "non spdx license",
},
},
},
{
name: "big licenses get hashed and space is trimmed",
name: "big licenses get hashed",
packages: []pkg.Package{pkg3},
expected: []*spdx.OtherLicense{
{
LicenseIdentifier: "LicenseRef-3f17782eef51ae86f18fdd6832f5918e2b40f688b52c9adc07ba6ec1024ef408",
// Carries through the syft-json license value when we shasum large texts
LicenseName: "LicenseRef-sha256:3f17782eef51ae86f18fdd6832f5918e2b40f688b52c9adc07ba6ec1024ef408",
ExtractedText: strings.TrimSpace(bigText),
LicenseIdentifier: "LicenseRef-e9a1e42833d3e456f147052f4d312101bd171a0798893169fe596ca6b55c049e",
ExtractedText: bigText,
},
},
},
+12 -20
View File
@@ -1,7 +1,6 @@
package spdxhelpers
import (
"context"
"errors"
"fmt"
"net/url"
@@ -19,7 +18,6 @@ import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/internal"
"github.com/anchore/syft/syft/format/internal/spdxutil/helpers"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/linux"
@@ -501,24 +499,18 @@ func extractPkgInfo(p *spdx.Package) pkgInfo {
func toSyftPackage(p *spdx.Package) pkg.Package {
info := extractPkgInfo(p)
sP := &pkg.Package{
Type: info.typ,
Name: p.PackageName,
Version: p.PackageVersion,
Licenses: pkg.NewLicenseSet(parseSPDXLicenses(p)...),
CPEs: extractCPEs(p),
PURL: purlValue(info.purl),
Language: info.lang,
Metadata: extractMetadata(p, info),
Type: info.typ,
Name: p.PackageName,
Version: p.PackageVersion,
Licenses: pkg.NewLicenseSet(parseSPDXLicenses(p)...),
CPEs: extractCPEs(p),
PURL: purlValue(info.purl),
Language: info.lang,
Metadata: extractMetadata(p, info),
SupplementalData: []any{artifact.ID(p.PackageSPDXIdentifier)},
}
internal.Backfill(sP)
if p.PackageSPDXIdentifier != "" {
// always prefer the IDs from the SBOM over derived IDs
sP.OverrideID(artifact.ID(p.PackageSPDXIdentifier))
} else {
sP.SetID()
}
sP.SetID()
return *sP
}
@@ -536,14 +528,14 @@ func parseSPDXLicenses(p *spdx.Package) []pkg.License {
// concluded
if p.PackageLicenseConcluded != helpers.NOASSERTION && p.PackageLicenseConcluded != helpers.NONE && p.PackageLicenseConcluded != "" {
l := pkg.NewLicenseWithContext(context.TODO(), cleanSPDXID(p.PackageLicenseConcluded))
l := pkg.NewLicense(cleanSPDXID(p.PackageLicenseConcluded))
l.Type = license.Concluded
licenses = append(licenses, l)
}
// declared
if p.PackageLicenseDeclared != helpers.NOASSERTION && p.PackageLicenseDeclared != helpers.NONE && p.PackageLicenseDeclared != "" {
l := pkg.NewLicenseWithContext(context.TODO(), cleanSPDXID(p.PackageLicenseDeclared))
l := pkg.NewLicense(cleanSPDXID(p.PackageLicenseDeclared))
l.Type = license.Declared
licenses = append(licenses, l)
}
@@ -589,12 +589,18 @@ func Test_convertToAndFromFormat(t *testing.T) {
got, err := ToSyftModel(doc)
require.NoError(t, err)
for _, p := range got.Artifacts.Packages.Sorted() {
// all decoders should be setting an ID of sorts here (another test will verify the correctness of the value)
assert.NotEmpty(t, p.SupplementalData)
}
if diff := cmp.Diff(&s, got,
cmpopts.IgnoreUnexported(artifact.Relationship{}),
cmpopts.IgnoreUnexported(file.LocationSet{}),
cmpopts.IgnoreUnexported(pkg.Collection{}),
cmpopts.IgnoreUnexported(pkg.Package{}),
cmpopts.IgnoreUnexported(pkg.LicenseSet{}),
cmpopts.IgnoreFields(pkg.Package{}, "SupplementalData"), // this is used by decoders to store additional data from the original format
cmpopts.IgnoreFields(sbom.Artifacts{}, "FileMetadata", "FileDigests"),
); diff != "" {
t.Fatalf("packages do not match:\n%s", diff)
@@ -686,8 +692,9 @@ func Test_directPackageFiles(t *testing.T) {
require.NoError(t, err)
p := pkg.Package{
Name: "some-package",
Version: "1.0.5",
Name: "some-package",
Version: "1.0.5",
SupplementalData: []any{artifact.ID("1")}, // set by the decoders from the original element ID
}
p.OverrideID("1") // the same as the spdxID on the package element
f := file.Location{
@@ -730,32 +737,3 @@ func Test_directPackageFiles(t *testing.T) {
require.Equal(t, s, got)
}
func Test_useSPDXIdentifierOverDerivedSyftArtifactID(t *testing.T) {
doc := &spdx.Document{
SPDXVersion: "SPDX-2.3",
Packages: []*spdx.Package{
{
PackageName: "some-package",
PackageSPDXIdentifier: "1", // important!
PackageVersion: "1.0.5",
Files: []*spdx.File{
{
FileName: "some-file",
FileSPDXIdentifier: "2",
Checksums: []spdx.Checksum{
{
Algorithm: "SHA1",
Value: "a8d733c64f9123",
},
},
},
},
},
},
}
s, err := ToSyftModel(doc)
assert.Nil(t, err)
assert.NotNil(t, s.Artifacts.Packages.Package("1"))
}
-4
View File
@@ -40,10 +40,6 @@ func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error)
return nil, "", "", fmt.Errorf("unsupported cyclonedx json document version")
}
_, err = reader.Seek(0, io.SeekStart)
if err != nil {
return nil, id, version, fmt.Errorf("unable to seek to start of CycloneDX JSON SBOM: %+v", err)
}
doc, err := d.decoder.Decode(reader)
if err != nil {
return nil, id, version, fmt.Errorf("unable to decode cyclonedx json document: %w", err)
@@ -2,7 +2,6 @@ package cyclonedxjson
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
@@ -44,7 +43,6 @@ func TestDecoder_Decode(t *testing.T) {
t.Run(test.file, func(t *testing.T) {
reader, err := os.Open(filepath.Join("test-fixtures", test.file))
require.NoError(t, err)
reset := func() { _, err = reader.Seek(0, io.SeekStart); require.NoError(t, err) }
dec := NewFormatDecoder()
@@ -53,7 +51,6 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, sbom.FormatID(""), formatID)
assert.Equal(t, "", formatVersion)
reset()
_, decodeID, decodeVersion, err := dec.Decode(reader)
require.Error(t, err)
assert.Equal(t, sbom.FormatID(""), decodeID)
@@ -64,7 +61,6 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, ID, formatID)
assert.NotEmpty(t, formatVersion)
reset()
bom, decodeID, decodeVersion, err := dec.Decode(reader)
require.NotNil(t, bom)
require.NoError(t, err)
@@ -24,7 +24,7 @@
},
"components": [
{
"bom-ref": "f04d218ff5ff50db",
"bom-ref": "4dd25c6ee16b729a",
"type": "library",
"name": "package-1",
"version": "1.0.1",
@@ -25,7 +25,7 @@
},
"components": [
{
"bom-ref": "2f52f617f1548337",
"bom-ref": "72567175418f73f8",
"type": "library",
"name": "package-1",
"version": "1.0.1",
-4
View File
@@ -41,10 +41,6 @@ func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error)
return nil, "", "", fmt.Errorf("unsupported cyclonedx xml document version")
}
_, err = reader.Seek(0, io.SeekStart)
if err != nil {
return nil, id, version, fmt.Errorf("unable to seek to start of CycloneDX XML SBOM: %w", err)
}
doc, err := d.decoder.Decode(reader)
if err != nil {
return nil, id, version, fmt.Errorf("unable to decode cyclonedx xml document: %w", err)
-5
View File
@@ -2,7 +2,6 @@ package cyclonedxxml
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
@@ -45,8 +44,6 @@ func TestDecoder_Decode(t *testing.T) {
reader, err := os.Open(filepath.Join("test-fixtures", test.file))
require.NoError(t, err)
reset := func() { _, err = reader.Seek(0, io.SeekStart); require.NoError(t, err) }
dec := NewFormatDecoder()
formatID, formatVersion := dec.Identify(reader)
@@ -54,7 +51,6 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, sbom.FormatID(""), formatID)
assert.Equal(t, "", formatVersion)
reset()
_, decodeID, decodeVersion, err := dec.Decode(reader)
require.Error(t, err)
assert.Equal(t, sbom.FormatID(""), decodeID)
@@ -65,7 +61,6 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, ID, formatID)
assert.NotEmpty(t, formatVersion)
reset()
bom, decodeID, decodeVersion, err := dec.Decode(reader)
require.NotNil(t, bom)
require.NoError(t, err)
@@ -16,7 +16,7 @@
</component>
</metadata>
<components>
<component bom-ref="f04d218ff5ff50db" type="library">
<component bom-ref="4dd25c6ee16b729a" type="library">
<name>package-1</name>
<version>1.0.1</version>
<licenses>
@@ -17,7 +17,7 @@
</component>
</metadata>
<components>
<component bom-ref="2f52f617f1548337" type="library">
<component bom-ref="72567175418f73f8" type="library">
<name>package-1</name>
<version>1.0.1</version>
<licenses>
-2
View File
@@ -5,7 +5,6 @@ import (
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/cyclonedxxml"
"github.com/anchore/syft/syft/format/purls"
"github.com/anchore/syft/syft/format/spdxjson"
"github.com/anchore/syft/syft/format/spdxtagvalue"
"github.com/anchore/syft/syft/format/syftjson"
@@ -25,7 +24,6 @@ func Decoders() []sbom.FormatDecoder {
cyclonedxjson.NewFormatDecoder(),
spdxtagvalue.NewFormatDecoder(),
spdxjson.NewFormatDecoder(),
purls.NewFormatDecoder(),
}
}
-12
View File
@@ -34,10 +34,6 @@ func (c *DecoderCollection) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, stri
var bestID sbom.FormatID
for _, d := range c.decoders {
_, err = reader.Seek(0, io.SeekStart)
if err != nil {
return nil, "", "", fmt.Errorf("unable to seek to start of SBOM: %w", err)
}
id, version := d.Identify(reader)
if id == "" || version == "" {
if id != "" {
@@ -46,10 +42,6 @@ func (c *DecoderCollection) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, stri
continue
}
_, err = reader.Seek(0, io.SeekStart)
if err != nil {
return nil, "", "", fmt.Errorf("unable to seek to start of SBOM: %w", err)
}
return d.Decode(reader)
}
@@ -73,10 +65,6 @@ func (c *DecoderCollection) Identify(r io.Reader) (sbom.FormatID, string) {
}
for _, d := range c.decoders {
_, err = reader.Seek(0, io.SeekStart)
if err != nil {
log.Debugf("unable to seek to start of SBOM: %v", err)
}
id, version := d.Identify(reader)
if id != "" && version != "" {
return id, version
-2
View File
@@ -8,7 +8,6 @@ import (
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/cyclonedxxml"
"github.com/anchore/syft/syft/format/github"
"github.com/anchore/syft/syft/format/purls"
"github.com/anchore/syft/syft/format/spdxjson"
"github.com/anchore/syft/syft/format/spdxtagvalue"
"github.com/anchore/syft/syft/format/syftjson"
@@ -63,7 +62,6 @@ func (o EncodersConfig) Encoders() ([]sbom.FormatEncoder, error) {
l.addWithErr(syftjson.ID)(o.syftJSONEncoders())
l.add(table.ID)(table.NewFormatEncoder())
l.add(text.ID)(text.NewFormatEncoder())
l.add(purls.ID)(purls.NewFormatEncoder())
l.add(github.ID)(github.NewFormatEncoder())
l.addWithErr(cyclonedxxml.ID)(o.cyclonedxXMLEncoders())
l.addWithErr(cyclonedxjson.ID)(o.cyclonedxJSONEncoders())
-2
View File
@@ -37,7 +37,6 @@ func expectedDefaultEncoders() *strset.Set {
expected.Add("syft-table@") // no version
expected.Add("syft-text@") // no version
expected.Add("github-json@") // no version
expected.Add("purls@") // no version
for _, v := range spdxjson.SupportedVersions() {
expected.Add("spdx-json@" + v)
}
@@ -109,7 +108,6 @@ func TestEncodersConfig_Encoders(t *testing.T) {
expected.Add("syft-table@") // no version
expected.Add("syft-text@") // no version
expected.Add("github-json@") // no version
expected.Add("purls@") // no version
expected.Add("spdx-json@" + spdxutil.DefaultVersion)
expected.Add("spdx-tag-value@" + spdxutil.DefaultVersion)
expected.Add("cyclonedx-json@" + cyclonedxutil.DefaultVersion)
-137
View File
@@ -1,137 +0,0 @@
package internal
import (
"fmt"
"regexp"
"slices"
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg"
)
// Backfill takes all information present in the package and attempts to fill in any missing information
// from any available sources, such as the Metadata and PURL.
//
// Backfill does not call p.SetID(), but this needs to be called later to ensure it's up to date
func Backfill(p *pkg.Package) {
if p.PURL == "" {
return
}
purl, err := packageurl.FromString(p.PURL)
if err != nil {
log.Debugf("unable to parse purl: %s: %w", p.PURL, err)
return
}
var cpes []cpe.CPE
epoch := ""
for _, qualifier := range purl.Qualifiers {
switch qualifier.Key {
case pkg.PURLQualifierCPES:
rawCpes := strings.Split(qualifier.Value, ",")
for _, rawCpe := range rawCpes {
c, err := cpe.New(rawCpe, cpe.DeclaredSource)
if err != nil {
log.Debugf("unable to decode cpe %s in purl %s: %w", rawCpe, p.PURL, err)
continue
}
cpes = append(cpes, c)
}
case pkg.PURLQualifierEpoch:
epoch = qualifier.Value
}
}
if p.Type == "" {
p.Type = pkg.TypeFromPURL(p.PURL)
}
if p.Language == "" {
p.Language = pkg.LanguageFromPURL(p.PURL)
}
if p.Name == "" {
p.Name = nameFromPurl(purl)
}
setVersionFromPurl(p, purl, epoch)
if p.Language == pkg.Java {
setJavaMetadataFromPurl(p, purl)
}
for _, c := range cpes {
if slices.Contains(p.CPEs, c) {
continue
}
p.CPEs = append(p.CPEs, c)
}
}
func setJavaMetadataFromPurl(p *pkg.Package, purl packageurl.PackageURL) {
if p.Type != pkg.JavaPkg {
return
}
if purl.Namespace != "" {
if p.Metadata == nil {
p.Metadata = pkg.JavaArchive{}
}
meta, got := p.Metadata.(pkg.JavaArchive)
if got && meta.PomProperties == nil {
meta.PomProperties = &pkg.JavaPomProperties{}
p.Metadata = meta
}
if meta.PomProperties != nil {
// capture the group id from the purl if it is not already set
if meta.PomProperties.ArtifactID == "" {
meta.PomProperties.ArtifactID = purl.Name
}
if meta.PomProperties.GroupID == "" {
meta.PomProperties.GroupID = purl.Namespace
}
if meta.PomProperties.Version == "" {
meta.PomProperties.Version = purl.Version
}
}
}
}
func setVersionFromPurl(p *pkg.Package, purl packageurl.PackageURL, epoch string) {
if p.Version == "" {
p.Version = purl.Version
}
if epoch != "" && p.Type == pkg.RpmPkg && !epochPrefix.MatchString(p.Version) {
p.Version = fmt.Sprintf("%s:%s", epoch, p.Version)
}
}
var epochPrefix = regexp.MustCompile(`^\d+:`)
// nameFromPurl returns the syft package name of the package from the purl. If the purl includes a namespace,
// the name is prefixed as appropriate based on the PURL type
func nameFromPurl(purl packageurl.PackageURL) string {
if !nameExcludesPurlNamespace(purl.Type) && purl.Namespace != "" {
return fmt.Sprintf("%s/%s", purl.Namespace, purl.Name)
}
return purl.Name
}
func nameExcludesPurlNamespace(purlType string) bool {
switch purlType {
case packageurl.TypeAlpine,
packageurl.TypeAlpm,
packageurl.TypeConan,
packageurl.TypeCpan,
packageurl.TypeDebian,
packageurl.TypeMaven,
packageurl.TypeQpkg,
packageurl.TypeRPM,
packageurl.TypeSWID:
return true
}
return false
}
-177
View File
@@ -1,177 +0,0 @@
package internal
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg"
)
func Test_Backfill(t *testing.T) {
tests := []struct {
name string
in pkg.Package
expected pkg.Package
}{
{
name: "npm type",
in: pkg.Package{
PURL: "pkg:npm/test@3.0.0",
},
expected: pkg.Package{
PURL: "pkg:npm/test@3.0.0",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
Name: "test",
Version: "3.0.0",
},
},
{
name: "rpm no epoch",
in: pkg.Package{
PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&upstream=dbus-1.12.8-26.el8.src.rpm",
},
expected: pkg.Package{
PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&upstream=dbus-1.12.8-26.el8.src.rpm",
Type: pkg.RpmPkg,
Name: "dbus-common",
Version: "1.12.8-26.el8",
},
},
{
name: "rpm epoch",
in: pkg.Package{
PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm",
},
expected: pkg.Package{
PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm",
Type: pkg.RpmPkg,
Name: "dbus-common",
Version: "1:1.12.8-26.el8",
},
},
{
name: "bad cpe",
in: pkg.Package{
PURL: "pkg:npm/testp@3.0.0?cpes=cpe:2.3a:testv:testp:3.0.0:*:*:*:*:*:*:*",
},
expected: pkg.Package{
PURL: "pkg:npm/testp@3.0.0?cpes=cpe:2.3a:testv:testp:3.0.0:*:*:*:*:*:*:*",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
Name: "testp",
Version: "3.0.0",
},
},
{
name: "good cpe",
in: pkg.Package{
PURL: "pkg:npm/testp@3.0.0?cpes=cpe:2.3:a:testv:testp:3.0.0:*:*:*:*:*:*:*",
},
expected: pkg.Package{
PURL: "pkg:npm/testp@3.0.0?cpes=cpe:2.3:a:testv:testp:3.0.0:*:*:*:*:*:*:*",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
Name: "testp",
Version: "3.0.0",
CPEs: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "testv",
Product: "testp",
Version: "3.0.0",
},
Source: cpe.DeclaredSource,
},
},
},
},
{
name: "java type",
in: pkg.Package{
PURL: "pkg:maven/org.apache/some-thing@1.2.3",
},
expected: pkg.Package{
PURL: "pkg:maven/org.apache/some-thing@1.2.3",
Type: pkg.JavaPkg,
Language: pkg.Java,
Name: "some-thing",
Version: "1.2.3",
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.apache",
ArtifactID: "some-thing",
Version: "1.2.3",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Backfill(&tt.in)
tt.in.OverrideID("")
require.Equal(t, tt.expected, tt.in)
})
}
}
func Test_nameFromPurl(t *testing.T) {
tests := []struct {
in string
expected string
}{
{
in: "pkg:npm/some-name@3.0.0",
expected: "some-name",
},
{
in: "pkg:maven/org.apache/some-name@1.2.3",
expected: "some-name",
},
{
in: "pkg:deb/debian/some-name@3.0.0",
expected: "some-name",
},
{
in: "pkg:rpm/redhat/some-name@3.0.0",
expected: "some-name",
},
{
in: "pkg:gem/some-name@3.0.0",
expected: "some-name",
},
{
in: "pkg:apk/alpine/some-name@3.0.0",
expected: "some-name",
},
{
in: "pkg:docker/some-org/some-name@3.0.0",
expected: "some-org/some-name",
},
{
in: "pkg:npm/some-name@3.0.0",
expected: "some-name",
},
{
in: "pkg:npm/some-org/some-name@3.0.0",
expected: "some-org/some-name",
},
{
in: "pkg:oci/library/mysql@8.1.0",
expected: "library/mysql",
},
}
for _, tt := range tests {
t.Run(tt.in, func(t *testing.T) {
p, err := packageurl.FromString(tt.in)
require.NoError(t, err)
got := nameFromPurl(p)
require.Equal(t, tt.expected, got)
})
}
}
+13 -1
View File
@@ -1,9 +1,12 @@
package cyclonedxutil
import (
"fmt"
"io"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/format/internal/stream"
)
type Decoder struct {
@@ -17,10 +20,19 @@ func NewDecoder(format cyclonedx.BOMFileFormat) Decoder {
}
func (d Decoder) Decode(r io.Reader) (*cyclonedx.BOM, error) {
reader, err := stream.SeekableReader(r)
if err != nil {
return nil, err
}
doc := &cyclonedx.BOM{
Components: &[]cyclonedx.Component{},
}
err := cyclonedx.NewBOMDecoder(r, d.format).Decode(doc)
if _, err := reader.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("unable to seek to start of CycloneDX SBOM: %w", err)
}
err = cyclonedx.NewBOMDecoder(reader, d.format).Decode(doc)
if err != nil {
return nil, err
}
@@ -1,19 +1,18 @@
package helpers
import (
"fmt"
"reflect"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/internal"
"github.com/anchore/syft/syft/internal/packagemetadata"
"github.com/anchore/syft/syft/pkg"
)
func EncodeComponent(p pkg.Package, locationSorter func(a, b file.Location) int) cyclonedx.Component {
func EncodeComponent(p pkg.Package) cyclonedx.Component {
props := EncodeProperties(p, "syft:package")
if p.Metadata != nil {
@@ -25,7 +24,7 @@ func EncodeComponent(p pkg.Package, locationSorter func(a, b file.Location) int)
}
props = append(props, encodeCPEs(p)...)
locations := p.Locations.ToSlice(locationSorter)
locations := p.Locations.ToSlice()
if len(locations) > 0 {
props = append(props, EncodeProperties(locations, "syft:location")...)
}
@@ -86,11 +85,13 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package {
}
p := &pkg.Package{
Version: c.Version,
Locations: decodeLocations(values),
Licenses: pkg.NewLicenseSet(decodeLicenses(c)...),
CPEs: decodeCPEs(c),
PURL: c.PackageURL,
Name: c.Name,
Version: c.Version,
Locations: decodeLocations(values),
Licenses: pkg.NewLicenseSet(decodeLicenses(c)...),
CPEs: decodeCPEs(c),
PURL: c.PackageURL,
SupplementalData: []any{artifact.ID(c.BOMRef)},
}
DecodeInto(p, values, "syft:package", CycloneDXFields)
@@ -103,50 +104,13 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package {
p.Type = pkg.TypeFromPURL(p.PURL)
}
setPackageName(p, c)
internal.Backfill(p)
p.SetID()
if p.Language == "" {
p.Language = pkg.LanguageFromPURL(p.PURL)
}
return p
}
func setPackageName(p *pkg.Package, c *cyclonedx.Component) {
name := c.Name
if c.Group != "" {
switch p.Type {
case pkg.JavaPkg:
if p.Metadata == nil {
p.Metadata = pkg.JavaArchive{}
}
var pomProperties *pkg.JavaPomProperties
javaMetadata, ok := p.Metadata.(pkg.JavaArchive)
if ok {
pomProperties = javaMetadata.PomProperties
if pomProperties == nil {
pomProperties = &pkg.JavaPomProperties{}
javaMetadata.PomProperties = pomProperties
p.Metadata = javaMetadata
}
}
if pomProperties != nil {
if pomProperties.ArtifactID == "" {
pomProperties.ArtifactID = c.Name
}
if pomProperties.GroupID == "" {
pomProperties.GroupID = c.Group
}
if pomProperties.Version == "" {
pomProperties.Version = p.Version
}
}
default:
name = fmt.Sprintf("%s/%s", c.Group, name)
}
}
p.Name = name
}
func decodeLocations(vals map[string]string) file.LocationSet {
v := Decode(reflect.TypeOf([]file.Location{}), vals, "syft:location", CycloneDXFields)
out, ok := v.([]file.Location)
@@ -152,7 +152,7 @@ func Test_encodeComponentProperties(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := EncodeComponent(test.input, file.LocationSorter(nil))
c := EncodeComponent(test.input)
if test.expected == nil {
if c.Properties != nil {
t.Fatalf("expected no properties, got: %+v", *c.Properties)
@@ -212,7 +212,7 @@ func Test_encodeCompomentType(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.pkg.ID()
p := EncodeComponent(tt.pkg, file.LocationSorter(nil))
p := EncodeComponent(tt.pkg)
assert.Equal(t, tt.want, p)
})
}
@@ -5,6 +5,7 @@ import (
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
@@ -65,16 +66,11 @@ func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[str
case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary:
p := decodeComponent(component)
idMap[component.BOMRef] = p
if component.BOMRef != "" {
// always prefer the IDs from the SBOM over derived IDs
p.OverrideID(artifact.ID(component.BOMRef))
} else {
p.SetID()
}
syftID := p.ID()
syftID := extractSyftPacakgeID(component.BOMRef)
if syftID != "" {
idMap[string(syftID)] = p
idMap[syftID] = p
}
p.SetID()
s.Artifacts.Packages.Add(*p)
}
@@ -85,6 +81,19 @@ func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[str
}
}
func extractSyftPacakgeID(i string) string {
instance, err := packageurl.FromString(i)
if err != nil {
return ""
}
for _, q := range instance.Qualifiers {
if q.Key == "package-id" {
return q.Value
}
}
return ""
}
func linuxReleaseFromComponents(components []cyclonedx.Component) *linux.Release {
for i := range components {
component := &components[i]
@@ -421,39 +421,3 @@ func Test_decodeDependencies(t *testing.T) {
})
}
}
func Test_useBomRefOverDerivedSyftArtifactID(t *testing.T) {
packageWithId := cyclonedx.Component{
BOMRef: "pkg:maven/org.springframework.boot/spring-boot-starter-test?package-id=646a5a71a4abeee0",
Type: cyclonedx.ComponentTypeLibrary,
Name: "spring-boot-starter-test",
Version: "",
PackageURL: "pkg:maven/org.springframework.boot/spring-boot-starter-test",
}
packageWithoutId := cyclonedx.Component{
BOMRef: "pkg:maven/org.springframework.boot/spring-boot-starter-webflux",
Type: cyclonedx.ComponentTypeLibrary,
Name: "spring-boot-starter-webflux",
Version: "",
PackageURL: "pkg:maven/org.springframework.boot/spring-boot-starter-webflux",
}
bom := cyclonedx.BOM{Metadata: nil,
Components: &[]cyclonedx.Component{
packageWithId,
packageWithoutId,
}}
s, err := ToSyftModel(&bom)
assert.Nil(t, err)
assert.NotNil(t, s.Artifacts.Packages.Package("pkg:maven/org.springframework.boot/spring-boot-starter-test?package-id=646a5a71a4abeee0"))
pkgsWithoutID := s.Artifacts.Packages.PackagesByName(packageWithoutId.Name)
assert.Len(t, pkgsWithoutID, 1)
assert.NotEqual(t, "", pkgsWithoutID[0].ID())
}
@@ -1,7 +1,6 @@
package helpers
import (
"context"
"encoding/base64"
"strings"
@@ -59,11 +58,11 @@ func decodeLicenses(c *cyclonedx.Component) []pkg.License {
// these fields are mutually exclusive in the spec
switch {
case l.License != nil && l.License.ID != "":
licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), l.License.ID, l.License.URL))
licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.ID, l.License.URL))
case l.License != nil && l.License.Name != "":
licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), l.License.Name, l.License.URL))
licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.Name, l.License.URL))
case l.Expression != "":
licenses = append(licenses, pkg.NewLicenseWithContext(context.TODO(), l.Expression))
licenses = append(licenses, pkg.NewLicense(l.Expression))
default:
}
}
@@ -1,7 +1,6 @@
package helpers
import (
"context"
"testing"
"github.com/CycloneDX/cyclonedx-go"
@@ -13,7 +12,6 @@ import (
)
func Test_encodeLicense(t *testing.T) {
ctx := context.TODO()
tests := []struct {
name string
input pkg.Package
@@ -27,7 +25,7 @@ func Test_encodeLicense(t *testing.T) {
name: "no SPDX licenses",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "RandomLicense"),
pkg.NewLicense("RandomLicense"),
),
},
expected: &cyclonedx.Licenses{
@@ -42,8 +40,8 @@ func Test_encodeLicense(t *testing.T) {
name: "single SPDX ID and Non SPDX ID",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "mit"),
pkg.NewLicenseWithContext(ctx, "FOOBAR"),
pkg.NewLicense("mit"),
pkg.NewLicense("FOOBAR"),
),
},
expected: &cyclonedx.Licenses{
@@ -63,7 +61,7 @@ func Test_encodeLicense(t *testing.T) {
name: "with complex SPDX license expression",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"),
pkg.NewLicense("MIT AND GPL-3.0-only"),
),
},
expected: &cyclonedx.Licenses{
@@ -76,8 +74,8 @@ func Test_encodeLicense(t *testing.T) {
name: "with multiple complex SPDX license expression",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"),
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only WITH Classpath-exception-2.0"),
pkg.NewLicense("MIT AND GPL-3.0-only"),
pkg.NewLicense("MIT AND GPL-3.0-only WITH Classpath-exception-2.0"),
),
},
expected: &cyclonedx.Licenses{
@@ -90,9 +88,9 @@ func Test_encodeLicense(t *testing.T) {
name: "with multiple URLs and expressions",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromURLsWithContext(ctx, "MIT", "https://opensource.org/licenses/MIT", "https://spdx.org/licenses/MIT.html"),
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"),
pkg.NewLicenseFromURLsWithContext(ctx, "FakeLicense", "htts://someurl.com"),
pkg.NewLicenseFromURLs("MIT", "https://opensource.org/licenses/MIT", "https://spdx.org/licenses/MIT.html"),
pkg.NewLicense("MIT AND GPL-3.0-only"),
pkg.NewLicenseFromURLs("FakeLicense", "htts://someurl.com"),
),
},
expected: &cyclonedx.Licenses{
@@ -125,8 +123,8 @@ func Test_encodeLicense(t *testing.T) {
name: "with multiple values licenses are deduplicated",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "Apache-2"),
pkg.NewLicenseWithContext(ctx, "Apache-2.0"),
pkg.NewLicense("Apache-2"),
pkg.NewLicense("Apache-2.0"),
),
},
expected: &cyclonedx.Licenses{
@@ -141,9 +139,9 @@ func Test_encodeLicense(t *testing.T) {
name: "with multiple URLs and single with no URLs",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "MIT"),
pkg.NewLicenseFromURLsWithContext(ctx, "MIT", "https://opensource.org/licenses/MIT", "https://spdx.org/licenses/MIT.html"),
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"),
pkg.NewLicense("MIT"),
pkg.NewLicenseFromURLs("MIT", "https://opensource.org/licenses/MIT", "https://spdx.org/licenses/MIT.html"),
pkg.NewLicense("MIT AND GPL-3.0-only"),
),
},
expected: &cyclonedx.Licenses{
@@ -169,7 +167,7 @@ func Test_encodeLicense(t *testing.T) {
{
name: "single parenthesized SPDX expression",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, "(MIT OR Apache-2.0)")...),
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValues("(MIT OR Apache-2.0)")...),
},
expected: &cyclonedx.Licenses{
{
@@ -181,7 +179,7 @@ func Test_encodeLicense(t *testing.T) {
name: "single license AND to parenthesized SPDX expression",
// (LGPL-3.0-or-later OR GPL-2.0-or-later OR (LGPL-3.0-or-later AND GPL-2.0-or-later)) AND GFDL-1.3-invariants-or-later
input: pkg.Package{
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, "(LGPL-3.0-or-later OR GPL-2.0-or-later OR (LGPL-3.0-or-later AND GPL-2.0-or-later)) AND GFDL-1.3-invariants-or-later")...),
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValues("(LGPL-3.0-or-later OR GPL-2.0-or-later OR (LGPL-3.0-or-later AND GPL-2.0-or-later)) AND GFDL-1.3-invariants-or-later")...),
},
expected: &cyclonedx.Licenses{
{
@@ -250,6 +248,7 @@ func TestDecodeLicenses(t *testing.T) {
Value: "RandomLicense",
// CycloneDX specification doesn't give a field for determining the license type
Type: license.Declared,
URLs: []string{},
},
},
},
@@ -269,6 +268,7 @@ func TestDecodeLicenses(t *testing.T) {
Value: "MIT",
SPDXExpression: "MIT",
Type: license.Declared,
URLs: []string{},
},
},
},
-17
View File
@@ -1,17 +0,0 @@
package internal
import (
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func GetLocationSorters(s sbom.SBOM) (func(a, b file.Location) int, func(a, b file.Coordinates) int) {
var layers []string
if m, ok := s.Source.Metadata.(source.ImageMetadata); ok {
for _, l := range m.Layers {
layers = append(layers, l.Digest)
}
}
return file.LocationSorter(layers), file.CoordinatesSorter(layers)
}
@@ -1,17 +1,16 @@
package helpers
import (
"sort"
"crypto/sha256"
"fmt"
"strings"
"github.com/spdx/tools-golang/spdx"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg"
)
func License(p pkg.Package) (concluded, declared string, otherLicenses []spdx.OtherLicense) {
func License(p pkg.Package) (concluded, declared string) {
// source: https://spdx.github.io/spdx-spec/v2.3/package-information/#713-concluded-license-field
// The options to populate this field are limited to:
// A valid SPDX License Expression as defined in Annex D;
@@ -22,15 +21,15 @@ func License(p pkg.Package) (concluded, declared string, otherLicenses []spdx.Ot
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
if p.Licenses.Empty() {
return NOASSERTION, NOASSERTION, nil
return NOASSERTION, NOASSERTION
}
// take all licenses and assume an AND expression;
// for information about license expressions see:
// https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/
pc, pd, ol := ParseLicenses(p.Licenses.ToSlice())
pc, pd := ParseLicenses(p.Licenses.ToSlice())
return joinLicenses(pc), joinLicenses(pd), ol
return joinLicenses(pc), joinLicenses(pd)
}
func joinLicenses(licenses []SPDXLicense) string {
@@ -59,32 +58,14 @@ func joinLicenses(licenses []SPDXLicense) string {
}
type SPDXLicense struct {
// Valid SPDX ID OR License Value (should have LicenseRef- prefix and be sanitized)
// OR combination of the above as a valid SPDX License Expression as defined in Annex D.
// https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/
ID string
// If the SPDX license is not on the SPDX License List
LicenseName string
FullText string // 0..1 (Mandatory, one) if there is a License Identifier assigned (LicenseRef).
URLs []string
ID string
Value string
FullText string
}
func ParseLicenses(raw []pkg.License) (concluded, declared []SPDXLicense, otherLicenses []spdx.OtherLicense) {
func ParseLicenses(raw []pkg.License) (concluded, declared []SPDXLicense) {
for _, l := range raw {
candidate := createSPDXLicense(l)
// isCustomLicense determines if the candidate falls under https://spdx.github.io/spdx-spec/v2.3/other-licensing-information-detected/#
// of the SPDX spec, where:
// - we should not have a complex SPDX expression
// - if a single license, it should not be a known license (on the SPDX license list)
if l.SPDXExpression == "" && strings.Contains(candidate.ID, spdxlicense.LicenseRefPrefix) {
otherLicenses = append(otherLicenses, spdx.OtherLicense{
LicenseIdentifier: candidate.ID,
ExtractedText: candidate.FullText,
LicenseName: candidate.LicenseName,
LicenseCrossReferences: candidate.URLs,
})
}
switch l.Type {
case license.Concluded:
concluded = append(concluded, candidate)
@@ -93,70 +74,35 @@ func ParseLicenses(raw []pkg.License) (concluded, declared []SPDXLicense, otherL
}
}
return concluded, declared, otherLicenses
return concluded, declared
}
func createSPDXLicense(l pkg.License) SPDXLicense {
// source: https://spdx.github.io/spdx-spec/v2.3/other-licensing-information-detected/#102-extracted-text-field
// we need to populate this field in the spdx document if we have a license ref
// 0..1 (Mandatory, one) if there is a License Identifier assigned (LicenseRef).
ft := NOASSERTION
if l.Contents != "" {
ft = l.Contents
candidate := SPDXLicense{
ID: generateLicenseID(l),
FullText: l.Contents,
}
return SPDXLicense{
ID: generateLicenseID(l),
LicenseName: l.Value,
FullText: ft,
URLs: l.URLs,
if l.SPDXExpression == "" {
candidate.Value = l.Value
}
return candidate
}
// generateLicenseID generates a license ID for the given license, which is either the license value or the SPDX expression.
func generateLicenseID(l pkg.License) string {
if l.SPDXExpression != "" {
return l.SPDXExpression
}
// syft format includes the algo for the sha in the values
// we can strip this and just make LicenseRef-<sum> for spdx consumption
id := strings.ReplaceAll(l.Value, "sha256:", "")
if !strings.HasPrefix(id, "LicenseRef-") {
id = "LicenseRef-" + id
if l.Value != "" {
return spdxlicense.LicenseRefPrefix + SanitizeElementID(l.Value)
}
return SanitizeElementID(id)
return licenseSum(l.Contents)
}
type SPDXOtherLicenseSet struct {
set map[string]spdx.OtherLicense
}
func NewSPDXOtherLicenseSet() *SPDXOtherLicenseSet {
return &SPDXOtherLicenseSet{
set: make(map[string]spdx.OtherLicense),
func licenseSum(s string) string {
if len(s) <= 64 {
return spdxlicense.LicenseRefPrefix + SanitizeElementID(s)
}
}
func (s *SPDXOtherLicenseSet) Add(licenses ...spdx.OtherLicense) {
for _, l := range licenses {
s.set[l.LicenseIdentifier] = l
}
}
type ByLicenseIdentifier []spdx.OtherLicense
func (o ByLicenseIdentifier) Len() int { return len(o) }
func (o ByLicenseIdentifier) Swap(i, j int) { o[i], o[j] = o[j], o[i] }
func (o ByLicenseIdentifier) Less(i, j int) bool {
return o[i].LicenseIdentifier < o[j].LicenseIdentifier
}
func (s *SPDXOtherLicenseSet) ToSlice() []spdx.OtherLicense {
values := make([]spdx.OtherLicense, 0, len(s.set))
for _, v := range s.set {
values = append(values, v)
}
sort.Sort(ByLicenseIdentifier(values))
return values
hash := sha256.Sum256([]byte(s))
return fmt.Sprintf("%s%x", spdxlicense.LicenseRefPrefix, hash)
}
@@ -1,10 +1,9 @@
package helpers
import (
"context"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/internal/spdxlicense"
@@ -12,7 +11,6 @@ import (
)
func Test_License(t *testing.T) {
ctx := context.TODO()
type expected struct {
concluded string
declared string
@@ -33,7 +31,7 @@ func Test_License(t *testing.T) {
{
name: "no SPDX licenses",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "made-up")),
Licenses: pkg.NewLicenseSet(pkg.NewLicense("made-up")),
},
expected: expected{
concluded: "NOASSERTION",
@@ -43,7 +41,7 @@ func Test_License(t *testing.T) {
{
name: "with SPDX license",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "MIT")),
Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")),
},
expected: struct {
concluded string
@@ -57,8 +55,8 @@ func Test_License(t *testing.T) {
name: "with SPDX license expression",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "MIT"),
pkg.NewLicenseWithContext(ctx, "GPL-3.0-only"),
pkg.NewLicense("MIT"),
pkg.NewLicense("GPL-3.0-only"),
),
},
expected: expected{
@@ -71,9 +69,9 @@ func Test_License(t *testing.T) {
name: "includes valid LicenseRef-",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "one thing first"),
pkg.NewLicenseWithContext(ctx, "two things/#$^second"),
pkg.NewLicenseWithContext(ctx, "MIT"),
pkg.NewLicense("one thing first"),
pkg.NewLicense("two things/#$^second"),
pkg.NewLicense("MIT"),
),
},
expected: expected{
@@ -86,9 +84,9 @@ func Test_License(t *testing.T) {
name: "join parentheses correctly",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "one thing first"),
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"),
pkg.NewLicenseWithContext(ctx, "MIT OR APACHE-2.0"),
pkg.NewLicense("one thing first"),
pkg.NewLicense("MIT AND GPL-3.0-only"),
pkg.NewLicense("MIT OR APACHE-2.0"),
),
},
expected: expected{
@@ -100,7 +98,7 @@ func Test_License(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c, d, _ := License(test.input)
c, d := License(test.input)
assert.Equal(t, test.expected.concluded, c)
assert.Equal(t, test.expected.declared, d)
})
@@ -125,13 +123,11 @@ func TestGenerateLicenseID(t *testing.T) {
{
name: "Uses value if no SPDX expression",
license: pkg.License{
Value: "my-sweet-custom-license",
Value: "MIT",
},
expected: spdxlicense.LicenseRefPrefix + "my-sweet-custom-license",
expected: spdxlicense.LicenseRefPrefix + "MIT",
},
{
// note: this is an oversight of the SPDX spec. It does NOT allow "+" in the ID even though they are
// significant to the licenses in the expressions below
name: "Long value is sanitized correctly",
license: pkg.License{
Value: "LGPLv2+ and LGPLv2+ with exceptions and GPLv2+ and GPLv2+ with exceptions and BSD and Inner-Net and ISC and Public Domain and GFDL",
@@ -139,6 +135,13 @@ func TestGenerateLicenseID(t *testing.T) {
expected: spdxlicense.LicenseRefPrefix +
"LGPLv2--and-LGPLv2--with-exceptions-and-GPLv2--and-GPLv2--with-exceptions-and-BSD-and-Inner-Net-and-ISC-and-Public-Domain-and-GFDL",
},
{
name: "Uses hash of contents when nothing else is provided",
license: pkg.License{
Contents: "This is a very long custom license text that should be hashed because it's more than 64 characters long.",
},
expected: "", // We'll verify it starts with the correct prefix
},
}
for _, tt := range tests {
@@ -157,92 +160,34 @@ func TestGenerateLicenseID(t *testing.T) {
func Test_joinLicenses(t *testing.T) {
tests := []struct {
name string
args []SPDXLicense
args []string
want string
}{
{
name: "multiple licenses",
args: []SPDXLicense{{ID: "MIT"}, {ID: "GPL-3.0-only"}},
args: []string{"MIT", "GPL-3.0-only"},
want: "MIT AND GPL-3.0-only",
},
{
name: "multiple licenses with complex expressions",
args: []SPDXLicense{{ID: "MIT AND Apache"}, {ID: "GPL-3.0-only"}},
args: []string{"MIT AND Apache", "GPL-3.0-only"},
want: "(MIT AND Apache) AND GPL-3.0-only",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, joinLicenses(tt.args), "joinLicenses(%v)", tt.args)
assert.Equalf(t, tt.want, joinLicenses(toSpdxLicenses(tt.args)), "joinLicenses(%v)", tt.args)
})
}
}
func TestCreateSPDXLicenseAndGenerateLicenseID(t *testing.T) {
tests := []struct {
name string
input pkg.License
expected SPDXLicense
}{
{
name: "SPDX expression used as ID",
input: pkg.License{
SPDXExpression: "MIT",
Value: "MIT",
Contents: "",
},
expected: SPDXLicense{
ID: "MIT",
LicenseName: "MIT",
FullText: "NOASSERTION",
},
},
{
name: "LicenseRef with contents",
input: pkg.License{
Value: "sha256:123abc",
Contents: "license contents here",
},
expected: SPDXLicense{
ID: "LicenseRef-123abc",
LicenseName: "sha256:123abc",
FullText: "license contents here",
},
},
{
name: "LicenseRef without contents",
input: pkg.License{
Value: "custom-license",
Contents: "",
},
expected: SPDXLicense{
ID: "LicenseRef-custom-license",
LicenseName: "custom-license",
FullText: "NOASSERTION",
},
},
{
name: "URL is passed through",
input: pkg.License{
SPDXExpression: "MIT",
URLs: []string{
"https://example.com/license",
},
},
expected: SPDXLicense{
ID: "MIT",
FullText: "NOASSERTION",
URLs: []string{"https://example.com/license"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
license := createSPDXLicense(tt.input)
if d := cmp.Diff(tt.expected, license); d != "" {
t.Errorf("createSPDXLicense() mismatch (-want +got):\n%s", d)
}
})
func toSpdxLicenses(ids []string) (licenses []SPDXLicense) {
for _, l := range ids {
license := SPDXLicense{ID: l}
if strings.HasPrefix(l, spdxlicense.LicenseRefPrefix) {
license.Value = l
}
licenses = append(licenses, license)
}
return licenses
}
@@ -127,7 +127,6 @@ func Originator(p pkg.Package) (typ string, author string) { //nolint: gocyclo,f
// it seems that the vast majority of the time the author is an org, not a person
typ = orgType
author = metadata.Author
case pkg.SwiplPackEntry:
author = formatPersonOrOrg(metadata.Author, metadata.AuthorEmail)
}
@@ -19,7 +19,6 @@ func Test_OriginatorSupplier(t *testing.T) {
pkg.ConanfileEntry{},
pkg.ConaninfoEntry{},
pkg.DartPubspecLockEntry{},
pkg.DartPubspec{},
pkg.DotnetDepsEntry{},
pkg.DotnetPackagesLockEntry{},
pkg.ELFBinaryPackageNoteJSONPayload{},
@@ -27,7 +26,6 @@ func Test_OriginatorSupplier(t *testing.T) {
pkg.ErlangRebarLockEntry{},
pkg.GolangBinaryBuildinfoEntry{},
pkg.GolangModuleEntry{},
pkg.HomebrewFormula{},
pkg.HackageStackYamlLockEntry{},
pkg.HackageStackYamlEntry{},
pkg.LinuxKernel{},
@@ -76,8 +76,6 @@ func SourceInfo(p pkg.Package) string {
answer = "acquired package info from GitHub Actions workflow file or composite action file"
case pkg.WordpressPluginPkg:
answer = "acquired package info from found wordpress plugin PHP source files"
case pkg.HomebrewPkg:
answer = "acquired package info from Homebrew formula"
case pkg.TerraformPkg:
answer = "acquired package info from Terraform dependency lock file"
default:
@@ -327,14 +327,6 @@ func Test_SourceInfo(t *testing.T) {
"acquired package info from found wordpress plugin PHP source files",
},
},
{
input: pkg.Package{
Type: pkg.HomebrewPkg,
},
expected: []string{
"acquired package info from Homebrew formula",
},
},
{
input: pkg.Package{
Type: pkg.TerraformPkg,
+1 -56
View File
@@ -6,18 +6,13 @@ import (
"io"
)
// SeekableReader takes an io.Reader and returns an io.ReadSeeker relative to the current position of the reader.
// Users of this function expect to be able to reset the reader to the current position, not potentially reset the
// reader prior to the location when this reader is provided. An example is a reader with multiple JSON
// documents separated by newlines (JSONL). After reading the first document, if a call is made to decode
// the second and Seek(0, SeekStart) is called it would reset the overall reader back to the first document.
func SeekableReader(reader io.Reader) (io.ReadSeeker, error) {
if reader == nil {
return nil, fmt.Errorf("no bytes provided")
}
if r, ok := reader.(io.ReadSeeker); ok {
return getOffsetReadSeeker(r)
return r, nil
}
content, err := io.ReadAll(reader)
@@ -27,53 +22,3 @@ func SeekableReader(reader io.Reader) (io.ReadSeeker, error) {
return bytes.NewReader(content), nil
}
type offsetReadSeeker struct {
rdr io.ReadSeeker
offset int64
}
// getOffsetReadSeeker returns a new io.ReadSeeker that may wrap another io.ReadSeeker with the current offset, so
// seek calls will be relative to the _current_ position, rather than relative to the reader itself
func getOffsetReadSeeker(r io.ReadSeeker) (io.ReadSeeker, error) {
if r == nil {
return nil, fmt.Errorf("no reader provided")
}
pos, err := r.Seek(0, io.SeekCurrent)
if pos == 0 {
// if the ReadSeeker is currently at 0, we don't need to track an offset
return r, nil
}
return &offsetReadSeeker{
rdr: r,
offset: pos,
}, err
}
func (o *offsetReadSeeker) Read(p []byte) (n int, err error) {
return o.rdr.Read(p)
}
func (o *offsetReadSeeker) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekStart:
if offset < 0 {
return 0, fmt.Errorf("cannot seek < 0")
}
newOffset, err := o.rdr.Seek(o.offset+offset, io.SeekStart)
return newOffset - o.offset, err
case io.SeekCurrent:
currentOffset, err := o.rdr.Seek(0, io.SeekCurrent)
if err != nil {
return 0, fmt.Errorf("cannot seek current: %w", err)
}
if currentOffset-o.offset+offset < 0 {
return 0, fmt.Errorf("cannot seek < 0")
}
newOffset, err := o.rdr.Seek(offset, io.SeekCurrent)
return newOffset - o.offset, err
}
return 0, fmt.Errorf("only SeekStart and SeekCurrent supported")
}
var _ io.ReadSeeker = (*offsetReadSeeker)(nil)
@@ -3,9 +3,10 @@ package stream
import (
"bytes"
"io"
"strings"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -25,7 +26,7 @@ func TestSeekableReader(t *testing.T) {
name: "empty reader",
input: bytes.NewBuffer([]byte{}), // does not implement io.Seeker (but does implement io.Reader)
assert: func(input io.Reader, got io.ReadSeeker) {
impl, ok := got.(*bytes.Reader) // contents are copied to a byte slice, accessed via bytes.Reader
impl, ok := got.(*bytes.Reader) // implements bytes.Reader
require.True(t, ok)
_, err := impl.Seek(0, io.SeekStart)
require.NoError(t, err)
@@ -36,28 +37,38 @@ func TestSeekableReader(t *testing.T) {
},
{
name: "empty read seeker",
input: strings.NewReader(""), // implements io.ReadSeeker, not offset
input: bytes.NewReader([]byte{}), // implements io.ReadSeeker
assert: func(input io.Reader, got io.ReadSeeker) {
_, ok := got.(*strings.Reader) // same ReadSeeker is returned when not offset
impl, ok := got.(*bytes.Reader)
require.True(t, ok)
_, err := got.Seek(0, io.SeekStart)
_, err := impl.Seek(0, io.SeekStart)
require.NoError(t, err)
content, err := io.ReadAll(got)
content, err := io.ReadAll(impl)
require.NoError(t, err)
require.Equal(t, []byte{}, content)
// assert this is the same read seeker (reflect tt.input pointer is the same as the impl pointer
inputImpl, ok := input.(*bytes.Reader)
require.True(t, ok)
assert.Equal(t, reflect.ValueOf(inputImpl).Pointer(), reflect.ValueOf(impl).Pointer())
},
},
{
name: "non-empty read seeker",
input: strings.NewReader("hello world!"), // implements io.ReadSeeker, not offset
input: bytes.NewReader([]byte("hello world!")), // implements io.ReadSeeker
assert: func(input io.Reader, got io.ReadSeeker) {
_, ok := got.(*strings.Reader) // same ReadSeeker is returned when not offset
impl, ok := got.(*bytes.Reader)
require.True(t, ok)
_, err := got.Seek(0, io.SeekStart)
_, err := impl.Seek(0, io.SeekStart)
require.NoError(t, err)
content, err := io.ReadAll(got)
content, err := io.ReadAll(impl)
require.NoError(t, err)
require.Equal(t, []byte("hello world!"), content)
// assert this is the same read seeker (reflect tt.input pointer is the same as the impl pointer
inputImpl, ok := input.(*bytes.Reader)
require.True(t, ok)
assert.Equal(t, reflect.ValueOf(inputImpl).Pointer(), reflect.ValueOf(impl).Pointer())
},
},
{
@@ -73,32 +84,6 @@ func TestSeekableReader(t *testing.T) {
require.Equal(t, []byte("hello world!"), content)
},
},
{
name: "position zero read seeker",
input: strings.NewReader("a string reader"), // implements io.ReadSeeker at position 0
assert: func(input io.Reader, got io.ReadSeeker) {
_, ok := got.(*strings.Reader) // returns the same ReadSeeker
require.True(t, ok)
_, err := got.Seek(0, io.SeekStart)
require.NoError(t, err)
content, err := io.ReadAll(got)
require.NoError(t, err)
require.Equal(t, []byte("a string reader"), content)
},
},
{
name: "offset read seeker",
input: moveOffset(t, bytes.NewReader([]byte{1, 2, 3, 4, 5}), 3), // implements io.ReadSeeker, with an offset
assert: func(input io.Reader, got io.ReadSeeker) {
_, ok := got.(*offsetReadSeeker) // returns an offset-tracking ReadSeeker
require.True(t, ok)
_, err := got.Seek(0, io.SeekStart)
require.NoError(t, err)
content, err := io.ReadAll(got)
require.NoError(t, err)
require.Equal(t, []byte{4, 5}, content)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -114,113 +99,3 @@ func TestSeekableReader(t *testing.T) {
})
}
}
func Test_offsetReadSeeker(t *testing.T) {
abcd1234 := func() io.ReadSeeker { return strings.NewReader("abcd1234") }
abcd1234offset := func(offset int) func() io.ReadSeeker {
return func() io.ReadSeeker {
r := strings.NewReader("abcd1234")
_, err := r.Seek(int64(offset), io.SeekStart)
require.NoError(t, err)
return r
}
}
tests := []struct {
name string
input func() io.ReadSeeker
seek int64
seek2 int64
whence int
expected string
wantErr require.ErrorAssertionFunc
}{
{
name: "basic reader",
input: abcd1234,
seek: 0,
whence: io.SeekStart,
expected: "abcd1234",
},
{
name: "basic reader offset",
input: abcd1234offset(1),
seek: 0,
whence: io.SeekStart,
expected: "bcd1234",
},
{
name: "basic reader offset both",
input: abcd1234offset(2),
seek: 2,
whence: io.SeekStart,
expected: "1234",
},
{
name: "basic reader offset seek current",
input: abcd1234offset(1),
seek: -1,
whence: io.SeekCurrent,
wantErr: require.Error, // would be < current, which is an error
},
{
name: "valid negative offset from current",
input: abcd1234offset(1),
seek: 2,
seek2: -1,
whence: io.SeekCurrent,
expected: "cd1234",
},
{
name: "basic reader offset multiple",
input: abcd1234offset(2),
seek: 3,
seek2: 2,
whence: io.SeekCurrent,
expected: "4",
},
{
name: "bad whence",
input: abcd1234,
seek: 1,
whence: io.SeekEnd,
wantErr: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rdr := tt.input()
off, err := rdr.Seek(0, io.SeekCurrent)
require.NoError(t, err)
// construct new offsetReadSeeker
sr := offsetReadSeeker{rdr: rdr, offset: off}
_, err = sr.Seek(tt.seek, tt.whence)
if tt.seek2 != 0 {
require.NoError(t, err)
_, err = sr.Seek(tt.seek2, tt.whence)
}
if tt.wantErr != nil {
tt.wantErr(t, err)
return
} else {
require.NoError(t, err)
}
buf := make([]byte, 1024)
n, err := sr.Read(buf)
require.NoError(t, err)
require.Equal(t, tt.expected, string(buf[:n]))
})
}
}
func moveOffset(t *testing.T, reader io.ReadSeeker, offset int64) io.Reader {
pos, err := reader.Seek(offset, io.SeekStart)
require.NoError(t, err)
require.Equal(t, offset, pos)
return reader
}
@@ -1,7 +1,6 @@
package testutil
import (
"context"
"os"
"path/filepath"
"testing"
@@ -99,7 +98,7 @@ func DirectoryInputWithAuthorField(t testing.TB) sbom.SBOM {
func newDirectoryCatalog() *pkg.Collection {
catalog := pkg.NewCollection()
ctx := context.TODO()
// populate catalog with test data
catalog.Add(pkg.Package{
Name: "package-1",
@@ -111,7 +110,7 @@ func newDirectoryCatalog() *pkg.Collection {
),
Language: pkg.Python,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "MIT"),
pkg.NewLicense("MIT"),
),
Metadata: pkg.PythonPackage{
Name: "package-1",
@@ -150,7 +149,7 @@ func newDirectoryCatalog() *pkg.Collection {
func newDirectoryCatalogWithAuthorField() *pkg.Collection {
catalog := pkg.NewCollection()
ctx := context.TODO()
// populate catalog with test data
catalog.Add(pkg.Package{
Name: "package-1",
@@ -162,7 +161,7 @@ func newDirectoryCatalogWithAuthorField() *pkg.Collection {
),
Language: pkg.Python,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "MIT"),
pkg.NewLicense("MIT"),
),
Metadata: pkg.PythonPackage{
Name: "package-1",
+2 -3
View File
@@ -1,7 +1,6 @@
package testutil
import (
"context"
"os"
"path/filepath"
"testing"
@@ -99,7 +98,7 @@ func populateImageCatalog(catalog *pkg.Collection, img *image.Image) {
// TODO: this helper function is coupled to the image-simple fixture, which seems like a bad idea
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
ctx := context.TODO()
// populate catalog with test data
if ref1 != nil {
catalog.Add(pkg.Package{
@@ -112,7 +111,7 @@ func populateImageCatalog(catalog *pkg.Collection, img *image.Image) {
FoundBy: "the-cataloger-1",
Language: pkg.Python,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseWithContext(ctx, "MIT"),
pkg.NewLicense("MIT"),
),
Metadata: pkg.PythonPackage{
Name: "package-1",
-93
View File
@@ -1,93 +0,0 @@
package purls
import (
"bufio"
"errors"
"fmt"
"io"
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/format/internal"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
)
var _ sbom.FormatDecoder = (*decoder)(nil)
type decoder struct{}
func NewFormatDecoder() sbom.FormatDecoder {
return decoder{}
}
func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error) {
if r == nil {
return nil, "", "", fmt.Errorf("no reader provided")
}
s, err := toSyftModel(r)
return s, ID, version, err
}
func (d decoder) Identify(r io.Reader) (sbom.FormatID, string) {
if r == nil {
return "", ""
}
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
// skip whitespace only lines
continue
}
if strings.HasPrefix(line, "pkg:") {
_, err := packageurl.FromString(line)
if err != nil {
log.WithFields("error", err, "line", line).Debug("unable to parse purl")
continue
}
return ID, version
}
// not a purl, so we can't identify the format as a list of purls
return "", ""
}
return "", ""
}
func toSyftModel(r io.Reader) (*sbom.SBOM, error) {
var errs []error
pkgs := pkg.NewCollection()
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
// skip invalid PURLs
_, err := packageurl.FromString(line)
if err != nil {
log.WithFields("error", err, "line", line).Debug("unable to parse purl")
continue
}
p := pkg.Package{
// name, version and other properties set during Backfill
PURL: line,
}
internal.Backfill(&p)
p.SetID()
pkgs.Add(p)
}
return &sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkgs,
},
}, errors.Join(errs...)
}
-241
View File
@@ -1,241 +0,0 @@
package purls
import (
"bytes"
"regexp"
"slices"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/cmptest"
"github.com/anchore/syft/syft/pkg"
)
func TestDecoder_Decode(t *testing.T) {
tests := []struct {
purl string
expected []pkg.Package
}{
{
purl: "pkg:generic/some-package@1.2.3",
expected: []pkg.Package{
{
Name: "some-package",
Type: pkg.UnknownPkg,
Version: "1.2.3",
PURL: "pkg:generic/some-package@1.2.3",
},
},
},
{
purl: "pkg:npm/some-package@1.2.3",
expected: []pkg.Package{
{
Name: "some-package",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
Version: "1.2.3",
PURL: "pkg:npm/some-package@1.2.3",
},
},
},
{
purl: "pkg:apk/curl@7.61.1",
expected: []pkg.Package{
{
Name: "curl",
Version: "7.61.1",
Type: pkg.ApkPkg,
PURL: "pkg:apk/curl@7.61.1",
},
},
},
{
purl: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit",
expected: []pkg.Package{
{
Name: "sysv-rc",
Version: "2.88dsf-59",
Type: pkg.DebPkg,
PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit",
},
},
},
{
purl: "pkg:apk/libcrypto3@3.3.2?upstream=openssl",
expected: []pkg.Package{
{
Name: "libcrypto3",
Version: "3.3.2",
Type: pkg.ApkPkg,
PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl",
},
},
},
{
purl: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1", // %40 is @
expected: []pkg.Package{
{
Name: "libcrypto3",
Version: "3.3.2",
Type: pkg.ApkPkg,
PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1",
},
},
},
{
purl: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm",
expected: []pkg.Package{
{
Name: "systemd-x",
Version: "239-82.el8_10.2",
Type: pkg.RpmPkg,
PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm",
},
},
},
{
purl: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm",
expected: []pkg.Package{
{
Name: "dbus-common",
Version: "1:1.12.8-26.el8",
Type: pkg.RpmPkg,
PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm",
},
},
},
{
purl: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3",
expected: []pkg.Package{
{
Name: "curl",
Version: "7.61.1",
Type: pkg.ApkPkg,
PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3",
},
},
},
{
purl: "pkg:golang/k8s.io/ingress-nginx@v1.11.2",
expected: []pkg.Package{
{
Name: "k8s.io/ingress-nginx",
Version: "v1.11.2",
Type: pkg.GoModulePkg,
Language: pkg.Go,
PURL: "pkg:golang/k8s.io/ingress-nginx@v1.11.2",
},
},
},
{
purl: "pkg:golang/github.com/wazuh/wazuh@v4.5.0",
expected: []pkg.Package{
{
Name: "github.com/wazuh/wazuh",
Version: "v4.5.0",
Type: pkg.GoModulePkg,
PURL: "pkg:golang/github.com/wazuh/wazuh@v4.5.0",
Language: pkg.Go,
},
},
},
{
purl: "pkg:golang/wazuh@v4.5.0",
expected: []pkg.Package{
{
Name: "wazuh",
Version: "v4.5.0",
Type: pkg.GoModulePkg,
PURL: "pkg:golang/wazuh@v4.5.0",
Language: pkg.Go,
},
},
},
{
purl: "pkg:maven/org.apache/some-pkg@4.11.3",
expected: []pkg.Package{
{
Name: "some-pkg",
Version: "4.11.3",
Type: pkg.JavaPkg,
PURL: "pkg:maven/org.apache/some-pkg@4.11.3",
Language: pkg.Java,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.apache",
ArtifactID: "some-pkg",
Version: "4.11.3",
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.purl, func(t *testing.T) {
dec := NewFormatDecoder()
got, _, _, err := dec.Decode(strings.NewReader(test.purl))
require.NoError(t, err)
if diff := cmp.Diff(test.expected, got.Artifacts.Packages.Sorted(), cmptest.DefaultOptions()...); diff != "" {
t.Errorf("expected packages (-want +got):\n%s", diff)
}
})
}
}
func Test_DecodeEncodeCycle(t *testing.T) {
tests := []struct {
name string
input string
}{
{
name: "basic",
input: "pkg:generic/some-package@1.2.3",
},
{
name: "multiple",
input: "pkg:generic/pkg1\npkg:generic/pkg2\n\npkg:npm/@vercel/ncc@2.9.5",
},
{
name: "java",
input: "pkg:maven/org.apache/some-thing@4.11.3",
},
{
name: "leading whitespace",
input: " \n \t pkg:maven/org.apache/some-thing@4.11.3",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dec := NewFormatDecoder()
decodedSBOM, _, _, err := dec.Decode(strings.NewReader(tt.input))
require.NoError(t, err)
var buf bytes.Buffer
enc := NewFormatEncoder()
require.NoError(t, enc.Encode(&buf, *decodedSBOM))
in := strings.TrimSpace(regexp.MustCompile(`\s+`).ReplaceAllString(strings.TrimSpace(tt.input), "\n"))
expected := strings.Split(in, "\n")
slices.Sort(expected)
got := strings.Split(strings.TrimSpace(buf.String()), "\n")
slices.Sort(got)
require.EqualValues(t, expected, got)
for _, item := range got {
// require every result is a valid PURL -- no whitespace lines, etc.
_, err = packageurl.FromString(item)
require.NoError(t, err)
}
})
}
}
-62
View File
@@ -1,62 +0,0 @@
package purls
import (
"io"
"strings"
"github.com/scylladb/go-set/strset"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/sbom"
)
const ID sbom.FormatID = "purls"
const version = "1"
type encoder struct {
}
func NewFormatEncoder() sbom.FormatEncoder {
return encoder{}
}
func (e encoder) ID() sbom.FormatID {
return ID
}
func (e encoder) Aliases() []string {
return []string{
"purl",
}
}
func (e encoder) Version() string {
return sbom.AnyVersion
}
func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error {
output := strset.New()
for _, p := range s.Artifacts.Packages.Sorted() {
purl := strings.TrimSpace(p.PURL)
if purl == "" || output.Has(purl) {
continue
}
// ensure syft doesn't output invalid PURLs in this format
_, err := packageurl.FromString(purl)
if err != nil {
log.Debugf("invalid purl: %q", purl)
continue
}
output.Add(purl)
_, err = writer.Write([]byte(purl))
if err != nil {
return err
}
_, err = writer.Write([]byte("\n"))
if err != nil {
return err
}
}
return nil
}
-60
View File
@@ -1,60 +0,0 @@
package purls
import (
"flag"
"testing"
"github.com/anchore/syft/syft/format/internal/testutil"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
)
var updateSnapshot = flag.Bool("update-purls", false, "update the *.golden files for purls format")
func Test_Encoder(t *testing.T) {
pkgs := []pkg.Package{
{
Name: "npmtest",
Version: "1.5.1",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
PURL: "pkg:npm/npmtest@1.5.1",
},
{
Name: "npmtest2",
Version: "1.5.1",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
PURL: "pkg:npm/npmtest@1.5.1", // duplicate should not be included
},
{
Name: "npmtest",
Version: "3.1.1",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
PURL: "http://npm/npmtest@3.1.1", // invalid PURL should not be included
},
{
Name: "javatest",
Version: "0.30.1",
Type: pkg.JavaPkg,
Language: pkg.Java,
PURL: "pkg:maven/org.apache/javatest@0.30.1",
},
{
Type: pkg.UnknownPkg,
PURL: "pkg:generic/generic@1.2.3",
},
}
testutil.AssertEncoderAgainstGoldenSnapshot(t,
testutil.EncoderSnapshotTestConfig{
Subject: sbom.SBOM{Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(pkgs...),
}},
Format: NewFormatEncoder(),
UpdateSnapshot: *updateSnapshot,
PersistRedactionsInSnapshot: true,
IsJSON: false,
},
)
}
@@ -1,3 +0,0 @@
pkg:generic/generic@1.2.3
pkg:maven/org.apache/javatest@0.30.1
pkg:npm/npmtest@1.5.1
-5
View File
@@ -2,7 +2,6 @@ package spdxjson
import (
"fmt"
"io"
"os"
"path/filepath"
"testing"
@@ -92,8 +91,6 @@ func TestDecoder_Decode(t *testing.T) {
reader, err := os.Open(filepath.Join("test-fixtures", "spdx", test.name))
require.NoError(t, err)
reset := func() { _, err = reader.Seek(0, io.SeekStart); require.NoError(t, err) }
dec := NewFormatDecoder()
formatID, formatVersion := dec.Identify(reader)
@@ -101,7 +98,6 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, test.id, formatID)
assert.Equal(t, test.version, formatVersion)
reset()
_, decodeID, decodeVersion, err := dec.Decode(reader)
require.Error(t, err)
assert.Equal(t, test.id, decodeID)
@@ -112,7 +108,6 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, test.id, formatID)
assert.Equal(t, test.version, formatVersion)
reset()
s, decodeID, decodeVersion, err := dec.Decode(reader)
require.NoError(t, err)
@@ -15,7 +15,7 @@
"packages": [
{
"name": "package-1",
"SPDXID": "SPDXRef-Package-python-package-1-f04d218ff5ff50db",
"SPDXID": "SPDXRef-Package-python-package-1-4dd25c6ee16b729a",
"versionInfo": "1.0.1",
"supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION",
@@ -76,7 +76,7 @@
"relationships": [
{
"spdxElementId": "SPDXRef-DocumentRoot-Directory-some-path",
"relatedSpdxElement": "SPDXRef-Package-python-package-1-f04d218ff5ff50db",
"relatedSpdxElement": "SPDXRef-Package-python-package-1-4dd25c6ee16b729a",
"relationshipType": "CONTAINS"
},
{
@@ -15,7 +15,7 @@
"packages": [
{
"name": "package-1",
"SPDXID": "SPDXRef-Package-python-package-1-2f52f617f1548337",
"SPDXID": "SPDXRef-Package-python-package-1-72567175418f73f8",
"versionInfo": "1.0.1",
"supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION",
@@ -90,7 +90,7 @@
"relationships": [
{
"spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input",
"relatedSpdxElement": "SPDXRef-Package-python-package-1-2f52f617f1548337",
"relatedSpdxElement": "SPDXRef-Package-python-package-1-72567175418f73f8",
"relationshipType": "CONTAINS"
},
{
@@ -15,7 +15,7 @@
"packages": [
{
"name": "package-1",
"SPDXID": "SPDXRef-Package-python-package-1-2f52f617f1548337",
"SPDXID": "SPDXRef-Package-python-package-1-72567175418f73f8",
"versionInfo": "1.0.1",
"supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION",
@@ -199,38 +199,38 @@
],
"relationships": [
{
"spdxElementId": "SPDXRef-Package-python-package-1-2f52f617f1548337",
"spdxElementId": "SPDXRef-Package-python-package-1-72567175418f73f8",
"relatedSpdxElement": "SPDXRef-File-f1-5265a4dde3edbf7c",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-python-package-1-2f52f617f1548337",
"spdxElementId": "SPDXRef-Package-python-package-1-72567175418f73f8",
"relatedSpdxElement": "SPDXRef-File-z1-f5-839d99ee67d9d174",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-python-package-1-2f52f617f1548337",
"spdxElementId": "SPDXRef-Package-python-package-1-72567175418f73f8",
"relatedSpdxElement": "SPDXRef-File-a1-f6-9c2f7510199b17f6",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-python-package-1-2f52f617f1548337",
"spdxElementId": "SPDXRef-Package-python-package-1-72567175418f73f8",
"relatedSpdxElement": "SPDXRef-File-d2-f4-c641caa71518099f",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-python-package-1-2f52f617f1548337",
"spdxElementId": "SPDXRef-Package-python-package-1-72567175418f73f8",
"relatedSpdxElement": "SPDXRef-File-d1-f3-c6f5b29dca12661f",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-python-package-1-2f52f617f1548337",
"spdxElementId": "SPDXRef-Package-python-package-1-72567175418f73f8",
"relatedSpdxElement": "SPDXRef-File-f2-f9e49132a4b96ccd",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input",
"relatedSpdxElement": "SPDXRef-Package-python-package-1-2f52f617f1548337",
"relatedSpdxElement": "SPDXRef-Package-python-package-1-72567175418f73f8",
"relationshipType": "CONTAINS"
},
{
+11 -4
View File
@@ -8,6 +8,7 @@ import (
"github.com/spdx/tools-golang/tagvalue"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/format/common/spdxhelpers"
"github.com/anchore/syft/syft/format/internal/stream"
"github.com/anchore/syft/syft/sbom"
@@ -39,8 +40,8 @@ func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error)
return nil, "", "", fmt.Errorf("unsupported spdx tag-value document version")
}
if _, err = reader.Seek(0, io.SeekStart); err != nil {
return nil, "", "", fmt.Errorf("unable to seek to start of SPDX Tag-Value SBOM: %w", err)
if _, err := reader.Seek(0, io.SeekStart); err != nil {
return nil, "", "", fmt.Errorf("unable to seek to start of SPDX Tag-Value SBOM: %+v", err)
}
doc, err := tagvalue.Read(reader)
@@ -56,7 +57,13 @@ func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error)
}
func (d decoder) Identify(r io.Reader) (sbom.FormatID, string) {
if r == nil {
reader, err := stream.SeekableReader(r)
if err != nil {
return "", ""
}
if _, err := reader.Seek(0, io.SeekStart); err != nil {
log.Debugf("unable to seek to start of SPDX Tag-Value SBOM: %+v", err)
return "", ""
}
@@ -65,7 +72,7 @@ func (d decoder) Identify(r io.Reader) (sbom.FormatID, string) {
// DataLicense: CC0-1.0
// SPDXID: SPDXRef-DOCUMENT
scanner := bufio.NewScanner(r)
scanner := bufio.NewScanner(reader)
scanner.Split(bufio.ScanLines)
var id sbom.FormatID
-5
View File
@@ -2,7 +2,6 @@ package spdxtagvalue
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
@@ -57,10 +56,6 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, ID, formatID)
assert.NotEmpty(t, formatVersion)
// reset reader
_, err = reader.Seek(0, io.SeekStart)
require.NoError(t, err)
bom, decodeID, decodeVersion, err := dec.Decode(reader)
require.NotNil(t, bom)
require.NoError(t, err)
@@ -91,7 +91,7 @@ ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1
##### Package: package-1
PackageName: package-1
SPDXID: SPDXRef-Package-python-package-1-2f52f617f1548337
SPDXID: SPDXRef-Package-python-package-1-72567175418f73f8
PackageVersion: 1.0.1
PackageSupplier: NOASSERTION
PackageDownloadLocation: NOASSERTION
@@ -105,13 +105,13 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-1
##### Relationships
Relationship: SPDXRef-Package-python-package-1-2f52f617f1548337 CONTAINS SPDXRef-File-f1-5265a4dde3edbf7c
Relationship: SPDXRef-Package-python-package-1-2f52f617f1548337 CONTAINS SPDXRef-File-z1-f5-839d99ee67d9d174
Relationship: SPDXRef-Package-python-package-1-2f52f617f1548337 CONTAINS SPDXRef-File-a1-f6-9c2f7510199b17f6
Relationship: SPDXRef-Package-python-package-1-2f52f617f1548337 CONTAINS SPDXRef-File-d2-f4-c641caa71518099f
Relationship: SPDXRef-Package-python-package-1-2f52f617f1548337 CONTAINS SPDXRef-File-d1-f3-c6f5b29dca12661f
Relationship: SPDXRef-Package-python-package-1-2f52f617f1548337 CONTAINS SPDXRef-File-f2-f9e49132a4b96ccd
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-2f52f617f1548337
Relationship: SPDXRef-Package-python-package-1-72567175418f73f8 CONTAINS SPDXRef-File-f1-5265a4dde3edbf7c
Relationship: SPDXRef-Package-python-package-1-72567175418f73f8 CONTAINS SPDXRef-File-z1-f5-839d99ee67d9d174
Relationship: SPDXRef-Package-python-package-1-72567175418f73f8 CONTAINS SPDXRef-File-a1-f6-9c2f7510199b17f6
Relationship: SPDXRef-Package-python-package-1-72567175418f73f8 CONTAINS SPDXRef-File-d2-f4-c641caa71518099f
Relationship: SPDXRef-Package-python-package-1-72567175418f73f8 CONTAINS SPDXRef-File-d1-f3-c6f5b29dca12661f
Relationship: SPDXRef-Package-python-package-1-72567175418f73f8 CONTAINS SPDXRef-File-f2-f9e49132a4b96ccd
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-72567175418f73f8
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-4b756c6f6fb127a3
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Image-user-image-input
@@ -38,7 +38,7 @@ ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1
##### Package: package-1
PackageName: package-1
SPDXID: SPDXRef-Package-python-package-1-f04d218ff5ff50db
SPDXID: SPDXRef-Package-python-package-1-4dd25c6ee16b729a
PackageVersion: 1.0.1
PackageSupplier: NOASSERTION
PackageDownloadLocation: NOASSERTION
@@ -52,7 +52,7 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-2
##### Relationships
Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-python-package-1-f04d218ff5ff50db
Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-python-package-1-4dd25c6ee16b729a
Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-deb-package-2-39392bb5e270f669
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Directory-some-path
@@ -41,7 +41,7 @@ ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1
##### Package: package-1
PackageName: package-1
SPDXID: SPDXRef-Package-python-package-1-2f52f617f1548337
SPDXID: SPDXRef-Package-python-package-1-72567175418f73f8
PackageVersion: 1.0.1
PackageSupplier: NOASSERTION
PackageDownloadLocation: NOASSERTION
@@ -55,7 +55,7 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-1
##### Relationships
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-2f52f617f1548337
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-72567175418f73f8
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-4b756c6f6fb127a3
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Image-user-image-input
+9 -3
View File
@@ -53,7 +53,13 @@ func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error)
}
func (d decoder) Identify(r io.Reader) (sbom.FormatID, string) {
if r == nil {
reader, err := stream.SeekableReader(r)
if err != nil {
return "", ""
}
if _, err := reader.Seek(0, io.SeekStart); err != nil {
log.Debugf("unable to seek to start of Syft JSON SBOM: %+v", err)
return "", ""
}
@@ -61,10 +67,10 @@ func (d decoder) Identify(r io.Reader) (sbom.FormatID, string) {
Schema model.Schema `json:"schema"`
}
dec := json.NewDecoder(r)
dec := json.NewDecoder(reader)
var doc Document
if err := dec.Decode(&doc); err != nil {
if err = dec.Decode(&doc); err != nil {
// maybe not json? maybe not valid? doesn't matter, we won't process it.
return "", ""
}
+2 -3
View File
@@ -2,7 +2,6 @@ package syftjson
import (
"bytes"
"context"
"flag"
"strings"
"testing"
@@ -133,7 +132,7 @@ func TestImageEncoder(t *testing.T) {
func TestEncodeFullJSONDocument(t *testing.T) {
catalog := pkg.NewCollection()
ctx := context.TODO()
p1 := pkg.Package{
Name: "package-1",
Version: "1.0.1",
@@ -145,7 +144,7 @@ func TestEncodeFullJSONDocument(t *testing.T) {
Type: pkg.PythonPkg,
FoundBy: "the-cataloger-1",
Language: pkg.Python,
Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "MIT")),
Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")),
Metadata: pkg.PythonPackage{
Name: "package-1",
Version: "1.0.1",
+2 -1
View File
@@ -47,6 +47,7 @@ type licenses []License
type License struct {
Value string `json:"value"`
FullText string `json:"fullText"`
SPDXExpression string `json:"spdxExpression"`
Type license.Type `json:"type"`
URLs []string `json:"urls"`
@@ -58,7 +59,7 @@ func newModelLicensesFromValues(licenses []string) (ml []License) {
for _, v := range licenses {
expression, err := license.ParseExpression(v)
if err != nil {
log.Tracef("could not find valid spdx expression for %s: %w", v, err)
log.Trace("could not find valid spdx expression for %s: %w", v, err)
}
ml = append(ml, License{
Value: v,
@@ -1,7 +1,7 @@
{
"artifacts": [
{
"id": "f04d218ff5ff50db",
"id": "4dd25c6ee16b729a",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@@ -15,6 +15,7 @@
"licenses": [
{
"value": "MIT",
"fullText": "",
"spdxExpression": "MIT",
"type": "declared",
"urls": [],
@@ -1,7 +1,7 @@
{
"artifacts": [
{
"id": "951845d9a8d6b5b2",
"id": "fba4ca04d4906f25",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@@ -15,6 +15,7 @@
"licenses": [
{
"value": "MIT",
"fullText": "",
"spdxExpression": "MIT",
"type": "declared",
"urls": [],
@@ -1,7 +1,7 @@
{
"artifacts": [
{
"id": "2f52f617f1548337",
"id": "72567175418f73f8",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@@ -16,6 +16,7 @@
"licenses": [
{
"value": "MIT",
"fullText": "",
"spdxExpression": "MIT",
"type": "declared",
"urls": [],
+21 -34
View File
@@ -10,7 +10,6 @@ import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
formatInternal "github.com/anchore/syft/syft/format/internal"
"github.com/anchore/syft/syft/format/syftjson/model"
"github.com/anchore/syft/syft/internal/packagemetadata"
"github.com/anchore/syft/syft/internal/sourcemetadata"
@@ -34,12 +33,10 @@ func metadataType(metadata interface{}, legacy bool) string {
// ToFormatModel transforms the sbom import a format-specific model.
func ToFormatModel(s sbom.SBOM, cfg EncoderConfig) model.Document {
locationSorter, coordinateSorter := formatInternal.GetLocationSorters(s)
return model.Document{
Artifacts: toPackageModels(s.Artifacts.Packages, locationSorter, cfg),
Artifacts: toPackageModels(s.Artifacts.Packages, cfg),
ArtifactRelationships: toRelationshipModel(s.Relationships),
Files: toFile(s, coordinateSorter),
Files: toFile(s),
Source: toSourceModel(s.Source),
Distro: toLinuxReleaser(s.Artifacts.LinuxDistribution),
Descriptor: toDescriptor(s.Descriptor),
@@ -84,7 +81,7 @@ func toDescriptor(d sbom.Descriptor) model.Descriptor {
}
}
func toFile(s sbom.SBOM, coordinateSorter func(a, b file.Coordinates) int) []model.File {
func toFile(s sbom.SBOM) []model.File {
results := make([]model.File, 0)
artifacts := s.Artifacts
@@ -144,19 +141,11 @@ func toFile(s sbom.SBOM, coordinateSorter func(a, b file.Coordinates) int) []mod
})
}
// sort to ensure we're stable across multiple runs
// should order by the layer order from the container image then by real path
sortFiles(results, coordinateSorter)
return results
}
func sortFiles(files []model.File, coordinateSorter func(a, b file.Coordinates) int) {
fileSorter := func(a, b model.File) int {
return coordinateSorter(a.Location, b.Location)
}
sort.SliceStable(files, func(i, j int) bool {
return fileSorter(files[i], files[j]) < 0
// sort by real path then virtual path to ensure the result is stable across multiple runs
sort.SliceStable(results, func(i, j int) bool {
return results[i].Location.RealPath < results[j].Location.RealPath
})
return results
}
func toFileMetadataEntry(coordinates file.Coordinates, metadata *file.Metadata) *model.FileMetadataEntry {
@@ -166,7 +155,7 @@ func toFileMetadataEntry(coordinates file.Coordinates, metadata *file.Metadata)
var mode int
var size int64
if metadata.FileInfo != nil {
if metadata != nil && metadata.FileInfo != nil {
var err error
mode, err = strconv.Atoi(fmt.Sprintf("%o", metadata.Mode()))
@@ -214,19 +203,25 @@ func toFileType(ty stereoscopeFile.Type) string {
}
}
func toPackageModels(catalog *pkg.Collection, locationSorter func(a, b file.Location) int, cfg EncoderConfig) []model.Package {
func toPackageModels(catalog *pkg.Collection, cfg EncoderConfig) []model.Package {
artifacts := make([]model.Package, 0)
if catalog == nil {
return artifacts
}
for _, p := range catalog.Sorted() {
artifacts = append(artifacts, toPackageModel(p, locationSorter, cfg))
artifacts = append(artifacts, toPackageModel(p, cfg))
}
return artifacts
}
func toLicenseModel(pkgLicenses []pkg.License, locationSorter func(a, b file.Location) int) (modelLicenses []model.License) {
func toLicenseModel(pkgLicenses []pkg.License) (modelLicenses []model.License) {
for _, l := range pkgLicenses {
// guarantee collection
locations := make([]file.Location, 0)
if v := l.Locations.ToSlice(); v != nil {
locations = v
}
// format model must have allocated collections
urls := l.URLs
if urls == nil {
@@ -239,14 +234,14 @@ func toLicenseModel(pkgLicenses []pkg.License, locationSorter func(a, b file.Loc
Contents: l.Contents,
Type: l.Type,
URLs: urls,
Locations: toLocationsModel(l.Locations, locationSorter),
Locations: locations,
})
}
return
}
// toPackageModel crates a new Package from the given pkg.Package.
func toPackageModel(p pkg.Package, locationSorter func(a, b file.Location) int, cfg EncoderConfig) model.Package {
func toPackageModel(p pkg.Package, cfg EncoderConfig) model.Package {
var cpes = make([]model.CPE, len(p.CPEs))
for i, c := range p.CPEs {
convertedCPE := model.CPE{
@@ -260,7 +255,7 @@ func toPackageModel(p pkg.Package, locationSorter func(a, b file.Location) int,
// initializing the array; this is a good choke point for this check
var licenses = make([]model.License, 0)
if !p.Licenses.Empty() {
licenses = toLicenseModel(p.Licenses.ToSlice(), locationSorter)
licenses = toLicenseModel(p.Licenses.ToSlice())
}
return model.Package{
@@ -270,7 +265,7 @@ func toPackageModel(p pkg.Package, locationSorter func(a, b file.Location) int,
Version: p.Version,
Type: p.Type,
FoundBy: p.FoundBy,
Locations: toLocationsModel(p.Locations, locationSorter),
Locations: p.Locations.ToSlice(),
Licenses: licenses,
Language: p.Language,
CPEs: cpes,
@@ -283,14 +278,6 @@ func toPackageModel(p pkg.Package, locationSorter func(a, b file.Location) int,
}
}
func toLocationsModel(locations file.LocationSet, locationSorter func(a, b file.Location) int) []file.Location {
if locations.Empty() {
return []file.Location{}
}
return locations.ToSlice(locationSorter)
}
func toRelationshipModel(relationships []artifact.Relationship) []model.Relationship {
result := make([]model.Relationship, len(relationships))
for i, r := range relationships {
+1 -699
View File
@@ -345,707 +345,9 @@ func Test_toPackageModel_metadataType(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if d := cmp.Diff(tt.want, toPackageModel(tt.p, file.LocationSorter(nil), tt.cfg), cmpopts.EquateEmpty()); d != "" {
if d := cmp.Diff(tt.want, toPackageModel(tt.p, tt.cfg), cmpopts.EquateEmpty()); d != "" {
t.Errorf("unexpected package (-want +got):\n%s", d)
}
})
}
}
func Test_toPackageModel_layerOrdering(t *testing.T) {
tests := []struct {
name string
p pkg.Package
layerOrder []string
cfg EncoderConfig
want model.Package
}{
{
name: "with layer ordering",
p: pkg.Package{
Name: "pkg-1",
Licenses: pkg.NewLicenseSet(pkg.License{
Value: "MIT",
Locations: file.NewLocationSet(
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/lic-a",
FileSystemID: "fsid-3",
}),
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/lic-a",
FileSystemID: "fsid-1",
}),
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/lic-b",
FileSystemID: "fsid-0",
}),
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/lic-a",
FileSystemID: "fsid-2",
}),
),
}),
Locations: file.NewLocationSet(
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-3",
}),
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
}),
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-0",
}),
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-2",
}),
),
},
layerOrder: []string{
"fsid-0",
"fsid-1",
"fsid-2",
"fsid-3",
},
want: model.Package{
PackageBasicData: model.PackageBasicData{
Name: "pkg-1",
Licenses: []model.License{
{
Value: "MIT",
Locations: []file.Location{
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/lic-b",
FileSystemID: "fsid-0", // important!
},
AccessPath: "/lic-b",
},
},
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/lic-a",
FileSystemID: "fsid-1", // important!
},
AccessPath: "/lic-a",
},
},
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/lic-a",
FileSystemID: "fsid-2", // important!
},
AccessPath: "/lic-a",
},
},
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/lic-a",
FileSystemID: "fsid-3", // important!
},
AccessPath: "/lic-a",
},
},
},
},
},
Locations: []file.Location{
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-0", // important!
},
AccessPath: "/b",
},
},
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1", // important!
},
AccessPath: "/a",
},
},
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-2", // important!
},
AccessPath: "/a",
},
},
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-3", // important!
},
AccessPath: "/a",
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if d := cmp.Diff(tt.want, toPackageModel(tt.p, file.LocationSorter(tt.layerOrder), tt.cfg), cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(file.LocationData{})); d != "" {
t.Errorf("unexpected package (-want +got):\n%s", d)
}
})
}
}
func Test_toLocationModel(t *testing.T) {
tests := []struct {
name string
locations file.LocationSet
layers []string
want []file.Location
}{
{
name: "empty location set",
locations: file.NewLocationSet(),
layers: []string{"fsid-1"},
want: []file.Location{},
},
{
name: "nil layer order map",
locations: file.NewLocationSet(
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
}),
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-2",
}),
),
layers: nil, // please don't panic!
want: []file.Location{
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
AccessPath: "/a",
},
LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}},
},
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-2",
},
AccessPath: "/b",
},
LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}},
},
},
},
{
name: "go case",
locations: file.NewLocationSet(
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-3",
}),
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
}),
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/c",
FileSystemID: "fsid-2",
}),
),
layers: []string{
"fsid-1",
"fsid-2",
"fsid-3",
},
want: []file.Location{
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
AccessPath: "/b",
},
LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}},
},
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/c",
FileSystemID: "fsid-2",
},
AccessPath: "/c",
},
LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}},
},
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-3",
},
AccessPath: "/a",
},
LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}},
},
},
},
{
name: "same layer different paths", // prove we can sort by path irrespective of layer
locations: file.NewLocationSet(
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/c",
FileSystemID: "fsid-1",
}),
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
}),
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
}),
),
layers: []string{
"fsid-1",
},
want: []file.Location{
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
AccessPath: "/a",
},
LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}},
},
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
AccessPath: "/b",
},
LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}},
},
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/c",
FileSystemID: "fsid-1",
},
AccessPath: "/c",
},
LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}},
},
},
},
{
name: "mixed layers and paths",
locations: file.NewLocationSet(
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/z",
FileSystemID: "fsid-3",
}),
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-2",
}),
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
}),
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/c",
FileSystemID: "fsid-2",
}),
),
layers: []string{
"fsid-1",
"fsid-2",
"fsid-3",
},
want: []file.Location{
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
AccessPath: "/b",
},
LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}},
},
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-2",
},
AccessPath: "/a",
},
LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}},
},
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/c",
FileSystemID: "fsid-2",
},
AccessPath: "/c",
},
LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}},
},
{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/z",
FileSystemID: "fsid-3",
},
AccessPath: "/z",
},
LocationMetadata: file.LocationMetadata{Annotations: map[string]string{}},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := toLocationsModel(tt.locations, file.LocationSorter(tt.layers))
if d := cmp.Diff(tt.want, got, cmpopts.IgnoreUnexported(file.LocationData{})); d != "" {
t.Errorf("toLocationsModel() mismatch (-want +got):\n%s", d)
}
})
}
}
func Test_sortFiles(t *testing.T) {
tests := []struct {
name string
files []model.File
layers []string
want []model.File
}{
{
name: "empty files slice",
files: []model.File{},
layers: []string{"fsid-1"},
want: []model.File{},
},
{
name: "nil layer order map",
files: []model.File{
{
ID: "file-1",
Location: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
},
{
ID: "file-2",
Location: file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-2",
},
},
},
layers: nil,
want: []model.File{
{
ID: "file-1",
Location: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
},
{
ID: "file-2",
Location: file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-2",
},
},
},
},
{
name: "layer ordering",
files: []model.File{
{
ID: "file-1",
Location: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-3",
},
},
{
ID: "file-2",
Location: file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
},
{
ID: "file-3",
Location: file.Coordinates{
RealPath: "/c",
FileSystemID: "fsid-2",
},
},
},
layers: []string{
"fsid-1",
"fsid-2",
"fsid-3",
},
want: []model.File{
{
ID: "file-2",
Location: file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
},
{
ID: "file-3",
Location: file.Coordinates{
RealPath: "/c",
FileSystemID: "fsid-2",
},
},
{
ID: "file-1",
Location: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-3",
},
},
},
},
{
name: "same layer different paths",
files: []model.File{
{
ID: "file-1",
Location: file.Coordinates{
RealPath: "/c",
FileSystemID: "fsid-1",
},
},
{
ID: "file-2",
Location: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
},
{
ID: "file-3",
Location: file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
},
},
layers: []string{
"fsid-1",
},
want: []model.File{
{
ID: "file-2",
Location: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
},
{
ID: "file-3",
Location: file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
},
{
ID: "file-1",
Location: file.Coordinates{
RealPath: "/c",
FileSystemID: "fsid-1",
},
},
},
},
{
name: "stability test - preserve original order for equivalent items",
files: []model.File{
{
ID: "file-1",
Location: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
},
{
ID: "file-2",
Location: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
},
{
ID: "file-3",
Location: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
},
},
layers: []string{
"fsid-1",
},
want: []model.File{
{
ID: "file-1",
Location: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
},
{
ID: "file-2",
Location: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
},
{
ID: "file-3",
Location: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
},
},
},
{
name: "complex file metadata doesn't affect sorting",
files: []model.File{
{
ID: "file-1",
Location: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-2",
},
Metadata: &model.FileMetadataEntry{
Mode: 0644,
Type: "file",
UserID: 1000,
GroupID: 1000,
MIMEType: "text/plain",
Size: 100,
},
Contents: "content1",
Digests: []file.Digest{
{
Algorithm: "sha256",
Value: "abc123",
},
},
},
{
ID: "file-2",
Location: file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
Metadata: &model.FileMetadataEntry{
Mode: 0755,
Type: "directory",
UserID: 0,
GroupID: 0,
MIMEType: "application/directory",
Size: 4096,
},
},
},
layers: []string{
"fsid-1",
"fsid-2",
},
want: []model.File{
{
ID: "file-2",
Location: file.Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
Metadata: &model.FileMetadataEntry{
Mode: 0755,
Type: "directory",
UserID: 0,
GroupID: 0,
MIMEType: "application/directory",
Size: 4096,
},
},
{
ID: "file-1",
Location: file.Coordinates{
RealPath: "/a",
FileSystemID: "fsid-2",
},
Metadata: &model.FileMetadataEntry{
Mode: 0644,
Type: "file",
UserID: 1000,
GroupID: 1000,
MIMEType: "text/plain",
Size: 100,
},
Contents: "content1",
Digests: []file.Digest{
{
Algorithm: "sha256",
Value: "abc123",
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
files := make([]model.File, len(tt.files))
copy(files, tt.files)
sortFiles(files, file.CoordinatesSorter(tt.layers))
if d := cmp.Diff(tt.want, files); d != "" {
t.Errorf("sortFiles() mismatch (-want +got):\n%s", d)
}
})
}
}
+3 -4
View File
@@ -16,7 +16,6 @@ import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/internal"
"github.com/anchore/syft/syft/format/syftjson/model"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
@@ -352,9 +351,9 @@ func toSyftPackage(p model.Package, idAliases map[string]string) pkg.Package {
Metadata: p.Metadata,
}
internal.Backfill(&out)
// always prefer the IDs from the SBOM over derived IDs
// we don't know if this package ID is truly unique, however, we need to trust the user input in case there are
// external references to it. That is, we can't derive our own ID (using pkg.SetID()) since consumers won't
// be able to historically interact with data that references the IDs from the original SBOM document being decoded now.
out.OverrideID(artifact.ID(p.ID))
// this alias mapping is currently defunct, but could be useful in the future.

Some files were not shown because too many files have changed in this diff Show More