mirror of
https://github.com/anchore/syft.git
synced 2026-10-13 14:47:22 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47cc8b58a7 |
+1
-1
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
desc "Advanced Forensic Format"
|
||||
homepage "https://github.com/sshock/AFFLIBv3"
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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{},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user