From f77d5038926c28b8e96304e910f073f4c6cfaf84 Mon Sep 17 00:00:00 2001 From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com> Date: Tue, 13 May 2025 16:37:18 -0400 Subject: [PATCH] detect license ID from full text when incidentally provided as a value (#3876) --------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- cmd/syft/internal/options/catalog.go | 4 +- cmd/syft/internal/options/license.go | 65 ++- internal/licenses/context_test.go | 6 +- internal/licenses/find_evidence.go | 36 ++ internal/licenses/find_evidence_test.go | 81 ++++ internal/licenses/scanner.go | 47 +- internal/licenses/scanner_test.go | 84 ---- internal/licenses/search.go | 123 ----- internal/licenses/search_test.go | 166 ------- internal/licenses/test-fixtures/multi-license | 445 ++++++++++++++++++ internal/task/package_task_factory.go | 31 ++ internal/task/package_task_factory_test.go | 108 +++++ syft/cataloging/license.go | 28 +- syft/create_sbom.go | 1 - .../common/spdxhelpers/to_format_model.go | 101 +--- .../spdxhelpers/to_format_model_test.go | 64 +-- .../common/spdxhelpers/to_syft_model.go | 5 +- .../TestCycloneDxDirectoryEncoder.golden | 2 +- .../snapshot/TestCycloneDxImageEncoder.golden | 2 +- .../TestCycloneDxDirectoryEncoder.golden | 2 +- .../snapshot/TestCycloneDxImageEncoder.golden | 2 +- .../cyclonedxutil/helpers/licenses.go | 7 +- .../cyclonedxutil/helpers/licenses_test.go | 36 +- .../internal/spdxutil/helpers/license.go | 106 ++++- .../internal/spdxutil/helpers/license_test.go | 121 +++-- .../internal/testutil/directory_input.go | 9 +- syft/format/internal/testutil/image_input.go | 5 +- .../TestSPDXJSONDirectoryEncoder.golden | 4 +- .../snapshot/TestSPDXJSONImageEncoder.golden | 4 +- .../snapshot/TestSPDXRelationshipOrder.golden | 16 +- .../snapshot/TestSPDXRelationshipOrder.golden | 16 +- .../TestSPDXTagValueDirectoryEncoder.golden | 4 +- .../TestSPDXTagValueImageEncoder.golden | 4 +- syft/format/syftjson/encoder_test.go | 5 +- .../snapshot/TestDirectoryEncoder.golden | 2 +- .../TestEncodeFullJSONDocument.golden | 2 +- .../snapshot/TestImageEncoder.golden | 2 +- syft/pkg/cataloger/alpine/cataloger_test.go | 11 +- syft/pkg/cataloger/alpine/package.go | 5 +- syft/pkg/cataloger/alpine/parse_apk_db.go | 4 +- .../pkg/cataloger/alpine/parse_apk_db_test.go | 11 +- syft/pkg/cataloger/arch/cataloger_test.go | 10 +- syft/pkg/cataloger/arch/package.go | 5 +- syft/pkg/cataloger/arch/parse_alpm_db.go | 3 +- syft/pkg/cataloger/binary/elf_package.go | 6 +- .../cataloger/binary/elf_package_cataloger.go | 4 +- syft/pkg/cataloger/binary/elf_package_test.go | 4 +- syft/pkg/cataloger/bitnami/cataloger_test.go | 74 +-- syft/pkg/cataloger/debian/cataloger_test.go | 16 +- syft/pkg/cataloger/debian/package.go | 13 +- .../pkg/cataloger/debian/parse_deb_archive.go | 2 +- syft/pkg/cataloger/debian/parse_dpkg_db.go | 2 +- .../gentoo/parse_portage_contents.go | 15 +- syft/pkg/cataloger/golang/cataloger.go | 2 +- syft/pkg/cataloger/golang/licenses.go | 137 ++---- syft/pkg/cataloger/golang/licenses_test.go | 71 ++- syft/pkg/cataloger/golang/parse_go_binary.go | 18 +- .../cataloger/golang/parse_go_binary_test.go | 8 +- syft/pkg/cataloger/golang/parse_go_mod.go | 10 +- syft/pkg/cataloger/golang/stdlib_package.go | 13 +- .../cataloger/golang/stdlib_package_test.go | 10 +- syft/pkg/cataloger/java/archive_parser.go | 28 +- .../pkg/cataloger/java/archive_parser_test.go | 67 ++- syft/pkg/cataloger/java/parse_pom_xml.go | 4 +- syft/pkg/cataloger/java/parse_pom_xml_test.go | 3 +- .../cataloger/javascript/cataloger_test.go | 6 +- syft/pkg/cataloger/javascript/package.go | 29 +- .../javascript/parse_package_json.go | 4 +- .../javascript/parse_package_json_test.go | 18 +- .../javascript/parse_package_lock.go | 6 +- .../javascript/parse_package_lock_test.go | 22 +- .../cataloger/javascript/parse_pnpm_lock.go | 6 +- .../cataloger/javascript/parse_yarn_lock.go | 8 +- .../javascript/parse_yarn_lock_test.go | 4 +- syft/pkg/cataloger/kernel/cataloger_test.go | 4 +- syft/pkg/cataloger/kernel/package.go | 5 +- .../kernel/parse_linux_kernel_module_file.go | 3 +- syft/pkg/cataloger/lua/package.go | 6 +- syft/pkg/cataloger/lua/parse_rockspec.go | 3 +- syft/pkg/cataloger/lua/parse_rockspec_test.go | 10 +- syft/pkg/cataloger/ocaml/package.go | 6 +- syft/pkg/cataloger/ocaml/parse_opam.go | 3 +- syft/pkg/cataloger/ocaml/parse_opam_test.go | 11 +- syft/pkg/cataloger/php/package.go | 17 +- syft/pkg/cataloger/php/parse_composer_lock.go | 3 +- .../cataloger/php/parse_composer_lock_test.go | 6 +- .../pkg/cataloger/php/parse_installed_json.go | 3 +- .../php/parse_installed_json_test.go | 6 +- syft/pkg/cataloger/php/parse_pecl_pear.go | 8 +- .../pkg/cataloger/php/parse_pecl_pear_test.go | 11 +- syft/pkg/cataloger/python/cataloger_test.go | 49 +- syft/pkg/cataloger/python/parse_wheel_egg.go | 34 +- syft/pkg/cataloger/r/cataloger_test.go | 6 +- syft/pkg/cataloger/r/package.go | 13 +- syft/pkg/cataloger/r/package_test.go | 18 +- syft/pkg/cataloger/r/parse_description.go | 4 +- syft/pkg/cataloger/redhat/cataloger_test.go | 10 +- syft/pkg/cataloger/redhat/package.go | 9 +- .../pkg/cataloger/redhat/parse_rpm_archive.go | 4 +- .../redhat/parse_rpm_archive_test.go | 6 +- syft/pkg/cataloger/redhat/parse_rpm_db.go | 3 +- .../pkg/cataloger/redhat/parse_rpm_db_test.go | 5 +- .../cataloger/redhat/parse_rpm_manifest.go | 4 +- syft/pkg/cataloger/ruby/package.go | 6 +- syft/pkg/cataloger/ruby/parse_gemspec.go | 3 +- syft/pkg/cataloger/ruby/parse_gemspec_test.go | 5 +- syft/pkg/cataloger/sbom/cataloger_test.go | 48 +- syft/pkg/cataloger/wordpress/package.go | 6 +- syft/pkg/cataloger/wordpress/parse_plugin.go | 3 +- .../cataloger/wordpress/parse_plugin_test.go | 5 +- syft/pkg/collection_test.go | 8 +- syft/pkg/license.go | 353 ++++++++++---- syft/pkg/license_deprecated.go | 112 +++++ syft/pkg/license_set_test.go | 74 +-- syft/pkg/license_test.go | 86 ++-- syft/pkg/package_test.go | 12 +- test/cli/license_test.go | 15 +- 117 files changed, 2083 insertions(+), 1328 deletions(-) create mode 100644 internal/licenses/find_evidence.go create mode 100644 internal/licenses/find_evidence_test.go delete mode 100644 internal/licenses/scanner_test.go delete mode 100644 internal/licenses/search.go delete mode 100644 internal/licenses/search_test.go create mode 100644 internal/licenses/test-fixtures/multi-license create mode 100644 syft/pkg/license_deprecated.go diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go index c27d5019e..b853518d8 100644 --- a/cmd/syft/internal/options/catalog.go +++ b/cmd/syft/internal/options/catalog.go @@ -158,8 +158,8 @@ func (cfg Catalog) ToFilesConfig() filecataloging.Config { func (cfg Catalog) ToLicenseConfig() cataloging.LicenseConfig { return cataloging.LicenseConfig{ - IncludeUnkownLicenseContent: cfg.License.IncludeUnknownLicenseContent, - Coverage: cfg.License.LicenseCoverage, + IncludeContent: cfg.License.Content, + Coverage: cfg.License.Coverage, } } diff --git a/cmd/syft/internal/options/license.go b/cmd/syft/internal/options/license.go index 1cce6c298..c9eef8769 100644 --- a/cmd/syft/internal/options/license.go +++ b/cmd/syft/internal/options/license.go @@ -1,12 +1,22 @@ package options import ( + "fmt" + "github.com/anchore/clio" + "github.com/anchore/syft/syft/cataloging" ) type licenseConfig struct { - 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"` + 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:"-"` } var _ interface { @@ -14,15 +24,56 @@ var _ interface { } = (*licenseConfig)(nil) func (o *licenseConfig) DescribeFields(descriptions clio.FieldDescriptionSet) { - 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 + 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 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{ - IncludeUnknownLicenseContent: false, - LicenseCoverage: 75, + Content: cfg.IncludeContent, + Coverage: cfg.Coverage, + AvailableLicenseContent: []cataloging.LicenseContent{ + cataloging.LicenseContentIncludeAll, + cataloging.LicenseContentIncludeUnknown, + cataloging.LicenseContentExcludeAll, + }, } } diff --git a/internal/licenses/context_test.go b/internal/licenses/context_test.go index 412222e36..9d979ea58 100644 --- a/internal/licenses/context_test.go +++ b/internal/licenses/context_test.go @@ -8,7 +8,7 @@ import ( ) func TestSetContextLicenseScanner(t *testing.T) { - scanner := testScanner(true) + scanner := testScanner() ctx := context.Background() ctx = SetContextLicenseScanner(ctx, scanner) @@ -20,7 +20,7 @@ func TestSetContextLicenseScanner(t *testing.T) { } func TestIsContextLicenseScannerSet(t *testing.T) { - scanner := testScanner(true) + scanner := testScanner() 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(true) + scanner := testScanner() ctx := SetContextLicenseScanner(context.Background(), scanner) s, err := ContextLicenseScanner(ctx) if err != nil || s != scanner { diff --git a/internal/licenses/find_evidence.go b/internal/licenses/find_evidence.go new file mode 100644 index 000000000..1e41036e5 --- /dev/null +++ b/internal/licenses/find_evidence.go @@ -0,0 +1,36 @@ +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 +} diff --git a/internal/licenses/find_evidence_test.go b/internal/licenses/find_evidence_test.go new file mode 100644 index 000000000..6ebf79f4d --- /dev/null +++ b/internal/licenses/find_evidence_test.go @@ -0,0 +1,81 @@ +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 +} diff --git a/internal/licenses/scanner.go b/internal/licenses/scanner.go index 7728ba26b..d64c46b8e 100644 --- a/internal/licenses/scanner.go +++ b/internal/licenses/scanner.go @@ -8,33 +8,37 @@ import ( "github.com/google/licensecheck" "github.com/anchore/syft/internal/log" - "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/pkg" ) const ( - DefaultCoverageThreshold = 75 // determined by experimentation - DefaultIncludeLicenseContent = false + UnknownLicensePrefix = unknownLicenseType + "_" + DefaultCoverageThreshold = 75 // determined by experimentation + + unknownLicenseType = "UNKNOWN" ) +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 { - IdentifyLicenseIDs(context.Context, io.Reader) ([]string, []byte, error) - FileSearch(context.Context, file.LocationReadCloser) ([]file.License, error) - PkgSearch(context.Context, file.LocationReadCloser) ([]pkg.License, error) + FindEvidence(context.Context, io.Reader) ([]Evidence, []byte, error) } var _ Scanner = (*scanner)(nil) type scanner struct { - coverageThreshold float64 // between 0 and 100 - includeLicenseContent bool - scanner func([]byte) licensecheck.Coverage + coverageThreshold float64 // between 0 and 100 + scanner func([]byte) licensecheck.Coverage } type ScannerConfig struct { - CoverageThreshold float64 - IncludeLicenseContent bool - Scanner func([]byte) licensecheck.Coverage + CoverageThreshold float64 + Scanner func([]byte) licensecheck.Coverage } type Option func(*scanner) @@ -45,12 +49,6 @@ 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()) @@ -58,10 +56,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, - includeLicenseContent: DefaultIncludeLicenseContent, - scanner: s.Scan, + coverageThreshold: DefaultCoverageThreshold, + scanner: s.Scan, } for _, opt := range o { @@ -78,8 +76,7 @@ func NewScanner(c *ScannerConfig) (Scanner, error) { } return &scanner{ - coverageThreshold: c.CoverageThreshold, - includeLicenseContent: c.IncludeLicenseContent, - scanner: c.Scanner, + coverageThreshold: c.CoverageThreshold, + scanner: c.Scanner, }, nil } diff --git a/internal/licenses/scanner_test.go b/internal/licenses/scanner_test.go deleted file mode 100644 index 090a65464..000000000 --- a/internal/licenses/scanner_test.go +++ /dev/null @@ -1,84 +0,0 @@ -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 -} diff --git a/internal/licenses/search.go b/internal/licenses/search.go deleted file mode 100644 index 1ade9b633..000000000 --- a/internal/licenses/search.go +++ /dev/null @@ -1,123 +0,0 @@ -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 -} diff --git a/internal/licenses/search_test.go b/internal/licenses/search_test.go deleted file mode 100644 index 11138bf49..000000000 --- a/internal/licenses/search_test.go +++ /dev/null @@ -1,166 +0,0 @@ -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) - } - }) - } -} diff --git a/internal/licenses/test-fixtures/multi-license b/internal/licenses/test-fixtures/multi-license new file mode 100644 index 000000000..55e8f119b --- /dev/null +++ b/internal/licenses/test-fixtures/multi-license @@ -0,0 +1,445 @@ +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 + + 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 + +============================================================================== + +Files: tools/eliminator/node_modules/uglify-js/... tools/node_modules/terser/... + + Distributed under the BSD license: + + Copyright 2012 (c) Mihai Bazon + + 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 +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. \ No newline at end of file diff --git a/internal/task/package_task_factory.go b/internal/task/package_task_factory.go index 403b0eca7..7834edeb1 100644 --- a/internal/task/package_task_factory.go +++ b/internal/task/package_task_factory.go @@ -107,6 +107,11 @@ 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 @@ -262,3 +267,29 @@ 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...) +} diff --git a/internal/task/package_task_factory_test.go b/internal/task/package_task_factory_test.go index 592da3c94..d42030794 100644 --- a/internal/task/package_task_factory_test.go +++ b/internal/task/package_task_factory_test.go @@ -117,6 +117,114 @@ 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() diff --git a/syft/cataloging/license.go b/syft/cataloging/license.go index 24d8686f5..6d8e58fea 100644 --- a/syft/cataloging/license.go +++ b/syft/cataloging/license.go @@ -1,15 +1,33 @@ package cataloging -import "github.com/anchore/syft/internal/licenses" +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" +) type LicenseConfig struct { - 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"` + // 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"` } func DefaultLicenseConfig() LicenseConfig { return LicenseConfig{ - IncludeUnkownLicenseContent: licenses.DefaultIncludeLicenseContent, - Coverage: licenses.DefaultCoverageThreshold, + IncludeContent: LicenseContentExcludeAll, + Coverage: licenses.DefaultCoverageThreshold, } } diff --git a/syft/create_sbom.go b/syft/create_sbom.go index 68605d564..32c34c00b 100644 --- a/syft/create_sbom.go +++ b/syft/create_sbom.go @@ -109,7 +109,6 @@ 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 { diff --git a/syft/format/common/spdxhelpers/to_format_model.go b/syft/format/common/spdxhelpers/to_format_model.go index 9e05ca380..48d2c3ace 100644 --- a/syft/format/common/spdxhelpers/to_format_model.go +++ b/syft/format/common/spdxhelpers/to_format_model.go @@ -5,7 +5,6 @@ import ( "crypto/sha1" "fmt" "path" - "regexp" "slices" "sort" "strings" @@ -15,7 +14,6 @@ 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" @@ -50,7 +48,7 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document { name, namespace := helpers.DocumentNameAndNamespace(s.Source, s.Descriptor) rels := relationship.NewIndex(s.Relationships...) - packages := toPackages(rels, s.Artifacts.Packages, s) + packages, otherLicenses := toPackages(rels, s.Artifacts.Packages, s) allRelationships := toRelationships(rels.All()) @@ -156,7 +154,7 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document { Packages: packages, Files: toFiles(s), Relationships: allRelationships, - OtherLicenses: toOtherLicenses(s.Artifacts.Packages), + OtherLicenses: convertOtherLicense(otherLicenses), } } @@ -319,16 +317,18 @@ 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) { +func toPackages(rels *relationship.Index, catalog *pkg.Collection, sbom sbom.SBOM) (results []*spdx.Package, otherLicenses []spdx.OtherLicense) { + otherLicenseSet := helpers.NewSPDXOtherLicenseSet() 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 := helpers.License(p) + concluded, declared, ol := helpers.License(p) + otherLicenseSet.Add(ol...) // two ways to get filesAnalyzed == true: // 1. syft has generated a sha1 digest for the package itself - usually in the java cataloger @@ -487,7 +487,7 @@ func toPackages(rels *relationship.Index, catalog *pkg.Collection, sbom sbom.SBO PackageAttributionTexts: nil, }) } - return results + return results, otherLicenseSet.ToSlice() } func toPackageChecksums(p pkg.Package) ([]spdx.Checksum, bool) { @@ -740,81 +740,14 @@ 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 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 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 // a package verification code coordinates := rels.Coordinates(p, artifact.ContainsRelationship) var digests []file.Digest @@ -887,3 +820,15 @@ 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 +} diff --git a/syft/format/common/spdxhelpers/to_format_model_test.go b/syft/format/common/spdxhelpers/to_format_model_test.go index 67bc2885e..fe666c3bf 100644 --- a/syft/format/common/spdxhelpers/to_format_model_test.go +++ b/syft/format/common/spdxhelpers/to_format_model_test.go @@ -1,8 +1,10 @@ package spdxhelpers import ( + "context" "fmt" "regexp" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -736,7 +738,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 == "" { @@ -753,29 +755,31 @@ 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: nil, + expected: []spdx.OtherLicense{}, }, { name: "single licenseRef", pkg: pkg.Package{ Licenses: pkg.NewLicenseSet( - pkg.NewLicense("foobar"), + pkg.NewLicenseWithContext(ctx, "foobar"), ), }, - expected: []*spdx.OtherLicense{ + expected: []spdx.OtherLicense{ { LicenseIdentifier: "LicenseRef-foobar", - ExtractedText: "foobar", + LicenseName: "foobar", + ExtractedText: "NOASSERTION", }, }, }, @@ -783,18 +787,20 @@ func Test_OtherLicenses(t *testing.T) { name: "multiple licenseRef", pkg: pkg.Package{ Licenses: pkg.NewLicenseSet( - pkg.NewLicense("internal made up license name"), - pkg.NewLicense("new apple license 2.0"), + pkg.NewLicenseWithContext(ctx, "internal made up license name"), + pkg.NewLicenseWithContext(ctx, "new apple license 2.0"), ), }, - expected: []*spdx.OtherLicense{ + expected: []spdx.OtherLicense{ { LicenseIdentifier: "LicenseRef-internal-made-up-license-name", - ExtractedText: "internal made up license name", + ExtractedText: "NOASSERTION", + LicenseName: "internal made up license name", }, { LicenseIdentifier: "LicenseRef-new-apple-license-2.0", - ExtractedText: "new apple license 2.0", + ExtractedText: "NOASSERTION", + LicenseName: "new apple license 2.0", }, }, }, @@ -802,31 +808,27 @@ func Test_OtherLicenses(t *testing.T) { name: "LicenseRef as a valid spdx expression", pkg: pkg.Package{ Licenses: pkg.NewLicenseSet( - pkg.NewLicense("LicenseRef-Fedora-Public-Domain"), + pkg.NewLicenseWithContext(ctx, "LicenseRef-Fedora-Public-Domain"), ), }, - expected: []*spdx.OtherLicense{ - { - LicenseIdentifier: "LicenseRef-Fedora-Public-Domain", - ExtractedText: "Fedora-Public-Domain", - }, - }, + expected: []spdx.OtherLicense{}, }, { name: "LicenseRef as a valid spdx expression does not otherize compound spdx expressions", pkg: pkg.Package{ Licenses: pkg.NewLicenseSet( - pkg.NewLicense("(MIT AND LicenseRef-Fedora-Public-Domain)"), + pkg.NewLicenseWithContext(ctx, "(MIT AND LicenseRef-Fedora-Public-Domain)"), ), }, - expected: nil, + expected: []spdx.OtherLicense{}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { catalog := pkg.NewCollection(test.pkg) - otherLicenses := toOtherLicenses(catalog) + rels := relationship.NewIndex() + _, otherLicenses := toPackages(rels, catalog, sbom.SBOM{}) require.Len(t, otherLicenses, len(test.expected)) require.Equal(t, test.expected, otherLicenses) }) @@ -902,18 +904,19 @@ 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.NewLicense("MIT"), + pkg.NewLicenseWithContext(ctx, "MIT"), ), } pkg2 := pkg.Package{ Name: "second-pkg", Version: "2.2", Licenses: pkg.NewLicenseSet( - pkg.NewLicense("non spdx license"), + pkg.NewLicenseWithContext(ctx, "non spdx license"), ), } bigText := ` @@ -923,7 +926,7 @@ func Test_otherLicenses(t *testing.T) { Name: "third-pkg", Version: "3.3", Licenses: pkg.NewLicenseSet( - pkg.NewLicense(bigText), + pkg.NewLicenseWithContext(ctx, bigText), ), } @@ -938,22 +941,25 @@ func Test_otherLicenses(t *testing.T) { expected: nil, }, { - name: "other licenses includes original text", + name: "other licenses must include some original text", packages: []pkg.Package{pkg2}, expected: []*spdx.OtherLicense{ { LicenseIdentifier: "LicenseRef-non-spdx-license", - ExtractedText: "non spdx license", + LicenseName: "non spdx license", + ExtractedText: "NOASSERTION", }, }, }, { - name: "big licenses get hashed", + name: "big licenses get hashed and space is trimmed", packages: []pkg.Package{pkg3}, expected: []*spdx.OtherLicense{ { - LicenseIdentifier: "LicenseRef-e9a1e42833d3e456f147052f4d312101bd171a0798893169fe596ca6b55c049e", - ExtractedText: bigText, + LicenseIdentifier: "LicenseRef-3f17782eef51ae86f18fdd6832f5918e2b40f688b52c9adc07ba6ec1024ef408", + // Carries through the syft-json license value when we shasum large texts + LicenseName: "LicenseRef-sha256:3f17782eef51ae86f18fdd6832f5918e2b40f688b52c9adc07ba6ec1024ef408", + ExtractedText: strings.TrimSpace(bigText), }, }, }, diff --git a/syft/format/common/spdxhelpers/to_syft_model.go b/syft/format/common/spdxhelpers/to_syft_model.go index 280eba20d..2c15111af 100644 --- a/syft/format/common/spdxhelpers/to_syft_model.go +++ b/syft/format/common/spdxhelpers/to_syft_model.go @@ -1,6 +1,7 @@ package spdxhelpers import ( + "context" "errors" "fmt" "net/url" @@ -535,14 +536,14 @@ func parseSPDXLicenses(p *spdx.Package) []pkg.License { // concluded if p.PackageLicenseConcluded != helpers.NOASSERTION && p.PackageLicenseConcluded != helpers.NONE && p.PackageLicenseConcluded != "" { - l := pkg.NewLicense(cleanSPDXID(p.PackageLicenseConcluded)) + l := pkg.NewLicenseWithContext(context.TODO(), 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.NewLicense(cleanSPDXID(p.PackageLicenseDeclared)) + l := pkg.NewLicenseWithContext(context.TODO(), cleanSPDXID(p.PackageLicenseDeclared)) l.Type = license.Declared licenses = append(licenses, l) } diff --git a/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden index 264e3ac9c..8c376f1a6 100644 --- a/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden +++ b/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -24,7 +24,7 @@ }, "components": [ { - "bom-ref": "4dd25c6ee16b729a", + "bom-ref": "f04d218ff5ff50db", "type": "library", "name": "package-1", "version": "1.0.1", diff --git a/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden index 3237b30d9..93ee86aec 100644 --- a/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden +++ b/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden @@ -25,7 +25,7 @@ }, "components": [ { - "bom-ref": "72567175418f73f8", + "bom-ref": "2f52f617f1548337", "type": "library", "name": "package-1", "version": "1.0.1", diff --git a/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden index 57a48832c..c66aaec7a 100644 --- a/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden +++ b/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -16,7 +16,7 @@ - + package-1 1.0.1 diff --git a/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden index 7c55b107a..bab78abd0 100644 --- a/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden +++ b/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden @@ -17,7 +17,7 @@ - + package-1 1.0.1 diff --git a/syft/format/internal/cyclonedxutil/helpers/licenses.go b/syft/format/internal/cyclonedxutil/helpers/licenses.go index 2ddbbb946..37cbd53f0 100644 --- a/syft/format/internal/cyclonedxutil/helpers/licenses.go +++ b/syft/format/internal/cyclonedxutil/helpers/licenses.go @@ -1,6 +1,7 @@ package helpers import ( + "context" "encoding/base64" "strings" @@ -58,11 +59,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.NewLicenseFromURLs(l.License.ID, l.License.URL)) + licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), l.License.ID, l.License.URL)) case l.License != nil && l.License.Name != "": - licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.Name, l.License.URL)) + licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), l.License.Name, l.License.URL)) case l.Expression != "": - licenses = append(licenses, pkg.NewLicense(l.Expression)) + licenses = append(licenses, pkg.NewLicenseWithContext(context.TODO(), l.Expression)) default: } } diff --git a/syft/format/internal/cyclonedxutil/helpers/licenses_test.go b/syft/format/internal/cyclonedxutil/helpers/licenses_test.go index 737555d63..d330f5d17 100644 --- a/syft/format/internal/cyclonedxutil/helpers/licenses_test.go +++ b/syft/format/internal/cyclonedxutil/helpers/licenses_test.go @@ -1,6 +1,7 @@ package helpers import ( + "context" "testing" "github.com/CycloneDX/cyclonedx-go" @@ -12,6 +13,7 @@ import ( ) func Test_encodeLicense(t *testing.T) { + ctx := context.TODO() tests := []struct { name string input pkg.Package @@ -25,7 +27,7 @@ func Test_encodeLicense(t *testing.T) { name: "no SPDX licenses", input: pkg.Package{ Licenses: pkg.NewLicenseSet( - pkg.NewLicense("RandomLicense"), + pkg.NewLicenseWithContext(ctx, "RandomLicense"), ), }, expected: &cyclonedx.Licenses{ @@ -40,8 +42,8 @@ func Test_encodeLicense(t *testing.T) { name: "single SPDX ID and Non SPDX ID", input: pkg.Package{ Licenses: pkg.NewLicenseSet( - pkg.NewLicense("mit"), - pkg.NewLicense("FOOBAR"), + pkg.NewLicenseWithContext(ctx, "mit"), + pkg.NewLicenseWithContext(ctx, "FOOBAR"), ), }, expected: &cyclonedx.Licenses{ @@ -61,7 +63,7 @@ func Test_encodeLicense(t *testing.T) { name: "with complex SPDX license expression", input: pkg.Package{ Licenses: pkg.NewLicenseSet( - pkg.NewLicense("MIT AND GPL-3.0-only"), + pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"), ), }, expected: &cyclonedx.Licenses{ @@ -74,8 +76,8 @@ func Test_encodeLicense(t *testing.T) { name: "with multiple complex SPDX license expression", input: pkg.Package{ Licenses: pkg.NewLicenseSet( - pkg.NewLicense("MIT AND GPL-3.0-only"), - pkg.NewLicense("MIT AND GPL-3.0-only WITH Classpath-exception-2.0"), + pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"), + pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only WITH Classpath-exception-2.0"), ), }, expected: &cyclonedx.Licenses{ @@ -88,9 +90,9 @@ func Test_encodeLicense(t *testing.T) { name: "with multiple URLs and expressions", input: pkg.Package{ Licenses: pkg.NewLicenseSet( - 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"), + 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"), ), }, expected: &cyclonedx.Licenses{ @@ -123,8 +125,8 @@ func Test_encodeLicense(t *testing.T) { name: "with multiple values licenses are deduplicated", input: pkg.Package{ Licenses: pkg.NewLicenseSet( - pkg.NewLicense("Apache-2"), - pkg.NewLicense("Apache-2.0"), + pkg.NewLicenseWithContext(ctx, "Apache-2"), + pkg.NewLicenseWithContext(ctx, "Apache-2.0"), ), }, expected: &cyclonedx.Licenses{ @@ -139,9 +141,9 @@ func Test_encodeLicense(t *testing.T) { name: "with multiple URLs and single with no URLs", input: pkg.Package{ Licenses: pkg.NewLicenseSet( - 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"), + 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"), ), }, expected: &cyclonedx.Licenses{ @@ -167,7 +169,7 @@ func Test_encodeLicense(t *testing.T) { { name: "single parenthesized SPDX expression", input: pkg.Package{ - Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValues("(MIT OR Apache-2.0)")...), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, "(MIT OR Apache-2.0)")...), }, expected: &cyclonedx.Licenses{ { @@ -179,7 +181,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.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")...), + 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")...), }, expected: &cyclonedx.Licenses{ { @@ -248,7 +250,6 @@ func TestDecodeLicenses(t *testing.T) { Value: "RandomLicense", // CycloneDX specification doesn't give a field for determining the license type Type: license.Declared, - URLs: []string{}, }, }, }, @@ -268,7 +269,6 @@ func TestDecodeLicenses(t *testing.T) { Value: "MIT", SPDXExpression: "MIT", Type: license.Declared, - URLs: []string{}, }, }, }, diff --git a/syft/format/internal/spdxutil/helpers/license.go b/syft/format/internal/spdxutil/helpers/license.go index c6db3d819..979ea16d5 100644 --- a/syft/format/internal/spdxutil/helpers/license.go +++ b/syft/format/internal/spdxutil/helpers/license.go @@ -1,16 +1,17 @@ package helpers import ( - "crypto/sha256" - "fmt" + "sort" "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) { +func License(p pkg.Package) (concluded, declared string, otherLicenses []spdx.OtherLicense) { // 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; @@ -21,15 +22,15 @@ func License(p pkg.Package) (concluded, declared string) { // (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 + return NOASSERTION, NOASSERTION, nil } // 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 := ParseLicenses(p.Licenses.ToSlice()) + pc, pd, ol := ParseLicenses(p.Licenses.ToSlice()) - return joinLicenses(pc), joinLicenses(pd) + return joinLicenses(pc), joinLicenses(pd), ol } func joinLicenses(licenses []SPDXLicense) string { @@ -58,14 +59,32 @@ func joinLicenses(licenses []SPDXLicense) string { } type SPDXLicense struct { - ID string - Value string - FullText string + // 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 } -func ParseLicenses(raw []pkg.License) (concluded, declared []SPDXLicense) { +func ParseLicenses(raw []pkg.License) (concluded, declared []SPDXLicense, otherLicenses []spdx.OtherLicense) { 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) @@ -74,35 +93,70 @@ func ParseLicenses(raw []pkg.License) (concluded, declared []SPDXLicense) { } } - return concluded, declared + return concluded, declared, otherLicenses } func createSPDXLicense(l pkg.License) SPDXLicense { - candidate := SPDXLicense{ - ID: generateLicenseID(l), - FullText: l.Contents, + // 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 } - if l.SPDXExpression == "" { - candidate.Value = l.Value + return SPDXLicense{ + ID: generateLicenseID(l), + LicenseName: l.Value, + FullText: ft, + URLs: l.URLs, } - 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 } - if l.Value != "" { - return spdxlicense.LicenseRefPrefix + SanitizeElementID(l.Value) + + // syft format includes the algo for the sha in the values + // we can strip this and just make LicenseRef- for spdx consumption + id := strings.ReplaceAll(l.Value, "sha256:", "") + if !strings.HasPrefix(id, "LicenseRef-") { + id = "LicenseRef-" + id } - return licenseSum(l.Contents) + return SanitizeElementID(id) } -func licenseSum(s string) string { - if len(s) <= 64 { - return spdxlicense.LicenseRefPrefix + SanitizeElementID(s) - } - hash := sha256.Sum256([]byte(s)) - return fmt.Sprintf("%s%x", spdxlicense.LicenseRefPrefix, hash) +type SPDXOtherLicenseSet struct { + set map[string]spdx.OtherLicense +} + +func NewSPDXOtherLicenseSet() *SPDXOtherLicenseSet { + return &SPDXOtherLicenseSet{ + set: make(map[string]spdx.OtherLicense), + } +} + +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 } diff --git a/syft/format/internal/spdxutil/helpers/license_test.go b/syft/format/internal/spdxutil/helpers/license_test.go index 19e466ab4..4dd262ec3 100644 --- a/syft/format/internal/spdxutil/helpers/license_test.go +++ b/syft/format/internal/spdxutil/helpers/license_test.go @@ -1,9 +1,10 @@ package helpers import ( - "strings" + "context" "testing" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/anchore/syft/internal/spdxlicense" @@ -11,6 +12,7 @@ import ( ) func Test_License(t *testing.T) { + ctx := context.TODO() type expected struct { concluded string declared string @@ -31,7 +33,7 @@ func Test_License(t *testing.T) { { name: "no SPDX licenses", input: pkg.Package{ - Licenses: pkg.NewLicenseSet(pkg.NewLicense("made-up")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "made-up")), }, expected: expected{ concluded: "NOASSERTION", @@ -41,7 +43,7 @@ func Test_License(t *testing.T) { { name: "with SPDX license", input: pkg.Package{ - Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "MIT")), }, expected: struct { concluded string @@ -55,8 +57,8 @@ func Test_License(t *testing.T) { name: "with SPDX license expression", input: pkg.Package{ Licenses: pkg.NewLicenseSet( - pkg.NewLicense("MIT"), - pkg.NewLicense("GPL-3.0-only"), + pkg.NewLicenseWithContext(ctx, "MIT"), + pkg.NewLicenseWithContext(ctx, "GPL-3.0-only"), ), }, expected: expected{ @@ -69,9 +71,9 @@ func Test_License(t *testing.T) { name: "includes valid LicenseRef-", input: pkg.Package{ Licenses: pkg.NewLicenseSet( - pkg.NewLicense("one thing first"), - pkg.NewLicense("two things/#$^second"), - pkg.NewLicense("MIT"), + pkg.NewLicenseWithContext(ctx, "one thing first"), + pkg.NewLicenseWithContext(ctx, "two things/#$^second"), + pkg.NewLicenseWithContext(ctx, "MIT"), ), }, expected: expected{ @@ -84,9 +86,9 @@ func Test_License(t *testing.T) { name: "join parentheses correctly", input: pkg.Package{ Licenses: pkg.NewLicenseSet( - pkg.NewLicense("one thing first"), - pkg.NewLicense("MIT AND GPL-3.0-only"), - pkg.NewLicense("MIT OR APACHE-2.0"), + pkg.NewLicenseWithContext(ctx, "one thing first"), + pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"), + pkg.NewLicenseWithContext(ctx, "MIT OR APACHE-2.0"), ), }, expected: expected{ @@ -98,7 +100,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) }) @@ -123,11 +125,13 @@ func TestGenerateLicenseID(t *testing.T) { { name: "Uses value if no SPDX expression", license: pkg.License{ - Value: "MIT", + Value: "my-sweet-custom-license", }, - expected: spdxlicense.LicenseRefPrefix + "MIT", + expected: spdxlicense.LicenseRefPrefix + "my-sweet-custom-license", }, { + // 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", @@ -135,13 +139,6 @@ 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 { @@ -160,34 +157,92 @@ func TestGenerateLicenseID(t *testing.T) { func Test_joinLicenses(t *testing.T) { tests := []struct { name string - args []string + args []SPDXLicense want string }{ { name: "multiple licenses", - args: []string{"MIT", "GPL-3.0-only"}, + args: []SPDXLicense{{ID: "MIT"}, {ID: "GPL-3.0-only"}}, want: "MIT AND GPL-3.0-only", }, { name: "multiple licenses with complex expressions", - args: []string{"MIT AND Apache", "GPL-3.0-only"}, + args: []SPDXLicense{{ID: "MIT AND Apache"}, {ID: "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(toSpdxLicenses(tt.args)), "joinLicenses(%v)", tt.args) + assert.Equalf(t, tt.want, joinLicenses(tt.args), "joinLicenses(%v)", tt.args) }) } } -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) +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) + } + }) } - return licenses } diff --git a/syft/format/internal/testutil/directory_input.go b/syft/format/internal/testutil/directory_input.go index e3ac03077..22bb91d63 100644 --- a/syft/format/internal/testutil/directory_input.go +++ b/syft/format/internal/testutil/directory_input.go @@ -1,6 +1,7 @@ package testutil import ( + "context" "os" "path/filepath" "testing" @@ -98,7 +99,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", @@ -110,7 +111,7 @@ func newDirectoryCatalog() *pkg.Collection { ), Language: pkg.Python, Licenses: pkg.NewLicenseSet( - pkg.NewLicense("MIT"), + pkg.NewLicenseWithContext(ctx, "MIT"), ), Metadata: pkg.PythonPackage{ Name: "package-1", @@ -149,7 +150,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", @@ -161,7 +162,7 @@ func newDirectoryCatalogWithAuthorField() *pkg.Collection { ), Language: pkg.Python, Licenses: pkg.NewLicenseSet( - pkg.NewLicense("MIT"), + pkg.NewLicenseWithContext(ctx, "MIT"), ), Metadata: pkg.PythonPackage{ Name: "package-1", diff --git a/syft/format/internal/testutil/image_input.go b/syft/format/internal/testutil/image_input.go index d033b3cc5..d40064e70 100644 --- a/syft/format/internal/testutil/image_input.go +++ b/syft/format/internal/testutil/image_input.go @@ -1,6 +1,7 @@ package testutil import ( + "context" "os" "path/filepath" "testing" @@ -98,7 +99,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{ @@ -111,7 +112,7 @@ func populateImageCatalog(catalog *pkg.Collection, img *image.Image) { FoundBy: "the-cataloger-1", Language: pkg.Python, Licenses: pkg.NewLicenseSet( - pkg.NewLicense("MIT"), + pkg.NewLicenseWithContext(ctx, "MIT"), ), Metadata: pkg.PythonPackage{ Name: "package-1", diff --git a/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden b/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden index 110d291dd..2a93b488c 100644 --- a/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden +++ b/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden @@ -15,7 +15,7 @@ "packages": [ { "name": "package-1", - "SPDXID": "SPDXRef-Package-python-package-1-4dd25c6ee16b729a", + "SPDXID": "SPDXRef-Package-python-package-1-f04d218ff5ff50db", "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-4dd25c6ee16b729a", + "relatedSpdxElement": "SPDXRef-Package-python-package-1-f04d218ff5ff50db", "relationshipType": "CONTAINS" }, { diff --git a/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden b/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden index 3a75a972c..3e55bda38 100644 --- a/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden +++ b/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden @@ -15,7 +15,7 @@ "packages": [ { "name": "package-1", - "SPDXID": "SPDXRef-Package-python-package-1-72567175418f73f8", + "SPDXID": "SPDXRef-Package-python-package-1-2f52f617f1548337", "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-72567175418f73f8", + "relatedSpdxElement": "SPDXRef-Package-python-package-1-2f52f617f1548337", "relationshipType": "CONTAINS" }, { diff --git a/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden b/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden index 2a8ad1ebe..2edf48c7e 100644 --- a/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden +++ b/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden @@ -15,7 +15,7 @@ "packages": [ { "name": "package-1", - "SPDXID": "SPDXRef-Package-python-package-1-72567175418f73f8", + "SPDXID": "SPDXRef-Package-python-package-1-2f52f617f1548337", "versionInfo": "1.0.1", "supplier": "NOASSERTION", "downloadLocation": "NOASSERTION", @@ -199,38 +199,38 @@ ], "relationships": [ { - "spdxElementId": "SPDXRef-Package-python-package-1-72567175418f73f8", + "spdxElementId": "SPDXRef-Package-python-package-1-2f52f617f1548337", "relatedSpdxElement": "SPDXRef-File-f1-5265a4dde3edbf7c", "relationshipType": "CONTAINS" }, { - "spdxElementId": "SPDXRef-Package-python-package-1-72567175418f73f8", + "spdxElementId": "SPDXRef-Package-python-package-1-2f52f617f1548337", "relatedSpdxElement": "SPDXRef-File-z1-f5-839d99ee67d9d174", "relationshipType": "CONTAINS" }, { - "spdxElementId": "SPDXRef-Package-python-package-1-72567175418f73f8", + "spdxElementId": "SPDXRef-Package-python-package-1-2f52f617f1548337", "relatedSpdxElement": "SPDXRef-File-a1-f6-9c2f7510199b17f6", "relationshipType": "CONTAINS" }, { - "spdxElementId": "SPDXRef-Package-python-package-1-72567175418f73f8", + "spdxElementId": "SPDXRef-Package-python-package-1-2f52f617f1548337", "relatedSpdxElement": "SPDXRef-File-d2-f4-c641caa71518099f", "relationshipType": "CONTAINS" }, { - "spdxElementId": "SPDXRef-Package-python-package-1-72567175418f73f8", + "spdxElementId": "SPDXRef-Package-python-package-1-2f52f617f1548337", "relatedSpdxElement": "SPDXRef-File-d1-f3-c6f5b29dca12661f", "relationshipType": "CONTAINS" }, { - "spdxElementId": "SPDXRef-Package-python-package-1-72567175418f73f8", + "spdxElementId": "SPDXRef-Package-python-package-1-2f52f617f1548337", "relatedSpdxElement": "SPDXRef-File-f2-f9e49132a4b96ccd", "relationshipType": "CONTAINS" }, { "spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input", - "relatedSpdxElement": "SPDXRef-Package-python-package-1-72567175418f73f8", + "relatedSpdxElement": "SPDXRef-Package-python-package-1-2f52f617f1548337", "relationshipType": "CONTAINS" }, { diff --git a/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden b/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden index bc4804074..fb5ffb8e9 100644 --- a/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden +++ b/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden @@ -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-72567175418f73f8 +SPDXID: SPDXRef-Package-python-package-1-2f52f617f1548337 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-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-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-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-4b756c6f6fb127a3 Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Image-user-image-input diff --git a/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden b/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden index 69d8e7e8a..a86e61f84 100644 --- a/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden +++ b/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden @@ -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-4dd25c6ee16b729a +SPDXID: SPDXRef-Package-python-package-1-f04d218ff5ff50db 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-4dd25c6ee16b729a +Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-python-package-1-f04d218ff5ff50db Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-deb-package-2-39392bb5e270f669 Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Directory-some-path diff --git a/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden b/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden index 2031d7663..54afe3558 100644 --- a/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden +++ b/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden @@ -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-72567175418f73f8 +SPDXID: SPDXRef-Package-python-package-1-2f52f617f1548337 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-72567175418f73f8 +Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-2f52f617f1548337 Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-4b756c6f6fb127a3 Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Image-user-image-input diff --git a/syft/format/syftjson/encoder_test.go b/syft/format/syftjson/encoder_test.go index 292fd6169..c07581884 100644 --- a/syft/format/syftjson/encoder_test.go +++ b/syft/format/syftjson/encoder_test.go @@ -2,6 +2,7 @@ package syftjson import ( "bytes" + "context" "flag" "strings" "testing" @@ -132,7 +133,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", @@ -144,7 +145,7 @@ func TestEncodeFullJSONDocument(t *testing.T) { Type: pkg.PythonPkg, FoundBy: "the-cataloger-1", Language: pkg.Python, - Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "MIT")), Metadata: pkg.PythonPackage{ Name: "package-1", Version: "1.0.1", diff --git a/syft/format/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden b/syft/format/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden index 8ced61f4e..646e72073 100644 --- a/syft/format/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden +++ b/syft/format/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden @@ -1,7 +1,7 @@ { "artifacts": [ { - "id": "4dd25c6ee16b729a", + "id": "f04d218ff5ff50db", "name": "package-1", "version": "1.0.1", "type": "python", diff --git a/syft/format/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/syft/format/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden index 3c017a4de..4b370e34a 100644 --- a/syft/format/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden +++ b/syft/format/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden @@ -1,7 +1,7 @@ { "artifacts": [ { - "id": "fba4ca04d4906f25", + "id": "951845d9a8d6b5b2", "name": "package-1", "version": "1.0.1", "type": "python", diff --git a/syft/format/syftjson/test-fixtures/snapshot/TestImageEncoder.golden b/syft/format/syftjson/test-fixtures/snapshot/TestImageEncoder.golden index 62379330c..99683db91 100644 --- a/syft/format/syftjson/test-fixtures/snapshot/TestImageEncoder.golden +++ b/syft/format/syftjson/test-fixtures/snapshot/TestImageEncoder.golden @@ -1,7 +1,7 @@ { "artifacts": [ { - "id": "72567175418f73f8", + "id": "2f52f617f1548337", "name": "package-1", "version": "1.0.1", "type": "python", diff --git a/syft/pkg/cataloger/alpine/cataloger_test.go b/syft/pkg/cataloger/alpine/cataloger_test.go index fff8e77f4..dd90b469a 100644 --- a/syft/pkg/cataloger/alpine/cataloger_test.go +++ b/syft/pkg/cataloger/alpine/cataloger_test.go @@ -1,6 +1,7 @@ package alpine import ( + "context" "testing" "github.com/google/go-cmp/cmp" @@ -14,14 +15,14 @@ import ( func TestApkDBCataloger(t *testing.T) { dbLocation := file.NewLocation("lib/apk/db/installed") - + ctx := context.TODO() bashPkg := pkg.Package{ Name: "bash", Version: "5.2.21-r0", Type: pkg.ApkPkg, FoundBy: "apk-db-cataloger", Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("GPL-3.0-or-later", dbLocation), + pkg.NewLicenseFromLocationsWithContext(ctx, "GPL-3.0-or-later", dbLocation), ), Locations: file.NewLocationSet(dbLocation), Metadata: pkg.ApkDBEntry{ @@ -50,7 +51,7 @@ func TestApkDBCataloger(t *testing.T) { Type: pkg.ApkPkg, FoundBy: "apk-db-cataloger", Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("GPL-2.0-only", dbLocation), + pkg.NewLicenseFromLocationsWithContext(ctx, "GPL-2.0-only", dbLocation), ), Locations: file.NewLocationSet(dbLocation), Metadata: pkg.ApkDBEntry{ @@ -79,7 +80,7 @@ func TestApkDBCataloger(t *testing.T) { Type: pkg.ApkPkg, FoundBy: "apk-db-cataloger", Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", dbLocation), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", dbLocation), ), Locations: file.NewLocationSet(dbLocation), Metadata: pkg.ApkDBEntry{ @@ -106,7 +107,7 @@ func TestApkDBCataloger(t *testing.T) { Type: pkg.ApkPkg, FoundBy: "apk-db-cataloger", Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("GPL-2.0-or-later", dbLocation), + pkg.NewLicenseFromLocationsWithContext(ctx, "GPL-2.0-or-later", dbLocation), ), Locations: file.NewLocationSet(dbLocation), Metadata: pkg.ApkDBEntry{ diff --git a/syft/pkg/cataloger/alpine/package.go b/syft/pkg/cataloger/alpine/package.go index 69c8a8d18..18ecad2df 100644 --- a/syft/pkg/cataloger/alpine/package.go +++ b/syft/pkg/cataloger/alpine/package.go @@ -1,6 +1,7 @@ package alpine import ( + "context" "strings" "github.com/anchore/packageurl-go" @@ -10,7 +11,7 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func newPackage(d parsedData, release *linux.Release, dbLocation file.Location) pkg.Package { +func newPackage(ctx context.Context, d parsedData, release *linux.Release, dbLocation file.Location) pkg.Package { // check if license is a valid spdx expression before splitting licenseStrings := []string{d.License} _, err := license.ParseExpression(d.License) @@ -23,7 +24,7 @@ func newPackage(d parsedData, release *linux.Release, dbLocation file.Location) Name: d.Package, Version: d.Version, Locations: file.NewLocationSet(dbLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), - Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(dbLocation, licenseStrings...)...), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocationWithContext(ctx, dbLocation, licenseStrings...)...), PURL: packageURL(d.ApkDBEntry, release), Type: pkg.ApkPkg, Metadata: d.ApkDBEntry, diff --git a/syft/pkg/cataloger/alpine/parse_apk_db.go b/syft/pkg/cataloger/alpine/parse_apk_db.go index bcc4030ca..548b9df11 100644 --- a/syft/pkg/cataloger/alpine/parse_apk_db.go +++ b/syft/pkg/cataloger/alpine/parse_apk_db.go @@ -36,7 +36,7 @@ type parsedData struct { // information on specific fields, see https://wiki.alpinelinux.org/wiki/Apk_spec. // //nolint:funlen -func parseApkDB(_ context.Context, resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parseApkDB(ctx context.Context, resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { scanner := bufio.NewScanner(reader) var errs error @@ -132,7 +132,7 @@ func parseApkDB(_ context.Context, resolver file.Resolver, env *generic.Environm pkgs := make([]pkg.Package, 0, len(apks)) for _, apk := range apks { - pkgs = append(pkgs, newPackage(apk, r, reader.Location)) + pkgs = append(pkgs, newPackage(ctx, apk, r, reader.Location)) } return pkgs, nil, errs diff --git a/syft/pkg/cataloger/alpine/parse_apk_db_test.go b/syft/pkg/cataloger/alpine/parse_apk_db_test.go index 7918f38f6..7f3b9ce1e 100644 --- a/syft/pkg/cataloger/alpine/parse_apk_db_test.go +++ b/syft/pkg/cataloger/alpine/parse_apk_db_test.go @@ -76,6 +76,7 @@ func TestExtraFileAttributes(t *testing.T) { } func TestSinglePackageDetails(t *testing.T) { + ctx := context.TODO() tests := []struct { fixture string expected pkg.Package @@ -86,9 +87,9 @@ func TestSinglePackageDetails(t *testing.T) { Name: "musl-utils", Version: "1.1.24-r2", Licenses: pkg.NewLicenseSet( - pkg.NewLicense("MIT"), - pkg.NewLicense("BSD"), - pkg.NewLicense("GPL2+"), + pkg.NewLicenseWithContext(ctx, "MIT"), + pkg.NewLicenseWithContext(ctx, "BSD"), + pkg.NewLicenseWithContext(ctx, "GPL2+"), ), Type: pkg.ApkPkg, Metadata: pkg.ApkDBEntry{ @@ -175,7 +176,7 @@ func TestSinglePackageDetails(t *testing.T) { Name: "alpine-baselayout-data", Version: "3.4.0-r0", Licenses: pkg.NewLicenseSet( - pkg.NewLicense("GPL-2.0-only"), + pkg.NewLicenseWithContext(ctx, "GPL-2.0-only"), ), Type: pkg.ApkPkg, Metadata: pkg.ApkDBEntry{ @@ -219,7 +220,7 @@ func TestSinglePackageDetails(t *testing.T) { Name: "alpine-baselayout", Version: "3.2.0-r6", Licenses: pkg.NewLicenseSet( - pkg.NewLicense("GPL-2.0-only"), + pkg.NewLicenseWithContext(ctx, "GPL-2.0-only"), ), Type: pkg.ApkPkg, PURL: "", diff --git a/syft/pkg/cataloger/arch/cataloger_test.go b/syft/pkg/cataloger/arch/cataloger_test.go index e61525246..d234d8507 100644 --- a/syft/pkg/cataloger/arch/cataloger_test.go +++ b/syft/pkg/cataloger/arch/cataloger_test.go @@ -1,6 +1,7 @@ package arch import ( + "context" "testing" "github.com/google/go-cmp/cmp/cmpopts" @@ -25,6 +26,7 @@ func TestAlpmCataloger(t *testing.T) { emacsDbLocation := file.NewLocation("var/lib/pacman/local/emacs-29.3-3/desc") fuzzyDbLocation := file.NewLocation("var/lib/pacman/local/fuzzy-1.2-3/desc") madeupDbLocation := file.NewLocation("var/lib/pacman/local/madeup-20.30-4/desc") + ctx := context.TODO() treeSitterPkg := pkg.Package{ Name: "tree-sitter", @@ -32,7 +34,7 @@ func TestAlpmCataloger(t *testing.T) { Type: pkg.AlpmPkg, FoundBy: "alpm-db-cataloger", Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", treeSitterDbLocation), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", treeSitterDbLocation), ), Locations: file.NewLocationSet(treeSitterDbLocation), Metadata: pkg.AlpmDBEntry{ @@ -58,7 +60,7 @@ func TestAlpmCataloger(t *testing.T) { Type: pkg.AlpmPkg, FoundBy: "alpm-db-cataloger", Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("GPL3", emacsDbLocation), + pkg.NewLicenseFromLocationsWithContext(ctx, "GPL3", emacsDbLocation), ), Locations: file.NewLocationSet(emacsDbLocation), Metadata: pkg.AlpmDBEntry{ @@ -123,8 +125,8 @@ func TestAlpmCataloger(t *testing.T) { Type: pkg.AlpmPkg, FoundBy: "alpm-db-cataloger", Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("LGPL3", gmpDbLocation), - pkg.NewLicenseFromLocations("GPL", gmpDbLocation), + pkg.NewLicenseFromLocationsWithContext(ctx, "LGPL3", gmpDbLocation), + pkg.NewLicenseFromLocationsWithContext(ctx, "GPL", gmpDbLocation), ), Locations: file.NewLocationSet( gmpDbLocation, diff --git a/syft/pkg/cataloger/arch/package.go b/syft/pkg/cataloger/arch/package.go index 88b557c76..a2c5bb3e9 100644 --- a/syft/pkg/cataloger/arch/package.go +++ b/syft/pkg/cataloger/arch/package.go @@ -1,6 +1,7 @@ package arch import ( + "context" "strings" "github.com/anchore/packageurl-go" @@ -9,7 +10,7 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func newPackage(m *parsedData, release *linux.Release, dbLocation file.Location, otherLocations ...file.Location) pkg.Package { +func newPackage(ctx context.Context, m *parsedData, release *linux.Release, dbLocation file.Location, otherLocations ...file.Location) pkg.Package { licenseCandidates := strings.Split(m.Licenses, "\n") locs := file.NewLocationSet(dbLocation) @@ -19,7 +20,7 @@ func newPackage(m *parsedData, release *linux.Release, dbLocation file.Location, Name: m.Package, Version: m.Version, Locations: locs, - Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(dbLocation.WithoutAnnotations(), licenseCandidates...)...), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocationWithContext(ctx, dbLocation.WithoutAnnotations(), licenseCandidates...)...), Type: pkg.AlpmPkg, PURL: packageURL(m, release), Metadata: m.AlpmDBEntry, diff --git a/syft/pkg/cataloger/arch/parse_alpm_db.go b/syft/pkg/cataloger/arch/parse_alpm_db.go index f59a90cf3..c030f60b0 100644 --- a/syft/pkg/cataloger/arch/parse_alpm_db.go +++ b/syft/pkg/cataloger/arch/parse_alpm_db.go @@ -41,7 +41,7 @@ type parsedData struct { } // parseAlpmDB parses the arch linux pacman database flat-files and returns the packages and relationships found within. -func parseAlpmDB(_ context.Context, resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parseAlpmDB(ctx context.Context, resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { var errs error data, err := parseAlpmDBEntry(reader) @@ -78,6 +78,7 @@ func parseAlpmDB(_ context.Context, resolver file.Resolver, env *generic.Environ return []pkg.Package{ newPackage( + ctx, data, env.LinuxRelease, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), diff --git a/syft/pkg/cataloger/binary/elf_package.go b/syft/pkg/cataloger/binary/elf_package.go index 81e4384d1..408732b8f 100644 --- a/syft/pkg/cataloger/binary/elf_package.go +++ b/syft/pkg/cataloger/binary/elf_package.go @@ -1,6 +1,8 @@ package binary import ( + "context" + "github.com/anchore/packageurl-go" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/cpe" @@ -8,11 +10,11 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func newELFPackage(metadata elfBinaryPackageNotes, locations file.LocationSet) pkg.Package { +func newELFPackage(ctx context.Context, metadata elfBinaryPackageNotes, locations file.LocationSet) pkg.Package { p := pkg.Package{ Name: metadata.Name, Version: metadata.Version, - Licenses: pkg.NewLicenseSet(pkg.NewLicense(metadata.License)), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, metadata.License)), PURL: packageURL(metadata), Type: pkgType(metadata.Type), Locations: locations, diff --git a/syft/pkg/cataloger/binary/elf_package_cataloger.go b/syft/pkg/cataloger/binary/elf_package_cataloger.go index 24b4c0b08..dd370b527 100644 --- a/syft/pkg/cataloger/binary/elf_package_cataloger.go +++ b/syft/pkg/cataloger/binary/elf_package_cataloger.go @@ -52,7 +52,7 @@ func (c *elfPackageCataloger) Name() string { return "elf-binary-package-cataloger" } -func (c *elfPackageCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) { +func (c *elfPackageCataloger) Catalog(ctx context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) { var errs error locations, err := resolver.FilesByMIMEType(mimetype.ExecutableMIMETypeSet.List()...) if err != nil { @@ -84,7 +84,7 @@ func (c *elfPackageCataloger) Catalog(_ context.Context, resolver file.Resolver) } // create a package for each unique name/version pair (based on the first note found) - pkgs = append(pkgs, newELFPackage(notes[0], noteLocations)) + pkgs = append(pkgs, newELFPackage(ctx, notes[0], noteLocations)) } // why not return relationships? We have an executable cataloger that will note the dynamic libraries imported by diff --git a/syft/pkg/cataloger/binary/elf_package_test.go b/syft/pkg/cataloger/binary/elf_package_test.go index 6bdaf2b08..342a5894d 100644 --- a/syft/pkg/cataloger/binary/elf_package_test.go +++ b/syft/pkg/cataloger/binary/elf_package_test.go @@ -1,6 +1,7 @@ package binary import ( + "context" "testing" "github.com/google/go-cmp/cmp" @@ -135,6 +136,7 @@ func Test_packageURL(t *testing.T) { } func Test_newELFPackage(t *testing.T) { + ctx := context.TODO() tests := []struct { name string metadata elfBinaryPackageNotes @@ -168,7 +170,7 @@ func Test_newELFPackage(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - actual := newELFPackage(test.metadata, file.NewLocationSet()) + actual := newELFPackage(ctx, test.metadata, file.NewLocationSet()) if diff := cmp.Diff(test.expected, actual, cmpopts.IgnoreFields(pkg.Package{}, "id"), cmpopts.IgnoreUnexported(pkg.Package{}, file.LocationSet{}, pkg.LicenseSet{})); diff != "" { t.Errorf("newELFPackage() mismatch (-want +got):\n%s", diff) } diff --git a/syft/pkg/cataloger/bitnami/cataloger_test.go b/syft/pkg/cataloger/bitnami/cataloger_test.go index 38c1eb823..aa4c55e7e 100644 --- a/syft/pkg/cataloger/bitnami/cataloger_test.go +++ b/syft/pkg/cataloger/bitnami/cataloger_test.go @@ -1,6 +1,7 @@ package bitnami import ( + "context" "testing" "github.com/stretchr/testify/require" @@ -23,14 +24,15 @@ func mustCPEs(s ...string) (c []cpe.CPE) { } func TestBitnamiCataloger(t *testing.T) { + ctx := context.TODO() postgresqlMainPkg := pkg.Package{ Name: "postgresql", Version: "17.2.0-8", Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("PostgreSQL", license.Concluded), - pkg.NewLicenseFromType("PostgreSQL", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "PostgreSQL", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "PostgreSQL", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/postgresql@17.2.0-8?arch=arm64&distro=debian-12", @@ -56,8 +58,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("LGPL-2.1-only", license.Concluded), - pkg.NewLicenseFromType("LGPL-2.1-only", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "LGPL-2.1-only", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "LGPL-2.1-only", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/geos@3.13.0?arch=arm64&distro=debian-12", @@ -78,8 +80,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("MIT", license.Concluded), - pkg.NewLicenseFromType("MIT", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "MIT", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "MIT", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/proj@6.3.2?arch=arm64&distro=debian-12", @@ -100,8 +102,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("MIT", license.Concluded), - pkg.NewLicenseFromType("MIT", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "MIT", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "MIT", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/gdal@3.10.1?arch=arm64&distro=debian-12", @@ -122,8 +124,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("MIT", license.Concluded), - pkg.NewLicenseFromType("MIT", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "MIT", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "MIT", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/json-c@0.16.20220414?arch=arm64&distro=debian-12", @@ -144,8 +146,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("0BSD", license.Concluded), - pkg.NewLicenseFromType("0BSD", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "0BSD", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "0BSD", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/orafce@4.14.1?arch=arm64&distro=debian-12", @@ -166,8 +168,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("BSD-3-Clause", license.Concluded), - pkg.NewLicenseFromType("BSD-3-Clause", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "BSD-3-Clause", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "BSD-3-Clause", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/pljava@1.6.8?arch=arm64&distro=debian-12", @@ -193,8 +195,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("LGPL-2.1-only", license.Concluded), - pkg.NewLicenseFromType("LGPL-2.1-only", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "LGPL-2.1-only", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "LGPL-2.1-only", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/unixodbc@2.3.12?arch=arm64&distro=debian-12", @@ -215,8 +217,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("LGPL-3.0-only", license.Concluded), - pkg.NewLicenseFromType("LGPL-3.0-only", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "LGPL-3.0-only", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "LGPL-3.0-only", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/psqlodbc@16.0.0?arch=arm64&distro=debian-12", @@ -237,8 +239,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("BSD-3-Clause", license.Concluded), - pkg.NewLicenseFromType("BSD-3-Clause", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "BSD-3-Clause", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "BSD-3-Clause", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/protobuf@3.21.12?arch=arm64&distro=debian-12", @@ -259,8 +261,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("BSD-2-Clause", license.Concluded), - pkg.NewLicenseFromType("BSD-2-Clause", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "BSD-2-Clause", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "BSD-2-Clause", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/protobuf-c@1.5.1?arch=arm64&distro=debian-12", @@ -281,8 +283,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("GPL-2.0-or-later", license.Concluded), - pkg.NewLicenseFromType("GPL-2.0-or-later", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "GPL-2.0-or-later", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "GPL-2.0-or-later", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/postgis@3.4.4?arch=arm64&distro=debian-12", @@ -303,8 +305,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("PostgreSQL", license.Concluded), - pkg.NewLicenseFromType("PostgreSQL", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "PostgreSQL", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "PostgreSQL", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/pgaudit@17.0.0?arch=arm64&distro=debian-12", @@ -322,8 +324,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("MIT", license.Concluded), - pkg.NewLicenseFromType("MIT", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "MIT", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "MIT", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/pgbackrest@2.54.2?arch=arm64&distro=debian-12", @@ -344,8 +346,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("BSD-3-Clause", license.Concluded), - pkg.NewLicenseFromType("BSD-3-Clause", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "BSD-3-Clause", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "BSD-3-Clause", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/wal2json@2.6.0?arch=arm64&distro=debian-12", @@ -366,8 +368,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/postgresql/.spdx-postgresql.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("BSD-3-Clause", license.Concluded), - pkg.NewLicenseFromType("BSD-3-Clause", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "BSD-3-Clause", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "BSD-3-Clause", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/nss_wrapper@1.1.16?arch=arm64&distro=debian-12", @@ -402,8 +404,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/render-template/.spdx-render-template.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("Apache-2.0", license.Concluded), - pkg.NewLicenseFromType("Apache-2.0", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "Apache-2.0", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "Apache-2.0", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/render-template@1.0.7-4?arch=arm64&distro=debian-12", @@ -427,8 +429,8 @@ func TestBitnamiCataloger(t *testing.T) { Type: pkg.BitnamiPkg, Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/redis/.spdx-redis.spdx")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromType("RSALv2", license.Concluded), - pkg.NewLicenseFromType("RSALv2", license.Declared), + pkg.NewLicenseFromTypeWithContext(ctx, "RSALv2", license.Concluded), + pkg.NewLicenseFromTypeWithContext(ctx, "RSALv2", license.Declared), ), FoundBy: catalogerName, PURL: "pkg:bitnami/redis@7.4.0-0?arch=arm64&distro=debian-12", diff --git a/syft/pkg/cataloger/debian/cataloger_test.go b/syft/pkg/cataloger/debian/cataloger_test.go index b8c0c6c18..02bd23c9e 100644 --- a/syft/pkg/cataloger/debian/cataloger_test.go +++ b/syft/pkg/cataloger/debian/cataloger_test.go @@ -13,6 +13,7 @@ import ( ) func TestDpkgCataloger(t *testing.T) { + ctx := context.TODO() tests := []struct { name string expected []pkg.Package @@ -25,9 +26,9 @@ func TestDpkgCataloger(t *testing.T) { Version: "1.1.8-3.6", FoundBy: "dpkg-db-cataloger", Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("GPL-1", file.NewLocation("/usr/share/doc/libpam-runtime/copyright")), - pkg.NewLicenseFromLocations("GPL-2", file.NewLocation("/usr/share/doc/libpam-runtime/copyright")), - pkg.NewLicenseFromLocations("LGPL-2.1", file.NewLocation("/usr/share/doc/libpam-runtime/copyright")), + pkg.NewLicenseFromLocationsWithContext(ctx, "GPL-1", file.NewLocation("/usr/share/doc/libpam-runtime/copyright")), + pkg.NewLicenseFromLocationsWithContext(ctx, "GPL-2", file.NewLocation("/usr/share/doc/libpam-runtime/copyright")), + pkg.NewLicenseFromLocationsWithContext(ctx, "LGPL-2.1", file.NewLocation("/usr/share/doc/libpam-runtime/copyright")), ), Locations: file.NewLocationSet( file.NewLocation("/var/lib/dpkg/status").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), @@ -100,9 +101,9 @@ func TestDpkgCataloger(t *testing.T) { Version: "3.34.1-3", FoundBy: "dpkg-db-cataloger", Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("public-domain", file.NewLocation("/usr/share/doc/libsqlite3-0/copyright")), - pkg.NewLicenseFromLocations("GPL-2+", file.NewLocation("/usr/share/doc/libsqlite3-0/copyright")), - pkg.NewLicenseFromLocations("GPL-2", file.NewLocation("/usr/share/doc/libsqlite3-0/copyright")), + pkg.NewLicenseFromLocationsWithContext(ctx, "public-domain", file.NewLocation("/usr/share/doc/libsqlite3-0/copyright")), + pkg.NewLicenseFromLocationsWithContext(ctx, "GPL-2+", file.NewLocation("/usr/share/doc/libsqlite3-0/copyright")), + pkg.NewLicenseFromLocationsWithContext(ctx, "GPL-2", file.NewLocation("/usr/share/doc/libsqlite3-0/copyright")), ), Locations: file.NewLocationSet( file.NewLocation("/var/lib/dpkg/status.d/libsqlite3-0").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), @@ -226,6 +227,7 @@ func Test_CatalogerRelationships(t *testing.T) { } func TestDpkgArchiveCataloger(t *testing.T) { + ctx := context.TODO() tests := []struct { name string expected []pkg.Package @@ -241,7 +243,7 @@ func TestDpkgArchiveCataloger(t *testing.T) { file.NewLocation("/zlib1g.deb"), ), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("Zlib"), + pkg.NewLicenseFromLocationsWithContext(ctx, "Zlib"), ), PURL: "pkg:deb/zlib1g@1%3A1.3.dfsg-3.1ubuntu2.1?arch=amd64&upstream=zlib", Type: pkg.DebPkg, diff --git a/syft/pkg/cataloger/debian/package.go b/syft/pkg/cataloger/debian/package.go index e44cb3844..b10084a59 100644 --- a/syft/pkg/cataloger/debian/package.go +++ b/syft/pkg/cataloger/debian/package.go @@ -1,6 +1,7 @@ package debian import ( + "context" "fmt" "io" "path" @@ -22,7 +23,7 @@ const ( docsPath = "/usr/share/doc" ) -func newDpkgPackage(d pkg.DpkgDBEntry, dbLocation file.Location, resolver file.Resolver, release *linux.Release, evidence ...file.Location) pkg.Package { +func newDpkgPackage(ctx context.Context, d pkg.DpkgDBEntry, dbLocation file.Location, resolver file.Resolver, release *linux.Release, evidence ...file.Location) pkg.Package { // TODO: separate pr to license refactor, but explore extracting dpkg-specific license parsing into a separate function var licenses []pkg.License @@ -46,7 +47,7 @@ func newDpkgPackage(d pkg.DpkgDBEntry, dbLocation file.Location, resolver file.R mergeFileListing(resolver, dbLocation, &p) // fetch additional data from the copyright file to derive the license information - addLicenses(resolver, dbLocation, &p) + addLicenses(ctx, resolver, dbLocation, &p) } p.SetID() @@ -54,11 +55,11 @@ func newDpkgPackage(d pkg.DpkgDBEntry, dbLocation file.Location, resolver file.R return p } -func newDebArchivePackage(location file.Location, metadata pkg.DpkgArchiveEntry, licenseStrings []string) pkg.Package { +func newDebArchivePackage(ctx context.Context, location file.Location, metadata pkg.DpkgArchiveEntry, licenseStrings []string) pkg.Package { p := pkg.Package{ Name: metadata.Package, Version: metadata.Version, - Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValues(licenseStrings...)...), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, licenseStrings...)...), Type: pkg.DebPkg, PURL: packageURL( pkg.DpkgDBEntry(metadata), @@ -108,7 +109,7 @@ func packageURL(m pkg.DpkgDBEntry, distro *linux.Release) string { ).ToString() } -func addLicenses(resolver file.Resolver, dbLocation file.Location, p *pkg.Package) { +func addLicenses(ctx context.Context, resolver file.Resolver, dbLocation file.Location, p *pkg.Package) { metadata, ok := p.Metadata.(pkg.DpkgDBEntry) if !ok { log.WithFields("package", p).Trace("unable to extract DPKG metadata to add licenses") @@ -123,7 +124,7 @@ func addLicenses(resolver file.Resolver, dbLocation file.Location, p *pkg.Packag // attach the licenses licenseStrs := parseLicensesFromCopyright(copyrightReader) for _, licenseStr := range licenseStrs { - p.Licenses.Add(pkg.NewLicenseFromLocations(licenseStr, copyrightLocation.WithoutAnnotations())) + p.Licenses.Add(pkg.NewLicenseFromLocationsWithContext(ctx, licenseStr, copyrightLocation.WithoutAnnotations())) } // keep a record of the file where this was discovered p.Locations.Add(*copyrightLocation) diff --git a/syft/pkg/cataloger/debian/parse_deb_archive.go b/syft/pkg/cataloger/debian/parse_deb_archive.go index 70a3b8ccc..8b78a2147 100644 --- a/syft/pkg/cataloger/debian/parse_deb_archive.go +++ b/syft/pkg/cataloger/debian/parse_deb_archive.go @@ -72,7 +72,7 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen } return []pkg.Package{ - newDebArchivePackage(reader.Location, *metadata, licenses), + newDebArchivePackage(ctx, reader.Location, *metadata, licenses), }, nil, nil } diff --git a/syft/pkg/cataloger/debian/parse_dpkg_db.go b/syft/pkg/cataloger/debian/parse_dpkg_db.go index 54f21a609..2f020d3f8 100644 --- a/syft/pkg/cataloger/debian/parse_dpkg_db.go +++ b/syft/pkg/cataloger/debian/parse_dpkg_db.go @@ -39,7 +39,7 @@ func parseDpkgDB(ctx context.Context, resolver file.Resolver, env *generic.Envir dbLoc := reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation) var pkgs []pkg.Package _ = sync.CollectSlice(&ctx, cataloging.ExecutorFile, sync.ToSeq(metadata), func(m pkg.DpkgDBEntry) (pkg.Package, error) { - return newDpkgPackage(m, dbLoc, resolver, env.LinuxRelease, findDpkgInfoFiles(m.Package, resolver, reader.Location)...), nil + return newDpkgPackage(ctx, m, dbLoc, resolver, env.LinuxRelease, findDpkgInfoFiles(m.Package, resolver, reader.Location)...), nil }, &pkgs) return pkgs, nil, unknown.IfEmptyf(pkgs, "unable to determine packages") diff --git a/syft/pkg/cataloger/gentoo/parse_portage_contents.go b/syft/pkg/cataloger/gentoo/parse_portage_contents.go index a5e3f86dc..30ba84049 100644 --- a/syft/pkg/cataloger/gentoo/parse_portage_contents.go +++ b/syft/pkg/cataloger/gentoo/parse_portage_contents.go @@ -24,7 +24,7 @@ var ( ) // parses individual CONTENTS files from the portage flat-file store (e.g. /var/db/pkg/*/*/CONTENTS). -func parsePortageContents(_ context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parsePortageContents(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { cpvMatch := cpvRe.FindStringSubmatch(reader.RealPath) if cpvMatch == nil { return nil, nil, fmt.Errorf("failed to match package and version in %s", reader.RealPath) @@ -43,7 +43,7 @@ func parsePortageContents(_ context.Context, resolver file.Resolver, _ *generic. locations := file.NewLocationSet(reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) - licenses, licenseLocations := addLicenses(resolver, reader.Location, &m) + licenses, licenseLocations := addLicenses(ctx, resolver, reader.Location, &m) locations.Add(licenseLocations...) locations.Add(addSize(resolver, reader.Location, &m)...) addFiles(resolver, reader.Location, &m) @@ -57,7 +57,6 @@ func parsePortageContents(_ context.Context, resolver file.Resolver, _ *generic. Type: pkg.PortagePkg, Metadata: m, } - p.SetID() return []pkg.Package{p}, nil, nil @@ -89,7 +88,7 @@ func addFiles(resolver file.Resolver, dbLocation file.Location, entry *pkg.Porta } } -func addLicenses(resolver file.Resolver, dbLocation file.Location, entry *pkg.PortageEntry) (pkg.LicenseSet, []file.Location) { +func addLicenses(ctx context.Context, resolver file.Resolver, dbLocation file.Location, entry *pkg.PortageEntry) (pkg.LicenseSet, []file.Location) { parentPath := filepath.Dir(dbLocation.RealPath) location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "LICENSE")) @@ -108,12 +107,8 @@ func addLicenses(resolver file.Resolver, dbLocation file.Location, entry *pkg.Po og, spdxExpression := extractLicenses(resolver, location, licenseReader) entry.Licenses = og - return pkg.NewLicenseSet( - pkg.NewLicenseFromLocations(spdxExpression, *location), - ), - []file.Location{ - location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation), - } + return pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, spdxExpression, *location)), []file.Location{ + location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation)} } func addSize(resolver file.Resolver, dbLocation file.Location, entry *pkg.PortageEntry) []file.Location { diff --git a/syft/pkg/cataloger/golang/cataloger.go b/syft/pkg/cataloger/golang/cataloger.go index c7d483faa..d52c210c0 100644 --- a/syft/pkg/cataloger/golang/cataloger.go +++ b/syft/pkg/cataloger/golang/cataloger.go @@ -31,5 +31,5 @@ func NewGoModuleBinaryCataloger(opts CatalogerConfig) pkg.Cataloger { newGoBinaryCataloger(opts).parseGoBinary, mimetype.ExecutableMIMETypeSet.List()..., ). - WithProcessors(stdlibProcessor) + WithResolvingProcessors(stdlibProcessor) } diff --git a/syft/pkg/cataloger/golang/licenses.go b/syft/pkg/cataloger/golang/licenses.go index 48294378b..1c88da64f 100644 --- a/syft/pkg/cataloger/golang/licenses.go +++ b/syft/pkg/cataloger/golang/licenses.go @@ -26,25 +26,15 @@ import ( "github.com/anchore/syft/internal/licenses" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/license" "github.com/anchore/syft/syft/pkg" ) -type goLicense struct { - Value string `json:"val,omitempty"` - SPDXExpression string `json:"spdx,omitempty"` - Type license.Type `json:"type,omitempty"` - URLs []string `json:"urls,omitempty"` - Locations []string `json:"locations,omitempty"` - Contents string `json:"contents,omitempty"` -} - type goLicenseResolver struct { catalogerName string opts CatalogerConfig localModCacheDir fs.FS localVendorDir fs.FS - licenseCache cache.Resolver[[]goLicense] + licenseCache cache.Resolver[[]pkg.License] lowerLicenseFileNames *strset.Set } @@ -73,7 +63,7 @@ func newGoLicenseResolver(catalogerName string, opts CatalogerConfig) goLicenseR opts: opts, localModCacheDir: localModCacheDir, localVendorDir: localVendorDir, - licenseCache: cache.GetResolverCachingErrors[[]goLicense]("golang", "v1"), + licenseCache: cache.GetResolverCachingErrors[[]pkg.License]("golang", "v2"), lowerLicenseFileNames: strset.New(lowercaseLicenseFiles()...), } } @@ -97,52 +87,52 @@ func remotesForModule(proxies []string, noProxy []string, module string) []strin return proxies } -func (c *goLicenseResolver) getLicenses(ctx context.Context, scanner licenses.Scanner, resolver file.Resolver, moduleName, moduleVersion string) []pkg.License { +func (c *goLicenseResolver) getLicenses(ctx context.Context, resolver file.Resolver, moduleName, moduleVersion string) []pkg.License { // search the scan target first, ignoring local and remote sources - goLicenses, err := c.findLicensesInSource(ctx, scanner, resolver, + pkgLicenses, err := c.findLicensesInSource(ctx, resolver, fmt.Sprintf(`**/go/pkg/mod/%s@%s/*`, processCaps(moduleName), moduleVersion), ) if err != nil { log.WithFields("error", err, "module", moduleName, "version", moduleVersion).Trace("unable to read golang licenses from source") } - if len(goLicenses) > 0 { - return toPkgLicenses(goLicenses) + if len(pkgLicenses) > 0 { + return pkgLicenses } // look in the local host mod directory... if c.opts.SearchLocalModCacheLicenses { - goLicenses, err = c.getLicensesFromLocal(ctx, scanner, moduleName, moduleVersion) + pkgLicenses, err = c.getLicensesFromLocal(ctx, moduleName, moduleVersion) if err != nil { log.WithFields("error", err, "module", moduleName, "version", moduleVersion).Trace("unable to read golang licenses local") } - if len(goLicenses) > 0 { - return toPkgLicenses(goLicenses) + if len(pkgLicenses) > 0 { + return pkgLicenses } } // look in the local vendor directory... if c.opts.SearchLocalVendorLicenses { - goLicenses, err = c.getLicensesFromLocalVendor(ctx, scanner, moduleName) + pkgLicenses, err = c.getLicensesFromLocalVendor(ctx, moduleName) if err != nil { log.WithFields("error", err, "module", moduleName, "version", moduleVersion).Trace("unable to read golang licenses vendor") } - if len(goLicenses) > 0 { - return toPkgLicenses(goLicenses) + if len(pkgLicenses) > 0 { + return pkgLicenses } } // download from remote sources if c.opts.SearchRemoteLicenses { - goLicenses, err = c.getLicensesFromRemote(ctx, scanner, moduleName, moduleVersion) + pkgLicenses, err = c.getLicensesFromRemote(ctx, moduleName, moduleVersion) if err != nil { log.WithFields("error", err, "module", moduleName, "version", moduleVersion).Debug("unable to read golang licenses remote") } } - return toPkgLicenses(goLicenses) + return pkgLicenses } -func (c *goLicenseResolver) getLicensesFromLocal(ctx context.Context, scanner licenses.Scanner, moduleName, moduleVersion string) ([]goLicense, error) { +func (c *goLicenseResolver) getLicensesFromLocal(ctx context.Context, moduleName, moduleVersion string) ([]pkg.License, error) { if c.localModCacheDir == nil { return nil, nil } @@ -158,10 +148,10 @@ func (c *goLicenseResolver) getLicensesFromLocal(ctx context.Context, scanner li // if we're running against a directory on the filesystem, it may not include the // user's homedir / GOPATH, so we defer to using the localModCacheResolver // we use $GOPATH/pkg/mod to avoid leaking information about the user's system - return c.findLicensesInFS(ctx, scanner, "file://$GOPATH/pkg/mod/"+subdir+"/", dir) + return c.findLicensesInFS(ctx, "file://$GOPATH/pkg/mod/"+subdir+"/", dir) } -func (c *goLicenseResolver) getLicensesFromLocalVendor(ctx context.Context, scanner licenses.Scanner, moduleName string) ([]goLicense, error) { +func (c *goLicenseResolver) getLicensesFromLocalVendor(ctx context.Context, moduleName string) ([]pkg.License, error) { if c.localVendorDir == nil { return nil, nil } @@ -177,11 +167,11 @@ func (c *goLicenseResolver) getLicensesFromLocalVendor(ctx context.Context, scan // if we're running against a directory on the filesystem, it may not include the // user's homedir / GOPATH, so we defer to using the localModCacheResolver // we use $GOPATH/pkg/mod to avoid leaking information about the user's system - return c.findLicensesInFS(ctx, scanner, "file://$GO_VENDOR/"+subdir+"/", dir) + return c.findLicensesInFS(ctx, "file://$GO_VENDOR/"+subdir+"/", dir) } -func (c *goLicenseResolver) getLicensesFromRemote(ctx context.Context, scanner licenses.Scanner, moduleName, moduleVersion string) ([]goLicense, error) { - return c.licenseCache.Resolve(fmt.Sprintf("%s/%s", moduleName, moduleVersion), func() ([]goLicense, error) { +func (c *goLicenseResolver) getLicensesFromRemote(ctx context.Context, moduleName, moduleVersion string) ([]pkg.License, error) { + return c.licenseCache.Resolve(fmt.Sprintf("%s/%s", moduleName, moduleVersion), func() ([]pkg.License, error) { proxies := remotesForModule(c.opts.Proxies, c.opts.NoProxy, moduleName) urlPrefix, fsys, err := getModule(proxies, moduleName, moduleVersion) @@ -189,12 +179,12 @@ func (c *goLicenseResolver) getLicensesFromRemote(ctx context.Context, scanner l return nil, err } - return c.findLicensesInFS(ctx, scanner, urlPrefix, fsys) + return c.findLicensesInFS(ctx, urlPrefix, fsys) }) } -func (c *goLicenseResolver) findLicensesInFS(ctx context.Context, scanner licenses.Scanner, urlPrefix string, fsys fs.FS) ([]goLicense, error) { - var out []goLicense +func (c *goLicenseResolver) findLicensesInFS(ctx context.Context, urlPrefix string, fsys fs.FS) ([]pkg.License, error) { + var out []pkg.License err := fs.WalkDir(fsys, ".", func(filePath string, d fs.DirEntry, err error) error { if err != nil { log.Debugf("error reading %s#%s: %v", urlPrefix, filePath, err) @@ -213,18 +203,13 @@ func (c *goLicenseResolver) findLicensesInFS(ctx context.Context, scanner licens return nil } defer internal.CloseAndLogError(rdr, filePath) - - parsed, err := scanner.PkgSearch(ctx, file.NewLocationReadCloser(file.NewLocation(filePath), rdr)) - if err != nil { - log.Debugf("error parsing license file %s: %v", filePath, err) - return nil - } + licenses := pkg.NewLicensesFromReadCloserWithContext(ctx, file.NewLocationReadCloser(file.NewLocation(filePath), rdr)) // since these licenses are found in an external fs.FS, not in the scanned source, // get rid of the locations but keep information about the where the license was found // by prepending the urlPrefix to the internal path for an accurate representation - for _, l := range toGoLicenses(parsed) { + for _, l := range licenses { l.URLs = []string{urlPrefix + filePath} - l.Locations = nil + l.Locations = file.NewLocationSet() out = append(out, l) } return nil @@ -232,15 +217,15 @@ func (c *goLicenseResolver) findLicensesInFS(ctx context.Context, scanner licens return out, err } -func (c *goLicenseResolver) findLicensesInSource(ctx context.Context, scanner licenses.Scanner, resolver file.Resolver, globMatch string) ([]goLicense, error) { - var out []goLicense +func (c *goLicenseResolver) findLicensesInSource(ctx context.Context, resolver file.Resolver, globMatch string) ([]pkg.License, error) { + var out []pkg.License locations, err := resolver.FilesByGlob(globMatch) if err != nil { return nil, err } for _, l := range locations { - parsed, err := c.parseLicenseFromLocation(ctx, scanner, l, resolver) + parsed, err := c.parseLicenseFromLocation(ctx, l, resolver) if err != nil { return nil, err } @@ -258,8 +243,8 @@ func (c *goLicenseResolver) findLicensesInSource(ctx context.Context, scanner li return out, nil } -func (c *goLicenseResolver) parseLicenseFromLocation(ctx context.Context, scanner licenses.Scanner, l file.Location, resolver file.Resolver) ([]goLicense, error) { - var out []goLicense +func (c *goLicenseResolver) parseLicenseFromLocation(ctx context.Context, l file.Location, resolver file.Resolver) ([]pkg.License, error) { + var out []pkg.License fileName := path.Base(l.RealPath) if c.lowerLicenseFileNames.Has(strings.ToLower(fileName)) { contents, err := resolver.FileContentsByLocation(l) @@ -267,12 +252,7 @@ func (c *goLicenseResolver) parseLicenseFromLocation(ctx context.Context, scanne return nil, err } defer internal.CloseAndLogError(contents, l.RealPath) - parsed, err := scanner.PkgSearch(ctx, file.NewLocationReadCloser(l, contents)) - if err != nil { - return nil, err - } - - out = append(out, toGoLicenses(parsed)...) + out = pkg.NewLicensesFromReadCloserWithContext(ctx, file.NewLocationReadCloser(l, contents)) } return out, nil } @@ -281,13 +261,6 @@ func moduleDir(moduleName, moduleVersion string) string { return fmt.Sprintf("%s@%s", processCaps(moduleName), moduleVersion) } -func requireCollection[T any](licenses []T) []T { - if licenses == nil { - return make([]T, 0) - } - return licenses -} - var capReplacer = regexp.MustCompile("[A-Z]") func processCaps(s string) string { @@ -440,49 +413,3 @@ func (l noLicensesFound) Error() string { } var _ error = (*noLicensesFound)(nil) - -func toPkgLicenses(goLicenses []goLicense) []pkg.License { - var out []pkg.License - for _, l := range goLicenses { - out = append(out, pkg.License{ - Value: l.Value, - SPDXExpression: l.SPDXExpression, - Type: l.Type, - URLs: l.URLs, - Locations: toPkgLocations(l.Locations), - Contents: l.Contents, - }) - } - return requireCollection(out) -} - -func toPkgLocations(goLocations []string) file.LocationSet { - out := file.NewLocationSet() - for _, l := range goLocations { - out.Add(file.NewLocation(l)) - } - return out -} - -func toGoLicenses(pkgLicenses []pkg.License) []goLicense { - var out []goLicense - for _, l := range pkgLicenses { - out = append(out, goLicense{ - Value: l.Value, - SPDXExpression: l.SPDXExpression, - Type: l.Type, - URLs: l.URLs, - Locations: toGoLocations(l.Locations), - Contents: l.Contents, - }) - } - return out -} - -func toGoLocations(locations file.LocationSet) []string { - var out []string - for _, l := range locations.ToSlice() { - out = append(out, l.RealPath) - } - return out -} diff --git a/syft/pkg/cataloger/golang/licenses_test.go b/syft/pkg/cataloger/golang/licenses_test.go index 13a73ebee..c6da9284a 100644 --- a/syft/pkg/cataloger/golang/licenses_test.go +++ b/syft/pkg/cataloger/golang/licenses_test.go @@ -14,17 +14,18 @@ import ( "strings" "testing" - "github.com/google/licensecheck" "github.com/stretchr/testify/require" - "github.com/anchore/syft/internal/licenses" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/internal/fileresolver" "github.com/anchore/syft/syft/license" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" ) func Test_LicenseSearch(t *testing.T) { + ctx := pkgtest.Context() + loc1 := file.NewLocation("github.com/someorg/somename@v0.3.2/LICENSE") loc2 := file.NewLocation("github.com/!cap!o!r!g/!cap!project@v4.111.5/LICENSE.txt") loc3 := file.NewLocation("github.com/someorg/strangelicense@v1.2.3/LiCeNsE.tXt") @@ -71,13 +72,6 @@ func Test_LicenseSearch(t *testing.T) { localVendorDir := filepath.Join(wd, "test-fixtures", "licenses-vendor") - sc := &licenses.ScannerConfig{ - CoverageThreshold: 75, - Scanner: licensecheck.Scan, - } - licenseScanner, err := licenses.NewScanner(sc) - require.NoError(t, err) - tests := []struct { name string version string @@ -95,6 +89,7 @@ func Test_LicenseSearch(t *testing.T) { Value: "Apache-2.0", SPDXExpression: "Apache-2.0", Type: license.Concluded, + Contents: mustContentsFromLocation(t, loc1), URLs: []string{"file://$GOPATH/pkg/mod/" + loc1.RealPath}, Locations: file.NewLocationSet(), }}, @@ -110,6 +105,7 @@ func Test_LicenseSearch(t *testing.T) { Value: "MIT", SPDXExpression: "MIT", Type: license.Concluded, + Contents: mustContentsFromLocation(t, loc2, 23, 1105), URLs: []string{"file://$GOPATH/pkg/mod/" + loc2.RealPath}, Locations: file.NewLocationSet(), }}, @@ -125,6 +121,7 @@ func Test_LicenseSearch(t *testing.T) { Value: "Apache-2.0", SPDXExpression: "Apache-2.0", Type: license.Concluded, + Contents: mustContentsFromLocation(t, loc3), URLs: []string{"file://$GOPATH/pkg/mod/" + loc3.RealPath}, Locations: file.NewLocationSet(), }}, @@ -139,6 +136,7 @@ func Test_LicenseSearch(t *testing.T) { expected: []pkg.License{{ Value: "Apache-2.0", SPDXExpression: "Apache-2.0", + Contents: mustContentsFromLocation(t, loc1), Type: license.Concluded, URLs: []string{server.URL + "/github.com/someorg/somename/@v/v0.3.2.zip#" + loc1.RealPath}, Locations: file.NewLocationSet(), @@ -154,6 +152,7 @@ func Test_LicenseSearch(t *testing.T) { expected: []pkg.License{{ Value: "MIT", SPDXExpression: "MIT", + Contents: mustContentsFromLocation(t, loc2, 23, 1105), // offset for correct scanner contents Type: license.Concluded, URLs: []string{server.URL + "/github.com/CapORG/CapProject/@v/v4.111.5.zip#" + loc2.RealPath}, Locations: file.NewLocationSet(), @@ -171,6 +170,7 @@ func Test_LicenseSearch(t *testing.T) { expected: []pkg.License{{ Value: "MIT", SPDXExpression: "MIT", + Contents: mustContentsFromLocation(t, loc2, 23, 1105), // offset for correct scanner contents Type: license.Concluded, URLs: []string{server.URL + "/github.com/CapORG/CapProject/@v/v4.111.5.zip#" + loc2.RealPath}, Locations: file.NewLocationSet(), @@ -187,6 +187,7 @@ func Test_LicenseSearch(t *testing.T) { Value: "Apache-2.0", SPDXExpression: "Apache-2.0", Type: license.Concluded, + Contents: mustContentsFromLocation(t, loc1), URLs: []string{"file://$GO_VENDOR/github.com/someorg/somename/LICENSE"}, Locations: file.NewLocationSet(), }}, @@ -201,6 +202,7 @@ func Test_LicenseSearch(t *testing.T) { expected: []pkg.License{{ Value: "MIT", SPDXExpression: "MIT", + Contents: mustContentsFromLocation(t, loc2, 23, 1105), // offset for correct scanner contents Type: license.Concluded, URLs: []string{"file://$GO_VENDOR/github.com/!cap!o!r!g/!cap!project/LICENSE.txt"}, Locations: file.NewLocationSet(), @@ -216,6 +218,7 @@ func Test_LicenseSearch(t *testing.T) { expected: []pkg.License{{ Value: "Apache-2.0", SPDXExpression: "Apache-2.0", + Contents: mustContentsFromLocation(t, loc1), Type: license.Concluded, URLs: []string{"file://$GO_VENDOR/github.com/someorg/strangelicense/LiCeNsE.tXt"}, Locations: file.NewLocationSet(), @@ -226,7 +229,7 @@ func Test_LicenseSearch(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { l := newGoLicenseResolver("", test.config) - lics := l.getLicenses(context.Background(), licenseScanner, fileresolver.Empty{}, test.name, test.version) + lics := l.getLicenses(ctx, fileresolver.Empty{}, test.name, test.version) require.EqualValues(t, test.expected, lics) }) } @@ -301,10 +304,7 @@ func Test_findVersionPath(t *testing.T) { func Test_walkDirErrors(t *testing.T) { resolver := newGoLicenseResolver("", CatalogerConfig{}) - sc := &licenses.ScannerConfig{Scanner: licensecheck.Scan, CoverageThreshold: 75} - scanner, err := licenses.NewScanner(sc) - require.NoError(t, err) - _, err = resolver.findLicensesInFS(context.Background(), scanner, "somewhere", badFS{}) + _, err := resolver.findLicensesInFS(context.Background(), "somewhere", badFS{}) require.Error(t, err) } @@ -321,10 +321,7 @@ func Test_noLocalGoModDir(t *testing.T) { validTmp := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(validTmp, "mod@ver"), 0700|os.ModeDir)) - - sc := &licenses.ScannerConfig{Scanner: licensecheck.Scan, CoverageThreshold: 75} - licenseScanner, err := licenses.NewScanner(sc) - require.NoError(t, err) + ctx := pkgtest.Context() tests := []struct { name string dir string @@ -358,35 +355,29 @@ func Test_noLocalGoModDir(t *testing.T) { SearchLocalModCacheLicenses: true, LocalModCacheDir: test.dir, }) - _, err := resolver.getLicensesFromLocal(context.Background(), licenseScanner, "mod", "ver") + _, err := resolver.getLicensesFromLocal(ctx, "mod", "ver") test.wantErr(t, err) }) } } -func TestLicenseConversion(t *testing.T) { - inputLicenses := []pkg.License{ - { - Value: "Apache-2.0", - SPDXExpression: "Apache-2.0", - Type: "concluded", - URLs: nil, - Locations: file.NewLocationSet(file.NewLocation("LICENSE")), - Contents: "", - }, - { - Value: "UNKNOWN", - SPDXExpression: "UNKNOWN_4d1cffe420916f2b706300ab63fcafaf35226a0ad3725cb9f95b26036cefae32", - Type: "declared", - URLs: nil, - Locations: file.NewLocationSet(file.NewLocation("LICENSE2")), - Contents: "NVIDIA Software License Agreement and CUDA Supplement to Software License Agreement", - }, +func mustContentsFromLocation(t *testing.T, loc file.Location, offset ...int) string { + t.Helper() + + contentsPath := "test-fixtures/licenses/pkg/mod/" + loc.RealPath + contents, err := os.ReadFile(contentsPath) + require.NoErrorf(t, err, "could not open contents for fixture at %s", contentsPath) + + if len(offset) == 0 { + return string(contents) } - goLicenses := toGoLicenses(inputLicenses) + require.Equal(t, 2, len(offset), "invalid offset provided, expected two integers: start and end") - result := toPkgLicenses(goLicenses) + start, end := offset[0], offset[1] + require.GreaterOrEqual(t, start, 0, "offset start must be >= 0") + require.LessOrEqual(t, end, len(contents), "offset end must be <= content length") + require.LessOrEqual(t, start, end, "offset start must be <= end") - require.Equal(t, inputLicenses, result) + return string(contents[start:end]) } diff --git a/syft/pkg/cataloger/golang/parse_go_binary.go b/syft/pkg/cataloger/golang/parse_go_binary.go index 28be593d3..7a76df875 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary.go +++ b/syft/pkg/cataloger/golang/parse_go_binary.go @@ -18,7 +18,6 @@ import ( "golang.org/x/mod/module" "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/licenses" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" @@ -63,11 +62,6 @@ func newGoBinaryCataloger(opts CatalogerConfig) *goBinaryCataloger { func (c *goBinaryCataloger) parseGoBinary(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { var pkgs []pkg.Package - licenseScanner, err := licenses.ContextLicenseScanner(ctx) - if err != nil { - return nil, nil, err - } - unionReader, err := unionreader.GetUnionReader(reader.ReadCloser) if err != nil { return nil, nil, err @@ -79,7 +73,7 @@ func (c *goBinaryCataloger) parseGoBinary(ctx context.Context, resolver file.Res var rels []artifact.Relationship for _, mod := range mods { var depPkgs []pkg.Package - mainPkg, depPkgs := c.buildGoPkgInfo(ctx, licenseScanner, resolver, reader.Location, mod, mod.arch, unionReader) + mainPkg, depPkgs := c.buildGoPkgInfo(ctx, resolver, reader.Location, mod, mod.arch, unionReader) if mainPkg != nil { rels = createModuleRelationships(*mainPkg, depPkgs) pkgs = append(pkgs, *mainPkg) @@ -107,7 +101,7 @@ func createModuleRelationships(main pkg.Package, deps []pkg.Package) []artifact. var emptyModule debug.Module var moduleFromPartialPackageBuild = debug.Module{Path: "command-line-arguments"} -func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, licenseScanner licenses.Scanner, resolver file.Resolver, location file.Location, mod *extendedBuildInfo, arch string, reader io.ReadSeekCloser) (*pkg.Package, []pkg.Package) { +func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, resolver file.Resolver, location file.Location, mod *extendedBuildInfo, arch string, reader io.ReadSeekCloser) (*pkg.Package, []pkg.Package) { if mod == nil { return nil, nil } @@ -122,7 +116,7 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, licenseScanner l continue } - lics := c.licenseResolver.getLicenses(ctx, licenseScanner, resolver, dep.Path, dep.Version) + lics := c.licenseResolver.getLicenses(ctx, resolver, dep.Path, dep.Version) gover, experiments := getExperimentsFromVersion(mod.GoVersion) m := newBinaryMetadata( @@ -150,7 +144,7 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, licenseScanner l return nil, pkgs } - main := c.makeGoMainPackage(ctx, licenseScanner, resolver, mod, arch, location, reader) + main := c.makeGoMainPackage(ctx, resolver, mod, arch, location, reader) return &main, pkgs } @@ -165,9 +159,9 @@ func missingMainModule(mod *extendedBuildInfo) bool { return mod.Main == moduleFromPartialPackageBuild } -func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, licenseScanner licenses.Scanner, resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser) pkg.Package { +func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser) pkg.Package { gbs := getBuildSettings(mod.Settings) - lics := c.licenseResolver.getLicenses(ctx, licenseScanner, resolver, mod.Main.Path, mod.Main.Version) + lics := c.licenseResolver.getLicenses(ctx, resolver, mod.Main.Path, mod.Main.Version) gover, experiments := getExperimentsFromVersion(mod.GoVersion) m := newBinaryMetadata( diff --git a/syft/pkg/cataloger/golang/parse_go_binary_test.go b/syft/pkg/cataloger/golang/parse_go_binary_test.go index d1949d352..3ada19a8a 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary_test.go +++ b/syft/pkg/cataloger/golang/parse_go_binary_test.go @@ -15,11 +15,9 @@ import ( "syscall" "testing" - "github.com/google/licensecheck" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/anchore/syft/internal/licenses" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/internal/fileresolver" "github.com/anchore/syft/syft/internal/unionreader" @@ -170,10 +168,6 @@ func TestBuildGoPkgInfo(t *testing.T) { }, } - sc := &licenses.ScannerConfig{Scanner: licensecheck.Scan, CoverageThreshold: 75} - licenseScanner, err := licenses.NewScanner(sc) - require.NoError(t, err) - tests := []struct { name string mod *extendedBuildInfo @@ -1074,7 +1068,7 @@ func TestBuildGoPkgInfo(t *testing.T) { c := newGoBinaryCataloger(*test.cfg) reader, err := unionreader.GetUnionReader(io.NopCloser(strings.NewReader(test.binaryContent))) require.NoError(t, err) - mainPkg, pkgs := c.buildGoPkgInfo(context.Background(), licenseScanner, fileresolver.Empty{}, location, test.mod, test.mod.arch, reader) + mainPkg, pkgs := c.buildGoPkgInfo(context.Background(), fileresolver.Empty{}, location, test.mod, test.mod.arch, reader) if mainPkg != nil { pkgs = append(pkgs, *mainPkg) } diff --git a/syft/pkg/cataloger/golang/parse_go_mod.go b/syft/pkg/cataloger/golang/parse_go_mod.go index b145bce74..a0440580e 100644 --- a/syft/pkg/cataloger/golang/parse_go_mod.go +++ b/syft/pkg/cataloger/golang/parse_go_mod.go @@ -11,7 +11,6 @@ import ( "golang.org/x/mod/modfile" "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/licenses" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" @@ -35,11 +34,6 @@ func newGoModCataloger(opts CatalogerConfig) *goModCataloger { func (c *goModCataloger) parseGoModFile(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { packages := make(map[string]pkg.Package) - licenseScanner, err := licenses.ContextLicenseScanner(ctx) - if err != nil { - return nil, nil, fmt.Errorf("unable to create default license scanner: %w", err) - } - contents, err := io.ReadAll(reader) if err != nil { return nil, nil, fmt.Errorf("failed to read go module: %w", err) @@ -56,7 +50,7 @@ func (c *goModCataloger) parseGoModFile(ctx context.Context, resolver file.Resol } for _, m := range f.Require { - lics := c.licenseResolver.getLicenses(ctx, licenseScanner, resolver, m.Mod.Path, m.Mod.Version) + lics := c.licenseResolver.getLicenses(ctx, resolver, m.Mod.Path, m.Mod.Version) packages[m.Mod.Path] = pkg.Package{ Name: m.Mod.Path, Version: m.Mod.Version, @@ -73,7 +67,7 @@ func (c *goModCataloger) parseGoModFile(ctx context.Context, resolver file.Resol // remove any old packages and replace with new ones... for _, m := range f.Replace { - lics := c.licenseResolver.getLicenses(ctx, licenseScanner, resolver, m.New.Path, m.New.Version) + lics := c.licenseResolver.getLicenses(ctx, resolver, m.New.Path, m.New.Version) // the old path and new path may be the same, in which case this is a noop, // but if they're different we need to remove the old package. diff --git a/syft/pkg/cataloger/golang/stdlib_package.go b/syft/pkg/cataloger/golang/stdlib_package.go index 6e480e8c8..b1c7ea67a 100644 --- a/syft/pkg/cataloger/golang/stdlib_package.go +++ b/syft/pkg/cataloger/golang/stdlib_package.go @@ -1,6 +1,7 @@ package golang import ( + "context" "fmt" "strings" @@ -11,12 +12,12 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func stdlibProcessor(pkgs []pkg.Package, relationships []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) { - compilerPkgs, newRelationships := stdlibPackageAndRelationships(pkgs) +func stdlibProcessor(ctx context.Context, _ file.Resolver, pkgs []pkg.Package, relationships []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) { + compilerPkgs, newRelationships := stdlibPackageAndRelationships(ctx, pkgs) return append(pkgs, compilerPkgs...), append(relationships, newRelationships...), err } -func stdlibPackageAndRelationships(pkgs []pkg.Package) ([]pkg.Package, []artifact.Relationship) { +func stdlibPackageAndRelationships(ctx context.Context, pkgs []pkg.Package) ([]pkg.Package, []artifact.Relationship) { var goCompilerPkgs []pkg.Package var relationships []artifact.Relationship totalLocations := file.NewLocationSet() @@ -32,7 +33,7 @@ func stdlibPackageAndRelationships(pkgs []pkg.Package) ([]pkg.Package, []artifac continue } - stdLibPkg := newGoStdLib(mValue.GoCompiledVersion, goPkg.Locations) + stdLibPkg := newGoStdLib(ctx, mValue.GoCompiledVersion, goPkg.Locations) if stdLibPkg == nil { continue } @@ -49,7 +50,7 @@ func stdlibPackageAndRelationships(pkgs []pkg.Package) ([]pkg.Package, []artifac return goCompilerPkgs, relationships } -func newGoStdLib(version string, location file.LocationSet) *pkg.Package { +func newGoStdLib(ctx context.Context, version string, location file.LocationSet) *pkg.Package { stdlibCpe, err := generateStdlibCpe(version) if err != nil { return nil @@ -60,7 +61,7 @@ func newGoStdLib(version string, location file.LocationSet) *pkg.Package { PURL: packageURL("stdlib", strings.TrimPrefix(version, "go")), CPEs: []cpe.CPE{stdlibCpe}, Locations: location, - Licenses: pkg.NewLicenseSet(pkg.NewLicense("BSD-3-Clause")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "BSD-3-Clause")), Language: pkg.Go, Type: pkg.GoModulePkg, Metadata: pkg.GolangBinaryBuildinfoEntry{ diff --git a/syft/pkg/cataloger/golang/stdlib_package_test.go b/syft/pkg/cataloger/golang/stdlib_package_test.go index 0ced95b04..395f626cd 100644 --- a/syft/pkg/cataloger/golang/stdlib_package_test.go +++ b/syft/pkg/cataloger/golang/stdlib_package_test.go @@ -1,6 +1,7 @@ package golang import ( + "context" "testing" "github.com/google/go-cmp/cmp" @@ -15,7 +16,7 @@ import ( ) func Test_stdlibPackageAndRelationships(t *testing.T) { - + ctx := context.Background() tests := []struct { name string pkgs []pkg.Package @@ -87,7 +88,7 @@ func Test_stdlibPackageAndRelationships(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotPkgs, gotRels := stdlibPackageAndRelationships(tt.pkgs) + gotPkgs, gotRels := stdlibPackageAndRelationships(ctx, tt.pkgs) assert.Len(t, gotPkgs, tt.wantPkgs) assert.Len(t, gotRels, tt.wantRels) }) @@ -97,6 +98,7 @@ func Test_stdlibPackageAndRelationships(t *testing.T) { func Test_stdlibPackageAndRelationships_values(t *testing.T) { loc := file.NewLocation("/bin/my-app") locSet := file.NewLocationSet(loc) + ctx := context.TODO() p := pkg.Package{ Name: "github.com/something/go", Version: "1.0.0", @@ -114,7 +116,7 @@ func Test_stdlibPackageAndRelationships_values(t *testing.T) { PURL: packageURL("stdlib", "1.22.2"), Language: pkg.Go, Type: pkg.GoModulePkg, - Licenses: pkg.NewLicenseSet(pkg.NewLicense("BSD-3-Clause")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "BSD-3-Clause")), CPEs: []cpe.CPE{ { Attributes: cpe.MustAttributes("cpe:2.3:a:golang:go:1.22.2:-:*:*:*:*:*:*"), @@ -135,7 +137,7 @@ func Test_stdlibPackageAndRelationships_values(t *testing.T) { Type: artifact.DependencyOfRelationship, } - gotPkgs, gotRels := stdlibPackageAndRelationships([]pkg.Package{p}) + gotPkgs, gotRels := stdlibPackageAndRelationships(ctx, []pkg.Package{p}) require.Len(t, gotPkgs, 1) gotPkg := gotPkgs[0] diff --git a/syft/pkg/cataloger/java/archive_parser.go b/syft/pkg/cataloger/java/archive_parser.go index 8893d5008..d06b65872 100644 --- a/syft/pkg/cataloger/java/archive_parser.go +++ b/syft/pkg/cataloger/java/archive_parser.go @@ -280,7 +280,7 @@ func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package, func (j *archiveParser) discoverNameVersionLicense(ctx context.Context, manifest *pkg.JavaManifest) (string, string, []pkg.License, error) { // we use j.location because we want to associate the license declaration with where we discovered the contents in the manifest // TODO: when we support locations of paths within archives we should start passing the specific manifest location object instead of the top jar - lics := pkg.NewLicensesFromLocation(j.location, selectLicenses(manifest)...) + lics := pkg.NewLicensesFromLocationWithContext(ctx, j.location, selectLicenses(manifest)...) /* We should name and version from, in this order: 1. pom.properties if we find exactly 1 @@ -351,10 +351,10 @@ func (j *archiveParser) findLicenseFromJavaMetadata(ctx context.Context, groupID } } - return toPkgLicenses(&j.location, pomLicenses) + return toPkgLicenses(ctx, &j.location, pomLicenses) } -func toPkgLicenses(location *file.Location, licenses []maven.License) []pkg.License { +func toPkgLicenses(ctx context.Context, location *file.Location, licenses []maven.License) []pkg.License { var out []pkg.License for _, license := range licenses { name := "" @@ -365,10 +365,14 @@ func toPkgLicenses(location *file.Location, licenses []maven.License) []pkg.Lice if license.URL != nil { url = *license.URL } + // note: it is possible to: + // - have a license without a URL + // - have license and a URL + // - have a URL without a license (this is weird, but can happen) if name == "" && url == "" { continue } - out = append(out, pkg.NewLicenseFromFields(name, url, location)) + out = append(out, pkg.NewLicenseFromFieldsWithContext(ctx, name, url, location)) } return out } @@ -492,7 +496,7 @@ func getDigestsFromArchive(ctx context.Context, archivePath string) ([]file.Dige } func (j *archiveParser) getLicenseFromFileInArchive(ctx context.Context) ([]pkg.License, error) { - var fileLicenses []pkg.License + var out []pkg.License for _, filename := range licenses.FileNames() { licenseMatches := j.fileManifest.GlobMatch(true, "/META-INF/"+filename) if len(licenseMatches) == 0 { @@ -509,19 +513,15 @@ func (j *archiveParser) getLicenseFromFileInArchive(ctx context.Context) ([]pkg. for _, licenseMatch := range licenseMatches { licenseContents := contents[licenseMatch] r := strings.NewReader(licenseContents) - parsed, err := j.licenseScanner.PkgSearch(ctx, file.NewLocationReadCloser(j.location, io.NopCloser(r))) - if err != nil { - return nil, err - } - - if len(parsed) > 0 { - fileLicenses = append(fileLicenses, parsed...) + lics := pkg.NewLicensesFromReadCloserWithContext(ctx, file.NewLocationReadCloser(j.location, io.NopCloser(r))) + if len(lics) > 0 { + out = append(out, lics...) } } } } - return fileLicenses, nil + return out, nil } func (j *archiveParser) discoverPkgsFromNestedArchives(ctx context.Context, parentPkg *pkg.Package) ([]pkg.Package, []artifact.Relationship, error) { @@ -692,7 +692,7 @@ func newPackageFromMavenData(ctx context.Context, r *maven.Resolver, pomProperti log.WithFields("error", err, "mavenID", maven.NewID(pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version)).Trace("error attempting to resolve licenses") } - licenseSet := pkg.NewLicenseSet(toPkgLicenses(&location, pomLicenses)...) + licenseSet := pkg.NewLicenseSet(toPkgLicenses(ctx, &location, pomLicenses)...) p := pkg.Package{ Name: pomProperties.ArtifactID, diff --git a/syft/pkg/cataloger/java/archive_parser_test.go b/syft/pkg/cataloger/java/archive_parser_test.go index a4bfc605e..55a4a0c54 100644 --- a/syft/pkg/cataloger/java/archive_parser_test.go +++ b/syft/pkg/cataloger/java/archive_parser_test.go @@ -14,13 +14,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/google/licensecheck" "github.com/gookit/color" "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/anchore/syft/internal/licenses" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/license" @@ -32,10 +30,7 @@ import ( func TestSearchMavenForLicenses(t *testing.T) { url := maventest.MockRepo(t, "internal/maven/test-fixtures/maven-repo") - sc := &licenses.ScannerConfig{Scanner: licensecheck.Scan, CoverageThreshold: 75} - scanner, err := licenses.NewScanner(sc) - require.NoError(t, err) - ctx := licenses.SetContextLicenseScanner(context.Background(), scanner) + ctx := pkgtest.Context() tests := []struct { name string @@ -88,17 +83,13 @@ func TestSearchMavenForLicenses(t *testing.T) { // assert licenses are discovered from upstream _, _, _, parsedPom := ap.discoverMainPackageFromPomInfo(context.Background()) resolvedLicenses, _ := ap.maven.ResolveLicenses(context.Background(), parsedPom.project) - assert.Equal(t, tc.expectedLicenses, toPkgLicenses(nil, resolvedLicenses)) + assert.Equal(t, tc.expectedLicenses, toPkgLicenses(ctx, nil, resolvedLicenses)) }) } } func TestParseJar(t *testing.T) { - sc := &licenses.ScannerConfig{Scanner: licensecheck.Scan, CoverageThreshold: 75} - scanner, err := licenses.NewScanner(sc) - require.NoError(t, err) - ctx := licenses.SetContextLicenseScanner(context.Background(), scanner) - + ctx := pkgtest.Context() tests := []struct { name string fixture string @@ -121,7 +112,7 @@ func TestParseJar(t *testing.T) { Version: "1.0-SNAPSHOT", PURL: "pkg:maven/io.jenkins.plugins/example-jenkins-plugin@1.0-SNAPSHOT", Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT License", file.NewLocation("test-fixtures/java-builds/packages/example-jenkins-plugin.hpi")), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT License", file.NewLocation("test-fixtures/java-builds/packages/example-jenkins-plugin.hpi")), ), Language: pkg.Java, Type: pkg.JenkinsPluginPkg, @@ -207,14 +198,10 @@ func TestParseJar(t *testing.T) { Language: pkg.Java, Type: pkg.JavaPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromFields( - "Apache 2", - "http://www.apache.org/licenses/LICENSE-2.0.txt", - func() *file.Location { - l := file.NewLocation("test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar") - return &l - }(), - ), + pkg.NewLicenseFromFieldsWithContext(ctx, "Apache 2", "http://www.apache.org/licenses/LICENSE-2.0.txt", func() *file.Location { + l := file.NewLocation("test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar") + return &l + }()), ), Metadata: pkg.JavaArchive{ // ensure that nested packages with different names than that of the parent are appended as @@ -306,14 +293,10 @@ func TestParseJar(t *testing.T) { Version: "2.9.2", PURL: "pkg:maven/joda-time/joda-time@2.9.2", Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromFields( - "Apache 2", - "http://www.apache.org/licenses/LICENSE-2.0.txt", - func() *file.Location { - l := file.NewLocation("test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar") - return &l - }(), - ), + pkg.NewLicenseFromFieldsWithContext(ctx, "Apache 2", "http://www.apache.org/licenses/LICENSE-2.0.txt", func() *file.Location { + l := file.NewLocation("test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar") + return &l + }()), ), Language: pkg.Java, Type: pkg.JavaPkg, @@ -369,7 +352,7 @@ func TestParseJar(t *testing.T) { defer cleanupFn() require.NoError(t, err) - actual, _, err := parser.parse(context.Background(), nil) + actual, _, err := parser.parse(ctx, nil) if test.wantErr != nil { test.wantErr(t, err) } else { @@ -432,11 +415,18 @@ func TestParseJar(t *testing.T) { metadata.Manifest.Main = newMain } } - // write censored data back a.Metadata = metadata - pkgtest.AssertPackagesEqual(t, e, a) + // we can't use cmpopts.IgnoreFields for the license contents because of the set structure + // drop the license contents from the comparison + licenses := a.Licenses.ToSlice() + for i := range licenses { + licenses[i].Contents = "" + } + a.Licenses = pkg.NewLicenseSet(licenses...) + + pkgtest.AssertPackagesEqual(t, e, a, cmpopts.IgnoreFields(pkg.License{}, "Contents")) } }) } @@ -1102,6 +1092,7 @@ func Test_artifactIDMatchesFilename(t *testing.T) { } func Test_parseJavaArchive_regressions(t *testing.T) { + ctx := context.TODO() apiAll := pkg.Package{ Name: "api-all", Version: "2.0.0", @@ -1192,7 +1183,8 @@ func Test_parseJavaArchive_regressions(t *testing.T) { PURL: "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.15.2", Locations: file.NewLocationSet(file.NewLocation("test-fixtures/jar-metadata/cache/jackson-core-2.15.2.jar")), Licenses: pkg.NewLicenseSet( - pkg.NewLicensesFromLocation( + pkg.NewLicensesFromLocationWithContext( + ctx, file.NewLocation("test-fixtures/jar-metadata/cache/jackson-core-2.15.2.jar"), "https://www.apache.org/licenses/LICENSE-2.0.txt", )..., @@ -1246,7 +1238,8 @@ func Test_parseJavaArchive_regressions(t *testing.T) { PURL: "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.15.2", Locations: file.NewLocationSet(file.NewLocation("test-fixtures/jar-metadata/cache/com.fasterxml.jackson.core.jackson-core-2.15.2.jar")), Licenses: pkg.NewLicenseSet( - pkg.NewLicensesFromLocation( + pkg.NewLicensesFromLocationWithContext( + ctx, file.NewLocation("test-fixtures/jar-metadata/cache/com.fasterxml.jackson.core.jackson-core-2.15.2.jar"), "https://www.apache.org/licenses/LICENSE-2.0.txt", )..., @@ -1380,11 +1373,7 @@ func Test_parseJavaArchive_regressions(t *testing.T) { } func Test_deterministicMatchingPomProperties(t *testing.T) { - sc := &licenses.ScannerConfig{Scanner: licensecheck.Scan, CoverageThreshold: 75} - scanner, err := licenses.NewScanner(sc) - require.NoError(t, err) - ctx := licenses.SetContextLicenseScanner(context.Background(), scanner) - + ctx := pkgtest.Context() tests := []struct { fixture string expected maven.ID diff --git a/syft/pkg/cataloger/java/parse_pom_xml.go b/syft/pkg/cataloger/java/parse_pom_xml.go index 2fead9fe5..a427623a4 100644 --- a/syft/pkg/cataloger/java/parse_pom_xml.go +++ b/syft/pkg/cataloger/java/parse_pom_xml.go @@ -117,7 +117,7 @@ func newPackageFromMavenPom(ctx context.Context, r *maven.Resolver, pom *maven.P if err != nil { log.Tracef("error resolving licenses: %v", err) } - licenses := toPkgLicenses(&location, pomLicenses) + licenses := toPkgLicenses(ctx, &location, pomLicenses) m := pkg.JavaArchive{ PomProject: &pkg.JavaPomProject{ @@ -240,7 +240,7 @@ func newPackageFromDependency(ctx context.Context, r *maven.Resolver, pom *maven var pomProject *pkg.JavaPomProject if dependencyPom != nil { depLicenses, _ := r.ResolveLicenses(ctx, dependencyPom) - licenses = append(licenses, toPkgLicenses(nil, depLicenses)...) + licenses = append(licenses, toPkgLicenses(ctx, nil, depLicenses)...) pomProject = &pkg.JavaPomProject{ Parent: pomParent(ctx, r, dependencyPom), GroupID: id.GroupID, diff --git a/syft/pkg/cataloger/java/parse_pom_xml_test.go b/syft/pkg/cataloger/java/parse_pom_xml_test.go index 6bd0a5f23..26d4a4f40 100644 --- a/syft/pkg/cataloger/java/parse_pom_xml_test.go +++ b/syft/pkg/cataloger/java/parse_pom_xml_test.go @@ -194,6 +194,7 @@ func Test_parseCommonsTextPomXMLProject(t *testing.T) { func Test_parsePomXMLProject(t *testing.T) { // TODO: ideally we would have the path to the contained pom.xml, not the jar jarLocation := file.NewLocation("path/to/archive.jar") + ctx := context.TODO() tests := []struct { name string project *pkg.JavaPomProject @@ -270,7 +271,7 @@ func Test_parsePomXMLProject(t *testing.T) { licenses, err := r.ResolveLicenses(context.Background(), pom) //assert.NoError(t, err) - assert.Equal(t, test.licenses, toPkgLicenses(&jarLocation, licenses)) + assert.Equal(t, test.licenses, toPkgLicenses(ctx, &jarLocation, licenses)) }) } } diff --git a/syft/pkg/cataloger/javascript/cataloger_test.go b/syft/pkg/cataloger/javascript/cataloger_test.go index 0adf2b4fd..abe4feb31 100644 --- a/syft/pkg/cataloger/javascript/cataloger_test.go +++ b/syft/pkg/cataloger/javascript/cataloger_test.go @@ -1,6 +1,7 @@ package javascript import ( + "context" "testing" "github.com/anchore/syft/syft/file" @@ -9,6 +10,7 @@ import ( ) func Test_JavascriptCataloger(t *testing.T) { + ctx := context.TODO() locationSet := file.NewLocationSet(file.NewLocation("package-lock.json")) expectedPkgs := []pkg.Package{ { @@ -20,7 +22,7 @@ func Test_JavascriptCataloger(t *testing.T) { Language: pkg.JavaScript, Type: pkg.NpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", file.NewLocation("package-lock.json")), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("package-lock.json")), ), Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz", Integrity: "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw=="}, }, @@ -43,7 +45,7 @@ func Test_JavascriptCataloger(t *testing.T) { Language: pkg.JavaScript, Type: pkg.NpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", file.NewLocation("package-lock.json")), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("package-lock.json")), ), Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/cowsay/-/cowsay-1.4.0.tgz", Integrity: "sha512-rdg5k5PsHFVJheO/pmE3aDg2rUDDTfPJau6yYkZYlHFktUz+UxbE+IgnUAEyyCyv4noL5ltxXD0gZzmHPCy/9g=="}, }, diff --git a/syft/pkg/cataloger/javascript/package.go b/syft/pkg/cataloger/javascript/package.go index e6374e9f4..ed901fc80 100644 --- a/syft/pkg/cataloger/javascript/package.go +++ b/syft/pkg/cataloger/javascript/package.go @@ -1,6 +1,7 @@ package javascript import ( + "context" "encoding/json" "fmt" "io" @@ -17,13 +18,13 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func newPackageJSONPackage(u packageJSON, indexLocation file.Location) pkg.Package { +func newPackageJSONPackage(ctx context.Context, u packageJSON, indexLocation file.Location) pkg.Package { licenseCandidates, err := u.licensesFromJSON() if err != nil { log.Debugf("unable to extract licenses from javascript package.json: %+v", err) } - license := pkg.NewLicensesFromLocation(indexLocation, licenseCandidates...) + license := pkg.NewLicensesFromLocationWithContext(ctx, indexLocation, licenseCandidates...) p := pkg.Package{ Name: u.Name, Version: u.Version, @@ -48,7 +49,7 @@ func newPackageJSONPackage(u packageJSON, indexLocation file.Location) pkg.Packa return p } -func newPackageLockV1Package(cfg CatalogerConfig, resolver file.Resolver, location file.Location, name string, u lockDependency) pkg.Package { +func newPackageLockV1Package(ctx context.Context, cfg CatalogerConfig, resolver file.Resolver, location file.Location, name string, u lockDependency) pkg.Package { version := u.Version const aliasPrefixPackageLockV1 = "npm:" @@ -69,7 +70,7 @@ func newPackageLockV1Package(cfg CatalogerConfig, resolver file.Resolver, locati if cfg.SearchRemoteLicenses { license, err := getLicenseFromNpmRegistry(cfg.NPMBaseURL, name, version) if err == nil && license != "" { - licenses := pkg.NewLicensesFromValues(license) + licenses := pkg.NewLicensesFromValuesWithContext(ctx, license) licenseSet = pkg.NewLicenseSet(licenses...) } if err != nil { @@ -78,6 +79,7 @@ func newPackageLockV1Package(cfg CatalogerConfig, resolver file.Resolver, locati } return finalizeLockPkg( + ctx, resolver, location, pkg.Package{ @@ -93,15 +95,15 @@ func newPackageLockV1Package(cfg CatalogerConfig, resolver file.Resolver, locati ) } -func newPackageLockV2Package(cfg CatalogerConfig, resolver file.Resolver, location file.Location, name string, u lockPackage) pkg.Package { +func newPackageLockV2Package(ctx context.Context, cfg CatalogerConfig, resolver file.Resolver, location file.Location, name string, u lockPackage) pkg.Package { var licenseSet pkg.LicenseSet if u.License != nil { - licenseSet = pkg.NewLicenseSet(pkg.NewLicensesFromLocation(location, u.License...)...) + licenseSet = pkg.NewLicenseSet(pkg.NewLicensesFromLocationWithContext(ctx, location, u.License...)...) } else if cfg.SearchRemoteLicenses { license, err := getLicenseFromNpmRegistry(cfg.NPMBaseURL, name, u.Version) if err == nil && license != "" { - licenses := pkg.NewLicensesFromValues(license) + licenses := pkg.NewLicensesFromValuesWithContext(ctx, license) licenseSet = pkg.NewLicenseSet(licenses...) } if err != nil { @@ -110,6 +112,7 @@ func newPackageLockV2Package(cfg CatalogerConfig, resolver file.Resolver, locati } return finalizeLockPkg( + ctx, resolver, location, pkg.Package{ @@ -125,8 +128,9 @@ func newPackageLockV2Package(cfg CatalogerConfig, resolver file.Resolver, locati ) } -func newPnpmPackage(resolver file.Resolver, location file.Location, name, version string) pkg.Package { +func newPnpmPackage(ctx context.Context, resolver file.Resolver, location file.Location, name, version string) pkg.Package { return finalizeLockPkg( + ctx, resolver, location, pkg.Package{ @@ -140,13 +144,13 @@ func newPnpmPackage(resolver file.Resolver, location file.Location, name, versio ) } -func newYarnLockPackage(cfg CatalogerConfig, resolver file.Resolver, location file.Location, name, version string, resolved string, integrity string) pkg.Package { +func newYarnLockPackage(ctx context.Context, cfg CatalogerConfig, resolver file.Resolver, location file.Location, name, version string, resolved string, integrity string) pkg.Package { var licenseSet pkg.LicenseSet if cfg.SearchRemoteLicenses { license, err := getLicenseFromNpmRegistry(cfg.NPMBaseURL, name, version) if err == nil && license != "" { - licenses := pkg.NewLicensesFromValues(license) + licenses := pkg.NewLicensesFromValuesWithContext(ctx, license) licenseSet = pkg.NewLicenseSet(licenses...) } if err != nil { @@ -154,6 +158,7 @@ func newYarnLockPackage(cfg CatalogerConfig, resolver file.Resolver, location fi } } return finalizeLockPkg( + ctx, resolver, location, pkg.Package{ @@ -226,9 +231,9 @@ func getLicenseFromNpmRegistry(baseURL, packageName, version string) (string, er return license.License, nil } -func finalizeLockPkg(resolver file.Resolver, location file.Location, p pkg.Package) pkg.Package { +func finalizeLockPkg(ctx context.Context, resolver file.Resolver, location file.Location, p pkg.Package) pkg.Package { licenseCandidate := addLicenses(p.Name, resolver, location) - p.Licenses.Add(pkg.NewLicensesFromLocation(location, licenseCandidate...)...) + p.Licenses.Add(pkg.NewLicensesFromLocationWithContext(ctx, location, licenseCandidate...)...) p.SetID() return p } diff --git a/syft/pkg/cataloger/javascript/parse_package_json.go b/syft/pkg/cataloger/javascript/parse_package_json.go index 0c13d344a..5da2c5412 100644 --- a/syft/pkg/cataloger/javascript/parse_package_json.go +++ b/syft/pkg/cataloger/javascript/parse_package_json.go @@ -51,7 +51,7 @@ type repository struct { var authorPattern = regexp.MustCompile(`^\s*(?P[^<(]*)(\s+<(?P.*)>)?(\s\((?P.*)\))?\s*$`) // parsePackageJSON parses a package.json and returns the discovered JavaScript packages. -func parsePackageJSON(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parsePackageJSON(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { var pkgs []pkg.Package dec := json.NewDecoder(reader) @@ -67,7 +67,7 @@ func parsePackageJSON(_ context.Context, _ file.Resolver, _ *generic.Environment // a compliance filter later will remove these packages based on compliance rules pkgs = append( pkgs, - newPackageJSONPackage(p, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), + newPackageJSONPackage(ctx, p, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), ) } diff --git a/syft/pkg/cataloger/javascript/parse_package_json_test.go b/syft/pkg/cataloger/javascript/parse_package_json_test.go index ea0102d98..7764cde5f 100644 --- a/syft/pkg/cataloger/javascript/parse_package_json_test.go +++ b/syft/pkg/cataloger/javascript/parse_package_json_test.go @@ -1,6 +1,7 @@ package javascript import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -11,6 +12,7 @@ import ( ) func TestParsePackageJSON(t *testing.T) { + ctx := context.TODO() tests := []struct { Fixture string ExpectedPkg pkg.Package @@ -24,7 +26,7 @@ func TestParsePackageJSON(t *testing.T) { Type: pkg.NpmPkg, Language: pkg.JavaScript, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package.json")), + pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package.json")), ), Metadata: pkg.NpmPackage{ Name: "npm", @@ -45,7 +47,7 @@ func TestParsePackageJSON(t *testing.T) { Type: pkg.NpmPkg, Language: pkg.JavaScript, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("ISC", file.NewLocation("test-fixtures/pkg-json/package-license-object.json")), + pkg.NewLicenseFromLocationsWithContext(ctx, "ISC", file.NewLocation("test-fixtures/pkg-json/package-license-object.json")), ), Metadata: pkg.NpmPackage{ Name: "npm", @@ -65,8 +67,8 @@ func TestParsePackageJSON(t *testing.T) { PURL: "pkg:npm/npm@6.14.6", Type: pkg.NpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", file.NewLocation("test-fixtures/pkg-json/package-license-objects.json")), - pkg.NewLicenseFromLocations("Apache-2.0", file.NewLocation("test-fixtures/pkg-json/package-license-objects.json")), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("test-fixtures/pkg-json/package-license-objects.json")), + pkg.NewLicenseFromLocationsWithContext(ctx, "Apache-2.0", file.NewLocation("test-fixtures/pkg-json/package-license-objects.json")), ), Language: pkg.JavaScript, Metadata: pkg.NpmPackage{ @@ -123,7 +125,7 @@ func TestParsePackageJSON(t *testing.T) { PURL: "pkg:npm/npm@6.14.6", Type: pkg.NpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-nested-author.json")), + pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-nested-author.json")), ), Language: pkg.JavaScript, Metadata: pkg.NpmPackage{ @@ -144,7 +146,7 @@ func TestParsePackageJSON(t *testing.T) { PURL: "pkg:npm/function-bind@1.1.1", Type: pkg.NpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", file.NewLocation("test-fixtures/pkg-json/package-repo-string.json")), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("test-fixtures/pkg-json/package-repo-string.json")), ), Language: pkg.JavaScript, Metadata: pkg.NpmPackage{ @@ -165,7 +167,7 @@ func TestParsePackageJSON(t *testing.T) { PURL: "pkg:npm/npm@6.14.6", Type: pkg.NpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-private.json")), + pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-private.json")), ), Language: pkg.JavaScript, Metadata: pkg.NpmPackage{ @@ -187,7 +189,7 @@ func TestParsePackageJSON(t *testing.T) { PURL: "pkg:npm/npm@6.14.6", Type: pkg.NpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-author-non-standard.json")), + pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-author-non-standard.json")), ), Language: pkg.JavaScript, Metadata: pkg.NpmPackage{ diff --git a/syft/pkg/cataloger/javascript/parse_package_lock.go b/syft/pkg/cataloger/javascript/parse_package_lock.go index 003683b76..a76686801 100644 --- a/syft/pkg/cataloger/javascript/parse_package_lock.go +++ b/syft/pkg/cataloger/javascript/parse_package_lock.go @@ -55,7 +55,7 @@ func newGenericPackageLockAdapter(cfg CatalogerConfig) genericPackageLockAdapter } // parsePackageLock parses a package-lock.json and returns the discovered JavaScript packages. -func (a genericPackageLockAdapter) parsePackageLock(_ context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func (a genericPackageLockAdapter) parsePackageLock(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { // in the case we find package-lock.json files in the node_modules directories, skip those // as the whole purpose of the lock file is for the specific dependencies of the root project if pathContainsNodeModulesDirectory(reader.Path()) { @@ -81,7 +81,7 @@ func (a genericPackageLockAdapter) parsePackageLock(_ context.Context, resolver continue } - pkgs = append(pkgs, newPackageLockV1Package(a.cfg, resolver, reader.Location, name, pkgMeta)) + pkgs = append(pkgs, newPackageLockV1Package(ctx, a.cfg, resolver, reader.Location, name, pkgMeta)) } } @@ -106,7 +106,7 @@ func (a genericPackageLockAdapter) parsePackageLock(_ context.Context, resolver pkgs = append( pkgs, - newPackageLockV2Package(a.cfg, resolver, reader.Location, getNameFromPath(name), pkgMeta), + newPackageLockV2Package(ctx, a.cfg, resolver, reader.Location, getNameFromPath(name), pkgMeta), ) } } diff --git a/syft/pkg/cataloger/javascript/parse_package_lock_test.go b/syft/pkg/cataloger/javascript/parse_package_lock_test.go index b3ba527bb..6c7f770f5 100644 --- a/syft/pkg/cataloger/javascript/parse_package_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_package_lock_test.go @@ -1,6 +1,7 @@ package javascript import ( + "context" "testing" "github.com/anchore/syft/syft/artifact" @@ -111,6 +112,7 @@ func TestParsePackageLock(t *testing.T) { } func TestParsePackageLockV2(t *testing.T) { + ctx := context.TODO() fixture := "test-fixtures/pkg-lock/package-lock-2.json" var expectedRelationships []artifact.Relationship expectedPkgs := []pkg.Package{ @@ -129,7 +131,7 @@ func TestParsePackageLockV2(t *testing.T) { Language: pkg.JavaScript, Type: pkg.NpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", file.NewLocation(fixture)), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation(fixture)), ), Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", Integrity: "sha1-XxnSuFqY6VWANvajysyIGUIPBc8="}, }, @@ -140,7 +142,7 @@ func TestParsePackageLockV2(t *testing.T) { Language: pkg.JavaScript, Type: pkg.NpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", file.NewLocation(fixture)), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation(fixture)), ), Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/@types/react/-/react-18.0.17.tgz", Integrity: "sha1-RYPZwyLWfv5LOak10iPtzHBQzPQ="}, }, @@ -151,7 +153,7 @@ func TestParsePackageLockV2(t *testing.T) { Language: pkg.JavaScript, Type: pkg.NpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", file.NewLocation(fixture)), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation(fixture)), ), Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", Integrity: "sha1-GmL4lSVyPd4kuhsBsJK/XfitTTk="}, }, @@ -162,7 +164,7 @@ func TestParsePackageLockV2(t *testing.T) { Language: pkg.JavaScript, Type: pkg.NpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", file.NewLocation(fixture)), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation(fixture)), ), Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", Integrity: "sha1-TdysNxjXh8+d8NG30VAzklyPKfI="}, }, @@ -227,6 +229,7 @@ func TestParsePackageLockV3(t *testing.T) { } func TestParsePackageLockAlias(t *testing.T) { + ctx := context.TODO() var expectedRelationships []artifact.Relationship commonPkgs := []pkg.Package{ { @@ -266,7 +269,7 @@ func TestParsePackageLockAlias(t *testing.T) { Language: pkg.JavaScript, Type: pkg.NpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("ISC", file.NewLocation(packageLockV2)), + pkg.NewLicenseFromLocationsWithContext(ctx, "ISC", file.NewLocation(packageLockV2)), ), Metadata: pkg.NpmPackageLockEntry{}, } @@ -288,6 +291,7 @@ func TestParsePackageLockAlias(t *testing.T) { } func TestParsePackageLockLicenseWithArray(t *testing.T) { + ctx := context.TODO() fixture := "test-fixtures/pkg-lock/array-license-package-lock.json" var expectedRelationships []artifact.Relationship expectedPkgs := []pkg.Package{ @@ -297,7 +301,7 @@ func TestParsePackageLockLicenseWithArray(t *testing.T) { Language: pkg.JavaScript, Type: pkg.NpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("ISC", file.NewLocation(fixture)), + pkg.NewLicenseFromLocationsWithContext(ctx, "ISC", file.NewLocation(fixture)), ), PURL: "pkg:npm/tmp@1.0.0", Metadata: pkg.NpmPackageLockEntry{}, @@ -309,8 +313,8 @@ func TestParsePackageLockLicenseWithArray(t *testing.T) { Type: pkg.NpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", file.NewLocation(fixture)), - pkg.NewLicenseFromLocations("Apache2", file.NewLocation(fixture)), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation(fixture)), + pkg.NewLicenseFromLocationsWithContext(ctx, "Apache2", file.NewLocation(fixture)), ), PURL: "pkg:npm/pause-stream@0.0.11", Metadata: pkg.NpmPackageLockEntry{}, @@ -321,7 +325,7 @@ func TestParsePackageLockLicenseWithArray(t *testing.T) { Language: pkg.JavaScript, Type: pkg.NpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", file.NewLocation(fixture)), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation(fixture)), ), PURL: "pkg:npm/through@2.3.8", Metadata: pkg.NpmPackageLockEntry{}, diff --git a/syft/pkg/cataloger/javascript/parse_pnpm_lock.go b/syft/pkg/cataloger/javascript/parse_pnpm_lock.go index 8c7257a38..82df0f1af 100644 --- a/syft/pkg/cataloger/javascript/parse_pnpm_lock.go +++ b/syft/pkg/cataloger/javascript/parse_pnpm_lock.go @@ -27,7 +27,7 @@ type pnpmLockYaml struct { Packages map[string]interface{} `json:"packages" yaml:"packages"` } -func parsePnpmLock(_ context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parsePnpmLock(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { bytes, err := io.ReadAll(reader) if err != nil { return nil, nil, fmt.Errorf("failed to load pnpm-lock.yaml file: %w", err) @@ -66,7 +66,7 @@ func parsePnpmLock(_ context.Context, resolver file.Resolver, _ *generic.Environ continue } - pkgs = append(pkgs, newPnpmPackage(resolver, reader.Location, name, version)) + pkgs = append(pkgs, newPnpmPackage(ctx, resolver, reader.Location, name, version)) } packageNameRegex := regexp.MustCompile(`^/?([^(]*)(?:\(.*\))*$`) @@ -90,7 +90,7 @@ func parsePnpmLock(_ context.Context, resolver file.Resolver, _ *generic.Environ continue } - pkgs = append(pkgs, newPnpmPackage(resolver, reader.Location, name, version)) + pkgs = append(pkgs, newPnpmPackage(ctx, resolver, reader.Location, name, version)) } pkg.Sort(pkgs) diff --git a/syft/pkg/cataloger/javascript/parse_yarn_lock.go b/syft/pkg/cataloger/javascript/parse_yarn_lock.go index 94aa430a4..88294309d 100644 --- a/syft/pkg/cataloger/javascript/parse_yarn_lock.go +++ b/syft/pkg/cataloger/javascript/parse_yarn_lock.go @@ -59,7 +59,7 @@ func newGenericYarnLockAdapter(cfg CatalogerConfig) genericYarnLockAdapter { } } -func (a genericYarnLockAdapter) parseYarnLock(_ context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func (a genericYarnLockAdapter) parseYarnLock(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { // in the case we find yarn.lock files in the node_modules directories, skip those // as the whole purpose of the lock file is for the specific dependencies of the project if pathContainsNodeModulesDirectory(reader.Path()) { @@ -78,7 +78,7 @@ func (a genericYarnLockAdapter) parseYarnLock(_ context.Context, resolver file.R if packageName := findPackageName(line); packageName != "" { // When we find a new package, check if we have unsaved identifiers if currentPackage != "" && currentVersion != "" && !parsedPackages.Has(currentPackage+"@"+currentVersion) { - pkgs = append(pkgs, newYarnLockPackage(a.cfg, resolver, reader.Location, currentPackage, currentVersion, currentResolved, currentIntegrity)) + pkgs = append(pkgs, newYarnLockPackage(ctx, a.cfg, resolver, reader.Location, currentPackage, currentVersion, currentResolved, currentIntegrity)) parsedPackages.Add(currentPackage + "@" + currentVersion) } @@ -90,7 +90,7 @@ func (a genericYarnLockAdapter) parseYarnLock(_ context.Context, resolver file.R currentPackage = packageName currentVersion = version } else if integrity := findIntegrity(line); integrity != "" && !parsedPackages.Has(currentPackage+"@"+currentVersion) { - pkgs = append(pkgs, newYarnLockPackage(a.cfg, resolver, reader.Location, currentPackage, currentVersion, currentResolved, integrity)) + pkgs = append(pkgs, newYarnLockPackage(ctx, a.cfg, resolver, reader.Location, currentPackage, currentVersion, currentResolved, integrity)) parsedPackages.Add(currentPackage + "@" + currentVersion) // Cleanup to indicate no unsaved identifiers @@ -103,7 +103,7 @@ func (a genericYarnLockAdapter) parseYarnLock(_ context.Context, resolver file.R // check if we have valid unsaved data after end-of-file has reached if currentPackage != "" && currentVersion != "" && !parsedPackages.Has(currentPackage+"@"+currentVersion) { - pkgs = append(pkgs, newYarnLockPackage(a.cfg, resolver, reader.Location, currentPackage, currentVersion, currentResolved, currentIntegrity)) + pkgs = append(pkgs, newYarnLockPackage(ctx, a.cfg, resolver, reader.Location, currentPackage, currentVersion, currentResolved, currentIntegrity)) parsedPackages.Add(currentPackage + "@" + currentVersion) } diff --git a/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go b/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go index 20fac12fe..f85a2cf3a 100644 --- a/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go @@ -1,6 +1,7 @@ package javascript import ( + "context" "io" "net/http" "net/http/httptest" @@ -235,6 +236,7 @@ type handlerPath struct { } func TestSearchYarnForLicenses(t *testing.T) { + ctx := context.TODO() fixture := "test-fixtures/yarn-remote/yarn.lock" locations := file.NewLocationSet(file.NewLocation(fixture)) mux, url, teardown := setup() @@ -262,7 +264,7 @@ func TestSearchYarnForLicenses(t *testing.T) { Version: "7.10.4", Locations: locations, PURL: "pkg:npm/%40babel/code-frame@7.10.4", - Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "MIT")), Language: pkg.JavaScript, Type: pkg.NpmPkg, Metadata: pkg.YarnLockEntry{ diff --git a/syft/pkg/cataloger/kernel/cataloger_test.go b/syft/pkg/cataloger/kernel/cataloger_test.go index a440c7981..9247fce74 100644 --- a/syft/pkg/cataloger/kernel/cataloger_test.go +++ b/syft/pkg/cataloger/kernel/cataloger_test.go @@ -1,6 +1,7 @@ package kernel import ( + "context" "testing" "github.com/anchore/syft/syft/artifact" @@ -11,6 +12,7 @@ import ( ) func Test_KernelCataloger(t *testing.T) { + ctx := context.TODO() kernelPkg := pkg.Package{ Name: "linux-kernel", Version: "6.0.7-301.fc37.x86_64", @@ -49,7 +51,7 @@ func Test_KernelCataloger(t *testing.T) { ), ), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("GPL v2", + pkg.NewLicenseFromLocationsWithContext(ctx, "GPL v2", file.NewVirtualLocation( "/lib/modules/6.0.7-301.fc37.x86_64/kernel/drivers/tty/ttynull.ko", "/lib/modules/6.0.7-301.fc37.x86_64/kernel/drivers/tty/ttynull.ko", diff --git a/syft/pkg/cataloger/kernel/package.go b/syft/pkg/cataloger/kernel/package.go index 28e9d6a99..f694a5d2e 100644 --- a/syft/pkg/cataloger/kernel/package.go +++ b/syft/pkg/cataloger/kernel/package.go @@ -1,6 +1,7 @@ package kernel import ( + "context" "strings" "github.com/anchore/packageurl-go" @@ -40,12 +41,12 @@ func newLinuxKernelPackage(metadata pkg.LinuxKernel, archiveLocation file.Locati return p } -func newLinuxKernelModulePackage(metadata pkg.LinuxKernelModule, kmLocation file.Location) pkg.Package { +func newLinuxKernelModulePackage(ctx context.Context, metadata pkg.LinuxKernelModule, kmLocation file.Location) pkg.Package { p := pkg.Package{ Name: metadata.Name, Version: metadata.Version, Locations: file.NewLocationSet(kmLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), - Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(kmLocation, metadata.License)...), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocationWithContext(ctx, kmLocation, metadata.License)...), PURL: packageURL(metadata.Name, metadata.Version), Type: pkg.LinuxKernelModulePkg, Metadata: metadata, diff --git a/syft/pkg/cataloger/kernel/parse_linux_kernel_module_file.go b/syft/pkg/cataloger/kernel/parse_linux_kernel_module_file.go index 1106bb942..3e3e061e8 100644 --- a/syft/pkg/cataloger/kernel/parse_linux_kernel_module_file.go +++ b/syft/pkg/cataloger/kernel/parse_linux_kernel_module_file.go @@ -15,7 +15,7 @@ import ( const modinfoName = ".modinfo" -func parseLinuxKernelModuleFile(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parseLinuxKernelModuleFile(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { unionReader, err := unionreader.GetUnionReader(reader) if err != nil { return nil, nil, fmt.Errorf("unable to get union reader for file: %w", err) @@ -32,6 +32,7 @@ func parseLinuxKernelModuleFile(_ context.Context, _ file.Resolver, _ *generic.E return []pkg.Package{ newLinuxKernelModulePackage( + ctx, *metadata, reader.Location, ), diff --git a/syft/pkg/cataloger/lua/package.go b/syft/pkg/cataloger/lua/package.go index c9eaa4da9..13012b813 100644 --- a/syft/pkg/cataloger/lua/package.go +++ b/syft/pkg/cataloger/lua/package.go @@ -1,13 +1,15 @@ package lua import ( + "context" + "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" ) -func newLuaRocksPackage(u luaRocksPackage, indexLocation file.Location) pkg.Package { - license := pkg.NewLicensesFromLocation(indexLocation, u.License) +func newLuaRocksPackage(ctx context.Context, u luaRocksPackage, indexLocation file.Location) pkg.Package { + license := pkg.NewLicensesFromLocationWithContext(ctx, indexLocation, u.License) p := pkg.Package{ Name: u.Name, Version: u.Version, diff --git a/syft/pkg/cataloger/lua/parse_rockspec.go b/syft/pkg/cataloger/lua/parse_rockspec.go index 67c4310fe..51acbdb8f 100644 --- a/syft/pkg/cataloger/lua/parse_rockspec.go +++ b/syft/pkg/cataloger/lua/parse_rockspec.go @@ -27,7 +27,7 @@ type repository struct { } // parseRockspec parses a package.rockspec and returns the discovered Lua packages. -func parseRockspec(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parseRockspec(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { doc, err := parseRockspecData(reader) if err != nil { log.WithFields("error", err).Trace("unable to parse Rockspec app") @@ -64,6 +64,7 @@ func parseRockspec(_ context.Context, _ file.Resolver, _ *generic.Environment, r } p := newLuaRocksPackage( + ctx, luaRocksPackage{ Name: name, Version: version, diff --git a/syft/pkg/cataloger/lua/parse_rockspec_test.go b/syft/pkg/cataloger/lua/parse_rockspec_test.go index a85cda58c..e2ae3b48e 100644 --- a/syft/pkg/cataloger/lua/parse_rockspec_test.go +++ b/syft/pkg/cataloger/lua/parse_rockspec_test.go @@ -1,6 +1,7 @@ package lua import ( + "context" "testing" "github.com/anchore/syft/syft/file" @@ -9,6 +10,7 @@ import ( ) func TestParseRockspec(t *testing.T) { + ctx := context.TODO() tests := []struct { Fixture string ExpectedPkg pkg.Package @@ -22,7 +24,7 @@ func TestParseRockspec(t *testing.T) { Type: pkg.LuaRocksPkg, Language: pkg.Lua, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("Apache-2.0", file.NewLocation("test-fixtures/rockspec/kong-3.7.0-0.rockspec")), + pkg.NewLicenseFromLocationsWithContext(ctx, "Apache-2.0", file.NewLocation("test-fixtures/rockspec/kong-3.7.0-0.rockspec")), ), Metadata: pkg.LuaRocksPackage{ Name: "kong", @@ -43,7 +45,7 @@ func TestParseRockspec(t *testing.T) { Type: pkg.LuaRocksPkg, Language: pkg.Lua, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT/X11", file.NewLocation("test-fixtures/rockspec/lpeg-1.0.2-1.rockspec")), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT/X11", file.NewLocation("test-fixtures/rockspec/lpeg-1.0.2-1.rockspec")), ), Metadata: pkg.LuaRocksPackage{ Name: "LPeg", @@ -64,7 +66,7 @@ func TestParseRockspec(t *testing.T) { Type: pkg.LuaRocksPkg, Language: pkg.Lua, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", file.NewLocation("test-fixtures/rockspec/kong-pgmoon-1.16.2-1.rockspec")), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("test-fixtures/rockspec/kong-pgmoon-1.16.2-1.rockspec")), ), Metadata: pkg.LuaRocksPackage{ Name: "kong-pgmoon", @@ -85,7 +87,7 @@ func TestParseRockspec(t *testing.T) { Type: pkg.LuaRocksPkg, Language: pkg.Lua, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT/X11", file.NewLocation("test-fixtures/rockspec/luasyslog-2.0.1-1.rockspec")), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT/X11", file.NewLocation("test-fixtures/rockspec/luasyslog-2.0.1-1.rockspec")), ), Metadata: pkg.LuaRocksPackage{ Name: "luasyslog", diff --git a/syft/pkg/cataloger/ocaml/package.go b/syft/pkg/cataloger/ocaml/package.go index 5144add41..b26432d10 100644 --- a/syft/pkg/cataloger/ocaml/package.go +++ b/syft/pkg/cataloger/ocaml/package.go @@ -1,16 +1,18 @@ package ocaml import ( + "context" + "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" ) -func newOpamPackage(m pkg.OpamPackage, fileLocation file.Location) pkg.Package { +func newOpamPackage(ctx context.Context, m pkg.OpamPackage, fileLocation file.Location) pkg.Package { p := pkg.Package{ Name: m.Name, Version: m.Version, - Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(fileLocation, m.Licenses...)...), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocationWithContext(ctx, fileLocation, m.Licenses...)...), PURL: opamPackageURL(m.Name, m.Version), Locations: file.NewLocationSet(fileLocation), Type: pkg.OpamPkg, diff --git a/syft/pkg/cataloger/ocaml/parse_opam.go b/syft/pkg/cataloger/ocaml/parse_opam.go index bb5f2084a..fdad51e9f 100644 --- a/syft/pkg/cataloger/ocaml/parse_opam.go +++ b/syft/pkg/cataloger/ocaml/parse_opam.go @@ -16,7 +16,7 @@ import ( ) //nolint:funlen -func parseOpamPackage(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parseOpamPackage(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { var pkgs []pkg.Package opamVersionRe := regexp.MustCompile(`(?m)opam-version:\s*"[0-9]+\.[0-9]+"`) @@ -94,6 +94,7 @@ func parseOpamPackage(_ context.Context, _ file.Resolver, _ *generic.Environment pkgs = append( pkgs, newOpamPackage( + ctx, entry, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), ), diff --git a/syft/pkg/cataloger/ocaml/parse_opam_test.go b/syft/pkg/cataloger/ocaml/parse_opam_test.go index 63b5fc9d3..4557aa25e 100644 --- a/syft/pkg/cataloger/ocaml/parse_opam_test.go +++ b/syft/pkg/cataloger/ocaml/parse_opam_test.go @@ -1,6 +1,7 @@ package ocaml import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -17,7 +18,7 @@ func TestParseOpamPackage(t *testing.T) { fixture2 := "test-fixtures/alcotest.opam" location2 := file.NewLocation(fixture2) - + ctx := context.TODO() tests := []struct { fixture string want []pkg.Package @@ -31,10 +32,7 @@ func TestParseOpamPackage(t *testing.T) { PURL: "pkg:opam/ocaml-base-compiler@4.14.0", Locations: file.NewLocationSet(location1), Licenses: pkg.NewLicenseSet( - pkg.NewLicensesFromLocation( - location1, - "LGPL-2.1-or-later WITH OCaml-LGPL-linking-exception", - )..., + pkg.NewLicensesFromLocationWithContext(ctx, location1, "LGPL-2.1-or-later WITH OCaml-LGPL-linking-exception")..., ), Language: pkg.OCaml, Type: pkg.OpamPkg, @@ -60,7 +58,8 @@ func TestParseOpamPackage(t *testing.T) { PURL: "pkg:opam/alcotest@1.5.0", Locations: file.NewLocationSet(location2), Licenses: pkg.NewLicenseSet( - pkg.NewLicensesFromLocation( + pkg.NewLicensesFromLocationWithContext( + ctx, location2, "ISC", )..., diff --git a/syft/pkg/cataloger/php/package.go b/syft/pkg/cataloger/php/package.go index de22ab5f3..0653828c1 100644 --- a/syft/pkg/cataloger/php/package.go +++ b/syft/pkg/cataloger/php/package.go @@ -1,6 +1,7 @@ package php import ( + "context" "strings" "github.com/anchore/packageurl-go" @@ -8,12 +9,12 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func newComposerLockPackage(pd parsedLockData, indexLocation file.Location) pkg.Package { +func newComposerLockPackage(ctx context.Context, pd parsedLockData, indexLocation file.Location) pkg.Package { p := pkg.Package{ Name: pd.Name, Version: pd.Version, Locations: file.NewLocationSet(indexLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), - Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(indexLocation, pd.License...)...), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocationWithContext(ctx, indexLocation, pd.License...)...), PURL: packageURLFromComposer(pd.Name, pd.Version), Language: pkg.PHP, Type: pkg.PhpComposerPkg, @@ -24,12 +25,12 @@ func newComposerLockPackage(pd parsedLockData, indexLocation file.Location) pkg. return p } -func newComposerInstalledPackage(pd parsedInstalledData, indexLocation file.Location) pkg.Package { +func newComposerInstalledPackage(ctx context.Context, pd parsedInstalledData, indexLocation file.Location) pkg.Package { p := pkg.Package{ Name: pd.Name, Version: pd.Version, Locations: file.NewLocationSet(indexLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), - Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(indexLocation, pd.License...)...), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocationWithContext(ctx, indexLocation, pd.License...)...), PURL: packageURLFromComposer(pd.Name, pd.Version), Language: pkg.PHP, Type: pkg.PhpComposerPkg, @@ -40,12 +41,12 @@ func newComposerInstalledPackage(pd parsedInstalledData, indexLocation file.Loca return p } -func newPearPackage(pd peclPearData, indexLocation file.Location) pkg.Package { +func newPearPackage(ctx context.Context, pd peclPearData, indexLocation file.Location) pkg.Package { p := pkg.Package{ Name: pd.Name, Version: pd.Version, Locations: file.NewLocationSet(indexLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), - Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(indexLocation, pd.License...)...), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocationWithContext(ctx, indexLocation, pd.License...)...), PURL: packageURLFromPear(pd.Name, pd.Channel, pd.Version), Language: pkg.PHP, Type: pkg.PhpPearPkg, @@ -56,12 +57,12 @@ func newPearPackage(pd peclPearData, indexLocation file.Location) pkg.Package { return p } -func newPeclPackage(pd peclPearData, indexLocation file.Location) pkg.Package { +func newPeclPackage(ctx context.Context, pd peclPearData, indexLocation file.Location) pkg.Package { p := pkg.Package{ Name: pd.Name, Version: pd.Version, Locations: file.NewLocationSet(indexLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), - Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(indexLocation, pd.License...)...), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocationWithContext(ctx, indexLocation, pd.License...)...), PURL: packageURLFromPear(pd.Name, pd.Channel, pd.Version), Language: pkg.PHP, Type: pkg.PhpPeclPkg, diff --git a/syft/pkg/cataloger/php/parse_composer_lock.go b/syft/pkg/cataloger/php/parse_composer_lock.go index 5348e6ffc..da743dfd3 100644 --- a/syft/pkg/cataloger/php/parse_composer_lock.go +++ b/syft/pkg/cataloger/php/parse_composer_lock.go @@ -27,7 +27,7 @@ type composerLock struct { } // parseComposerLock is a parser function for Composer.lock contents, returning "Default" php packages discovered. -func parseComposerLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parseComposerLock(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { pkgs := make([]pkg.Package, 0) dec := json.NewDecoder(reader) @@ -42,6 +42,7 @@ func parseComposerLock(_ context.Context, _ file.Resolver, _ *generic.Environmen pkgs = append( pkgs, newComposerLockPackage( + ctx, pd, reader.Location, ), diff --git a/syft/pkg/cataloger/php/parse_composer_lock_test.go b/syft/pkg/cataloger/php/parse_composer_lock_test.go index 1da97c42d..4f5759d78 100644 --- a/syft/pkg/cataloger/php/parse_composer_lock_test.go +++ b/syft/pkg/cataloger/php/parse_composer_lock_test.go @@ -1,6 +1,7 @@ package php import ( + "context" "testing" "github.com/anchore/syft/syft/artifact" @@ -10,6 +11,7 @@ import ( ) func TestParseComposerFileLock(t *testing.T) { + ctx := context.Background() var expectedRelationships []artifact.Relationship fixture := "test-fixtures/composer.lock" locations := file.NewLocationSet(file.NewLocation(fixture)) @@ -20,7 +22,7 @@ func TestParseComposerFileLock(t *testing.T) { PURL: "pkg:composer/adoy/fastcgi-client@1.0.2", Locations: locations, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", file.NewLocation(fixture)), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation(fixture)), ), Language: pkg.PHP, Type: pkg.PhpComposerPkg, @@ -60,7 +62,7 @@ func TestParseComposerFileLock(t *testing.T) { PURL: "pkg:composer/alcaeus/mongo-php-adapter@1.1.11", Language: pkg.PHP, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", file.NewLocation(fixture)), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation(fixture)), ), Type: pkg.PhpComposerPkg, Metadata: pkg.PhpComposerLockEntry{ diff --git a/syft/pkg/cataloger/php/parse_installed_json.go b/syft/pkg/cataloger/php/parse_installed_json.go index b7c7d9d32..3b6ccd429 100644 --- a/syft/pkg/cataloger/php/parse_installed_json.go +++ b/syft/pkg/cataloger/php/parse_installed_json.go @@ -48,7 +48,7 @@ func (w *installedJSONComposerV2) UnmarshalJSON(data []byte) error { } // parseInstalledJSON is a parser function for Composer.lock contents, returning "Default" php packages discovered. -func parseInstalledJSON(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parseInstalledJSON(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { var pkgs []pkg.Package dec := json.NewDecoder(reader) @@ -63,6 +63,7 @@ func parseInstalledJSON(_ context.Context, _ file.Resolver, _ *generic.Environme pkgs = append( pkgs, newComposerInstalledPackage( + ctx, pd, reader.Location, ), diff --git a/syft/pkg/cataloger/php/parse_installed_json_test.go b/syft/pkg/cataloger/php/parse_installed_json_test.go index 7e0c06c32..f5753b186 100644 --- a/syft/pkg/cataloger/php/parse_installed_json_test.go +++ b/syft/pkg/cataloger/php/parse_installed_json_test.go @@ -1,6 +1,7 @@ package php import ( + "context" "testing" "github.com/anchore/syft/syft/artifact" @@ -10,6 +11,7 @@ import ( ) func TestParseInstalledJsonComposerV1(t *testing.T) { + ctx := context.TODO() fixtures := []string{ "test-fixtures/vendor/composer_1/installed.json", "test-fixtures/vendor/composer_2/installed.json", @@ -24,7 +26,7 @@ func TestParseInstalledJsonComposerV1(t *testing.T) { Language: pkg.PHP, Type: pkg.PhpComposerPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicense("MIT"), + pkg.NewLicenseWithContext(ctx, "MIT"), ), Metadata: pkg.PhpComposerInstalledEntry{ Name: "asm89/stack-cors", @@ -73,7 +75,7 @@ func TestParseInstalledJsonComposerV1(t *testing.T) { Language: pkg.PHP, Type: pkg.PhpComposerPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicense("MIT"), + pkg.NewLicenseWithContext(ctx, "MIT"), ), Metadata: pkg.PhpComposerInstalledEntry{ Name: "behat/mink", diff --git a/syft/pkg/cataloger/php/parse_pecl_pear.go b/syft/pkg/cataloger/php/parse_pecl_pear.go index ac67a0865..b3580ef57 100644 --- a/syft/pkg/cataloger/php/parse_pecl_pear.go +++ b/syft/pkg/cataloger/php/parse_pecl_pear.go @@ -34,7 +34,7 @@ func (p *peclPearData) ToPecl() pkg.PhpPeclEntry { return pkg.PhpPeclEntry(p.ToPear()) } -func parsePecl(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parsePecl(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { m, err := parsePeclPearSerialized(reader) if err != nil { return nil, nil, err @@ -42,10 +42,10 @@ func parsePecl(_ context.Context, _ file.Resolver, _ *generic.Environment, reade if m == nil { return nil, nil, unknown.New(reader.Location, fmt.Errorf("no pecl package found")) } - return []pkg.Package{newPeclPackage(*m, reader.Location)}, nil, nil + return []pkg.Package{newPeclPackage(ctx, *m, reader.Location)}, nil, nil } -func parsePear(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parsePear(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { m, err := parsePeclPearSerialized(reader) if err != nil { return nil, nil, err @@ -53,7 +53,7 @@ func parsePear(_ context.Context, _ file.Resolver, _ *generic.Environment, reade if m == nil { return nil, nil, unknown.New(reader.Location, fmt.Errorf("no pear package found")) } - return []pkg.Package{newPearPackage(*m, reader.Location)}, nil, nil + return []pkg.Package{newPearPackage(ctx, *m, reader.Location)}, nil, nil } // parsePeclPearSerialized is a parser function for Pear metadata contents, returning "Default" php packages discovered. diff --git a/syft/pkg/cataloger/php/parse_pecl_pear_test.go b/syft/pkg/cataloger/php/parse_pecl_pear_test.go index c2a439bae..bce01dc8d 100644 --- a/syft/pkg/cataloger/php/parse_pecl_pear_test.go +++ b/syft/pkg/cataloger/php/parse_pecl_pear_test.go @@ -1,6 +1,7 @@ package php import ( + "context" "testing" "github.com/anchore/syft/syft/artifact" @@ -10,6 +11,7 @@ import ( ) func TestParsePear(t *testing.T) { + ctx := context.TODO() tests := []struct { name string fixture string @@ -26,7 +28,7 @@ func TestParsePear(t *testing.T) { PURL: "pkg:pear/pecl.php.net/memcached@3.2.0", Locations: file.NewLocationSet(file.NewLocation("test-fixtures/memcached-v6-format.reg")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("PHP License", file.NewLocation("test-fixtures/memcached-v6-format.reg")), + pkg.NewLicenseFromLocationsWithContext(ctx, "PHP License", file.NewLocation("test-fixtures/memcached-v6-format.reg")), ), Language: pkg.PHP, Type: pkg.PhpPearPkg, @@ -49,7 +51,7 @@ func TestParsePear(t *testing.T) { PURL: "pkg:pear/pecl.php.net/memcached@3.2.0", Locations: file.NewLocationSet(file.NewLocation("test-fixtures/memcached-v5-format.reg")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("PHP License", file.NewLocation("test-fixtures/memcached-v5-format.reg")), + pkg.NewLicenseFromLocationsWithContext(ctx, "PHP License", file.NewLocation("test-fixtures/memcached-v5-format.reg")), ), Language: pkg.PHP, Type: pkg.PhpPearPkg, @@ -71,6 +73,7 @@ func TestParsePear(t *testing.T) { } func TestParsePecl(t *testing.T) { + ctx := context.TODO() tests := []struct { name string fixture string @@ -87,7 +90,7 @@ func TestParsePecl(t *testing.T) { PURL: "pkg:pear/pecl.php.net/memcached@3.2.0", Locations: file.NewLocationSet(file.NewLocation("test-fixtures/memcached-v6-format.reg")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("PHP License", file.NewLocation("test-fixtures/memcached-v6-format.reg")), + pkg.NewLicenseFromLocationsWithContext(ctx, "PHP License", file.NewLocation("test-fixtures/memcached-v6-format.reg")), ), Language: pkg.PHP, Type: pkg.PhpPeclPkg, // important! @@ -110,7 +113,7 @@ func TestParsePecl(t *testing.T) { PURL: "pkg:pear/pecl.php.net/memcached@3.2.0", Locations: file.NewLocationSet(file.NewLocation("test-fixtures/memcached-v5-format.reg")), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("PHP License", file.NewLocation("test-fixtures/memcached-v5-format.reg")), + pkg.NewLicenseFromLocationsWithContext(ctx, "PHP License", file.NewLocation("test-fixtures/memcached-v5-format.reg")), ), Language: pkg.PHP, Type: pkg.PhpPeclPkg, // important! diff --git a/syft/pkg/cataloger/python/cataloger_test.go b/syft/pkg/cataloger/python/cataloger_test.go index 474042055..828dc63a4 100644 --- a/syft/pkg/cataloger/python/cataloger_test.go +++ b/syft/pkg/cataloger/python/cataloger_test.go @@ -1,7 +1,9 @@ package python import ( + "context" "fmt" + "os" "path" "testing" @@ -13,6 +15,7 @@ import ( ) func Test_PackageCataloger(t *testing.T) { + ctx := context.TODO() tests := []struct { name string fixture string @@ -55,7 +58,7 @@ func Test_PackageCataloger(t *testing.T) { Licenses: pkg.NewLicenseSet( // here we only used the license that was declared in the METADATA file, we did not go searching for other licenses // this is the better source of truth when there is no explicit LicenseFile given - pkg.NewLicenseFromLocations("BSD License", file.NewLocation("dist-name/dist-info/METADATA")), + pkg.NewLicenseFromLocationsWithContext(ctx, "BSD License", file.NewLocation("dist-name/dist-info/METADATA")), ), FoundBy: "python-installed-package-cataloger", Metadata: pkg.PythonPackage{ @@ -94,7 +97,7 @@ func Test_PackageCataloger(t *testing.T) { file.NewLocation("egg-name/egg-info/top_level.txt"), ), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("Apache 2.0", file.NewLocation("egg-name/egg-info/PKG-INFO")), + pkg.NewLicenseFromLocationsWithContext(ctx, "Apache 2.0", file.NewLocation("egg-name/egg-info/PKG-INFO")), ), FoundBy: "python-installed-package-cataloger", Metadata: pkg.PythonPackage{ @@ -138,7 +141,7 @@ func Test_PackageCataloger(t *testing.T) { Licenses: pkg.NewLicenseSet( // here we only used the license that was declared in the METADATA file, we did not go searching for other licenses // this is the better source of truth when there is no explicit LicenseFile given - pkg.NewLicenseFromLocations("BSD License", file.NewLocation("dist-name/DIST-INFO/METADATA")), + pkg.NewLicenseFromLocationsWithContext(ctx, "BSD License", file.NewLocation("dist-name/DIST-INFO/METADATA")), ), FoundBy: "python-installed-package-cataloger", Metadata: pkg.PythonPackage{ @@ -174,7 +177,7 @@ func Test_PackageCataloger(t *testing.T) { file.NewLocation("egg-name/EGG-INFO/top_level.txt"), ), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("Apache 2.0", file.NewLocation("egg-name/EGG-INFO/PKG-INFO")), + pkg.NewLicenseFromLocationsWithContext(ctx, "Apache 2.0", file.NewLocation("egg-name/EGG-INFO/PKG-INFO")), ), FoundBy: "python-installed-package-cataloger", Metadata: pkg.PythonPackage{ @@ -220,6 +223,7 @@ func Test_PackageCataloger(t *testing.T) { Value: "BSD-3-Clause", SPDXExpression: "BSD-3-Clause", Type: "concluded", + Contents: mustContentsFromLocation(t, "test-fixtures/site-packages/license/with-license-file-declared.dist-info/LICENSE.txt", 0, 1475), // we read the path from the LicenseFile field in the METADATA file, then read the license file directly Locations: file.NewLocationSet(file.NewLocation("with-license-file-declared.dist-info/LICENSE.txt")), }, @@ -266,8 +270,8 @@ func Test_PackageCataloger(t *testing.T) { Value: "BSD-3-Clause", SPDXExpression: "BSD-3-Clause", Type: "concluded", - // we discover license files automatically - Locations: file.NewLocationSet(file.NewLocation("without-license-file-declared.dist-info/LICENSE.txt")), + Contents: mustContentsFromLocation(t, "test-fixtures/site-packages/license/with-license-file-declared.dist-info/LICENSE.txt", 0, 1475), + Locations: file.NewLocationSet(file.NewLocation("without-license-file-declared.dist-info/LICENSE.txt")), }, ), FoundBy: "python-installed-package-cataloger", @@ -312,7 +316,7 @@ func Test_PackageCataloger(t *testing.T) { file.NewLocation("dist-info/RECORD"), ), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("BSD License", file.NewLocation("dist-info/METADATA")), + pkg.NewLicenseFromLocationsWithContext(ctx, "BSD License", file.NewLocation("dist-info/METADATA")), ), FoundBy: "python-installed-package-cataloger", Metadata: pkg.PythonPackage{ @@ -349,7 +353,7 @@ func Test_PackageCataloger(t *testing.T) { file.NewLocation("METADATA"), ), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("BSD License", file.NewLocation("METADATA")), + pkg.NewLicenseFromLocationsWithContext(ctx, "BSD License", file.NewLocation("METADATA")), ), FoundBy: "python-installed-package-cataloger", Metadata: pkg.PythonPackage{ @@ -378,7 +382,7 @@ func Test_PackageCataloger(t *testing.T) { file.NewLocation("test.egg-info"), ), Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("Apache 2.0", file.NewLocation("test.egg-info")), + pkg.NewLicenseFromLocationsWithContext(ctx, "Apache 2.0", file.NewLocation("test.egg-info")), ), FoundBy: "python-installed-package-cataloger", Metadata: pkg.PythonPackage{ @@ -398,10 +402,10 @@ func Test_PackageCataloger(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - pkgtest.NewCatalogTester(). + (pkgtest.NewCatalogTester(). FromDirectory(t, test.fixture). Expects(test.expectedPackages, nil). - TestCataloger(t, NewInstalledPackageCataloger()) + TestCataloger(t, NewInstalledPackageCataloger())) }) } } @@ -802,3 +806,26 @@ func stringPackage(p pkg.Package) string { return fmt.Sprintf("%s @ %s (%s)", p.Name, p.Version, loc) } + +func mustContentsFromLocation(t *testing.T, contentsPath string, offset ...int) string { + t.Helper() // Marks this function as a test helper for cleaner error reporting + contents, err := os.ReadFile(contentsPath) + if err != nil { + t.Fatalf("failed to read file %s: %v", contentsPath, err) + } + + if len(offset) == 0 { + return string(contents) + } + + if len(offset) != 2 { + t.Fatalf("invalid offset provided, expected two integers: start and end") + } + start, end := offset[0], offset[1] + + if start < 0 || end > len(contents) || start > end { + t.Fatalf("invalid offset range: start=%d, end=%d, content length=%d", start, end, len(contents)) + } + + return string(contents[start:end]) +} diff --git a/syft/pkg/cataloger/python/parse_wheel_egg.go b/syft/pkg/cataloger/python/parse_wheel_egg.go index e01139a79..e4da35b67 100644 --- a/syft/pkg/cataloger/python/parse_wheel_egg.go +++ b/syft/pkg/cataloger/python/parse_wheel_egg.go @@ -25,10 +25,6 @@ import ( // parseWheelOrEgg takes the primary metadata file reference and returns the python package it represents. Contained // fields are governed by the PyPA core metadata specification (https://packaging.python.org/en/latest/specifications/core-metadata/). func parseWheelOrEgg(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { - licenseScanner, err := licenses.ContextLicenseScanner(ctx) - if err != nil { - return nil, nil, err - } pd, sources, err := assembleEggOrWheelMetadata(resolver, reader.Location) if err != nil { return nil, nil, err @@ -46,7 +42,7 @@ func parseWheelOrEgg(ctx context.Context, resolver file.Resolver, _ *generic.Env pkgs := []pkg.Package{ newPackageForPackage( *pd, - findLicenses(ctx, licenseScanner, resolver, *pd), + findLicenses(ctx, resolver, *pd), sources..., ), } @@ -253,7 +249,7 @@ func assembleEggOrWheelMetadata(resolver file.Resolver, metadataLocation file.Lo return &pd, sources, nil } -func findLicenses(ctx context.Context, scanner licenses.Scanner, resolver file.Resolver, m parsedData) pkg.LicenseSet { +func findLicenses(ctx context.Context, resolver file.Resolver, m parsedData) pkg.LicenseSet { var licenseSet pkg.LicenseSet licenseLocations := file.NewLocationSet() @@ -268,9 +264,9 @@ func findLicenses(ctx context.Context, scanner licenses.Scanner, resolver file.R switch { case m.LicenseExpression != "" || m.Licenses != "": - licenseSet = getLicenseSetFromValues(licenseLocations.ToSlice(), m.LicenseExpression, m.Licenses) + licenseSet = getLicenseSetFromValues(ctx, licenseLocations.ToSlice(), m.LicenseExpression, m.Licenses) case !licenseLocations.Empty(): - licenseSet = getLicenseSetFromFiles(ctx, scanner, resolver, licenseLocations.ToSlice()...) + licenseSet = getLicenseSetFromFiles(ctx, resolver, licenseLocations.ToSlice()...) default: // search for known license paths from RECORDS file @@ -302,14 +298,14 @@ func findLicenses(ctx context.Context, scanner licenses.Scanner, resolver file.R locationSet.Add(locs...) } - licenseSet = getLicenseSetFromFiles(ctx, scanner, resolver, locationSet.ToSlice()...) + licenseSet = getLicenseSetFromFiles(ctx, resolver, locationSet.ToSlice()...) } return licenseSet } -func getLicenseSetFromValues(locations []file.Location, licenseValues ...string) pkg.LicenseSet { +func getLicenseSetFromValues(ctx context.Context, locations []file.Location, licenseValues ...string) pkg.LicenseSet { if len(locations) == 0 { - return pkg.NewLicenseSet(pkg.NewLicensesFromValues(licenseValues...)...) + return pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, licenseValues...)...) } licenseSet := pkg.NewLicenseSet() @@ -318,31 +314,25 @@ func getLicenseSetFromValues(locations []file.Location, licenseValues ...string) continue } - licenseSet.Add(pkg.NewLicenseFromLocations(value, locations...)) + licenseSet.Add(pkg.NewLicenseFromLocationsWithContext(ctx, value, locations...)) } return licenseSet } -func getLicenseSetFromFiles(ctx context.Context, scanner licenses.Scanner, resolver file.Resolver, locations ...file.Location) pkg.LicenseSet { +func getLicenseSetFromFiles(ctx context.Context, resolver file.Resolver, locations ...file.Location) pkg.LicenseSet { licenseSet := pkg.NewLicenseSet() for _, loc := range locations { - licenseSet.Add(getLicenseSetFromFile(ctx, scanner, resolver, loc)...) + licenseSet.Add(getLicenseSetFromFile(ctx, resolver, loc)...) } return licenseSet } -func getLicenseSetFromFile(ctx context.Context, scanner licenses.Scanner, resolver file.Resolver, location file.Location) []pkg.License { +func getLicenseSetFromFile(ctx context.Context, resolver file.Resolver, location file.Location) []pkg.License { metadataContents, err := resolver.FileContentsByLocation(location) if err != nil { log.WithFields("error", err, "path", location.Path()).Trace("unable to read file contents") return nil } defer internal.CloseAndLogError(metadataContents, location.Path()) - parsed, err := scanner.PkgSearch(ctx, file.NewLocationReadCloser(location, metadataContents)) - if err != nil { - log.WithFields("error", err, "path", location.Path()).Trace("unable to parse a license from the file") - return nil - } - - return parsed + return pkg.NewLicensesFromReadCloserWithContext(ctx, file.NewLocationReadCloser(location, metadataContents)) } diff --git a/syft/pkg/cataloger/r/cataloger_test.go b/syft/pkg/cataloger/r/cataloger_test.go index 18452cf88..b545859a6 100644 --- a/syft/pkg/cataloger/r/cataloger_test.go +++ b/syft/pkg/cataloger/r/cataloger_test.go @@ -1,6 +1,7 @@ package r import ( + "context" "testing" "github.com/anchore/syft/syft/artifact" @@ -10,13 +11,14 @@ import ( ) func TestRPackageCataloger(t *testing.T) { + ctx := context.Background() expectedPkgs := []pkg.Package{ { Name: "base", Version: "4.3.0", FoundBy: "r-package-cataloger", Locations: file.NewLocationSet(file.NewLocation("base/DESCRIPTION")), - Licenses: pkg.NewLicenseSet([]pkg.License{pkg.NewLicense("Part of R 4.3.0")}...), + Licenses: pkg.NewLicenseSet([]pkg.License{pkg.NewLicenseWithContext(ctx, "Part of R 4.3.0")}...), Language: pkg.R, Type: pkg.Rpkg, PURL: "pkg:cran/base@4.3.0", @@ -34,7 +36,7 @@ func TestRPackageCataloger(t *testing.T) { Version: "1.5.0.9000", FoundBy: "r-package-cataloger", Locations: file.NewLocationSet(file.NewLocation("stringr/DESCRIPTION")), - Licenses: pkg.NewLicenseSet([]pkg.License{pkg.NewLicense("MIT")}...), + Licenses: pkg.NewLicenseSet([]pkg.License{pkg.NewLicenseWithContext(ctx, "MIT")}...), Language: pkg.R, Type: pkg.Rpkg, PURL: "pkg:cran/stringr@1.5.0.9000", diff --git a/syft/pkg/cataloger/r/package.go b/syft/pkg/cataloger/r/package.go index e5ca3c486..36fed4ca3 100644 --- a/syft/pkg/cataloger/r/package.go +++ b/syft/pkg/cataloger/r/package.go @@ -1,6 +1,7 @@ package r import ( + "context" "strings" "github.com/anchore/packageurl-go" @@ -8,13 +9,13 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func newPackage(pd parseData, locations ...file.Location) pkg.Package { +func newPackage(ctx context.Context, pd parseData, locations ...file.Location) pkg.Package { locationSet := file.NewLocationSet() for _, loc := range locations { locationSet.Add(loc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) } - licenses := parseLicenseData(pd.License) + licenses := parseLicenseData(ctx, pd.License) result := pkg.Package{ Name: pd.Package, @@ -44,7 +45,7 @@ func packageURL(m parseData) string { // Multiple licences can be specified separated by ‘|’ // (surrounded by spaces) in which case the user can choose any of the above cases. // https://cran.rstudio.com/doc/manuals/r-devel/R-exts.html#Licensing -func parseLicenseData(license string, locations ...file.Location) []pkg.License { +func parseLicenseData(ctx context.Context, license string, locations ...file.Location) []pkg.License { licenses := make([]pkg.License, 0) // check if multiple licenses are separated by | @@ -56,7 +57,7 @@ func parseLicenseData(license string, locations ...file.Location) []pkg.License licenseVersion := strings.SplitN(l, " ", 2) if len(licenseVersion) == 2 { l = strings.Join([]string{licenseVersion[0], parseVersion(licenseVersion[1])}, "") - licenses = append(licenses, pkg.NewLicenseFromLocations(l, locations...)) + licenses = append(licenses, pkg.NewLicenseFromLocationsWithContext(ctx, l, locations...)) continue } } @@ -65,7 +66,7 @@ func parseLicenseData(license string, locations ...file.Location) []pkg.License if strings.Contains(l, "+") && strings.Contains(l, "LICENSE") { splitField := strings.Split(l, " ") if len(splitField) > 0 { - licenses = append(licenses, pkg.NewLicenseFromLocations(splitField[0], locations...)) + licenses = append(licenses, pkg.NewLicenseFromLocationsWithContext(ctx, splitField[0], locations...)) continue } } @@ -77,7 +78,7 @@ func parseLicenseData(license string, locations ...file.Location) []pkg.License // no specific case found for the above so assume case 2 // check if the common name in case 2 is valid SDPX otherwise value will be populated - licenses = append(licenses, pkg.NewLicenseFromLocations(l, locations...)) + licenses = append(licenses, pkg.NewLicenseFromLocationsWithContext(ctx, l, locations...)) continue } return licenses diff --git a/syft/pkg/cataloger/r/package_test.go b/syft/pkg/cataloger/r/package_test.go index 8eb06642c..e71158e04 100644 --- a/syft/pkg/cataloger/r/package_test.go +++ b/syft/pkg/cataloger/r/package_test.go @@ -1,12 +1,14 @@ package r import ( + "context" "testing" "github.com/anchore/syft/syft/pkg" ) func Test_NewPackageLicenses(t *testing.T) { + ctx := context.TODO() testCases := []struct { name string pd parseData @@ -20,7 +22,7 @@ func Test_NewPackageLicenses(t *testing.T) { License: "MIT", }, []pkg.License{ - pkg.NewLicense("MIT"), + pkg.NewLicenseWithContext(ctx, "MIT"), }, }, { @@ -31,7 +33,7 @@ func Test_NewPackageLicenses(t *testing.T) { License: "LGPL (== 2.0)", }, []pkg.License{ - pkg.NewLicense("LGPL2.0"), + pkg.NewLicenseWithContext(ctx, "LGPL2.0"), }, }, { @@ -42,7 +44,7 @@ func Test_NewPackageLicenses(t *testing.T) { License: "LGPL (>= 2.0, < 3)", }, []pkg.License{ - pkg.NewLicense("LGPL2.0+"), + pkg.NewLicenseWithContext(ctx, "LGPL2.0+"), }, }, { @@ -53,7 +55,7 @@ func Test_NewPackageLicenses(t *testing.T) { License: "GPL-2 + file LICENSE", }, []pkg.License{ - pkg.NewLicense("GPL-2"), + pkg.NewLicenseWithContext(ctx, "GPL-2"), }, }, { @@ -64,7 +66,7 @@ func Test_NewPackageLicenses(t *testing.T) { License: "Mozilla Public License", }, []pkg.License{ - pkg.NewLicense("Mozilla Public License"), + pkg.NewLicenseWithContext(ctx, "Mozilla Public License"), }, }, { @@ -75,15 +77,15 @@ func Test_NewPackageLicenses(t *testing.T) { License: "GPL-2 | file LICENSE | LGPL (>= 2.0)", }, []pkg.License{ - pkg.NewLicense("GPL-2"), - pkg.NewLicense("LGPL2.0+"), + pkg.NewLicenseWithContext(ctx, "GPL-2"), + pkg.NewLicenseWithContext(ctx, "LGPL2.0+"), }, }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - got := parseLicenseData(tt.pd.License) + got := parseLicenseData(ctx, tt.pd.License) if len(got) != len(tt.want) { t.Errorf("unexpected number of licenses: got=%d, want=%d", len(got), len(tt.want)) } diff --git a/syft/pkg/cataloger/r/parse_description.go b/syft/pkg/cataloger/r/parse_description.go index 4731aafc9..e1b151a3b 100644 --- a/syft/pkg/cataloger/r/parse_description.go +++ b/syft/pkg/cataloger/r/parse_description.go @@ -29,10 +29,10 @@ License: Part of R 4.3.0 License: Unlimited */ -func parseDescriptionFile(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parseDescriptionFile(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { values := extractFieldsFromDescriptionFile(reader) m := parseDataFromDescriptionMap(values) - p := newPackage(m, []file.Location{reader.Location}...) + p := newPackage(ctx, m, []file.Location{reader.Location}...) if p.Name == "" || p.Version == "" { return nil, nil, nil } diff --git a/syft/pkg/cataloger/redhat/cataloger_test.go b/syft/pkg/cataloger/redhat/cataloger_test.go index 5dbb133b1..1dfc0487c 100644 --- a/syft/pkg/cataloger/redhat/cataloger_test.go +++ b/syft/pkg/cataloger/redhat/cataloger_test.go @@ -1,6 +1,7 @@ package redhat import ( + "context" "errors" "testing" @@ -15,7 +16,7 @@ import ( ) func Test_DBCataloger(t *testing.T) { - + ctx := context.TODO() dbLocation := file.NewLocation("/var/lib/rpm/rpmdb.sqlite") locations := file.NewLocationSet(dbLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) @@ -24,7 +25,7 @@ func Test_DBCataloger(t *testing.T) { Version: "11-13.el9", Type: pkg.RpmPkg, Locations: locations, - Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocations("Public Domain", dbLocation)), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "Public Domain", dbLocation)), FoundBy: "rpm-db-cataloger", PURL: "pkg:rpm/basesystem@11-13.el9?arch=noarch&upstream=basesystem-11-13.el9.src.rpm", Metadata: pkg.RpmDBEntry{ @@ -54,7 +55,7 @@ func Test_DBCataloger(t *testing.T) { Version: "5.1.8-6.el9_1", Type: pkg.RpmPkg, Locations: locations, - Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocations("GPLv3+", dbLocation)), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "GPLv3+", dbLocation)), FoundBy: "rpm-db-cataloger", PURL: "pkg:rpm/bash@5.1.8-6.el9_1?arch=x86_64&upstream=bash-5.1.8-6.el9_1.src.rpm", Metadata: pkg.RpmDBEntry{ @@ -106,7 +107,7 @@ func Test_DBCataloger(t *testing.T) { Version: "3.16-2.el9", Type: pkg.RpmPkg, Locations: locations, - Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocations("Public Domain", dbLocation)), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "Public Domain", dbLocation)), FoundBy: "rpm-db-cataloger", PURL: "pkg:rpm/filesystem@3.16-2.el9?arch=x86_64&upstream=filesystem-3.16-2.el9.src.rpm", Metadata: pkg.RpmDBEntry{ @@ -246,7 +247,6 @@ func Test_RPMFileCataloger_Globs(t *testing.T) { } func Test_denySelfReferences(t *testing.T) { - a := pkg.Package{ Name: "a", } diff --git a/syft/pkg/cataloger/redhat/package.go b/syft/pkg/cataloger/redhat/package.go index 66c5f6053..6bcdea554 100644 --- a/syft/pkg/cataloger/redhat/package.go +++ b/syft/pkg/cataloger/redhat/package.go @@ -1,6 +1,7 @@ package redhat import ( + "context" "fmt" "strconv" "strings" @@ -11,11 +12,11 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func newDBPackage(dbOrRpmLocation file.Location, m pkg.RpmDBEntry, distro *linux.Release, licenses []string) pkg.Package { +func newDBPackage(ctx context.Context, dbOrRpmLocation file.Location, m pkg.RpmDBEntry, distro *linux.Release, licenses []string) pkg.Package { p := pkg.Package{ Name: m.Name, Version: toELVersion(m.Epoch, m.Version, m.Release), - Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(dbOrRpmLocation, licenses...)...), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocationWithContext(ctx, dbOrRpmLocation, licenses...)...), PURL: packageURL(m.Name, m.Arch, m.Epoch, m.SourceRpm, m.Version, m.Release, distro), Locations: file.NewLocationSet(dbOrRpmLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), Type: pkg.RpmPkg, @@ -26,11 +27,11 @@ func newDBPackage(dbOrRpmLocation file.Location, m pkg.RpmDBEntry, distro *linux return p } -func newArchivePackage(archiveLocation file.Location, m pkg.RpmArchive, licenses []string) pkg.Package { +func newArchivePackage(ctx context.Context, archiveLocation file.Location, m pkg.RpmArchive, licenses []string) pkg.Package { p := pkg.Package{ Name: m.Name, Version: toELVersion(m.Epoch, m.Version, m.Release), - Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(archiveLocation, licenses...)...), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocationWithContext(ctx, archiveLocation, licenses...)...), PURL: packageURL(m.Name, m.Arch, m.Epoch, m.SourceRpm, m.Version, m.Release, nil), Locations: file.NewLocationSet(archiveLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), Type: pkg.RpmPkg, diff --git a/syft/pkg/cataloger/redhat/parse_rpm_archive.go b/syft/pkg/cataloger/redhat/parse_rpm_archive.go index 9a5d268e7..b019d8465 100644 --- a/syft/pkg/cataloger/redhat/parse_rpm_archive.go +++ b/syft/pkg/cataloger/redhat/parse_rpm_archive.go @@ -16,7 +16,7 @@ import ( ) // parseRpmArchive parses a single RPM -func parseRpmArchive(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parseRpmArchive(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { rpm, err := rpmutils.ReadRpm(reader) if err != nil { return nil, nil, fmt.Errorf("RPM file found but unable to read: %s (%w)", reader.RealPath, err) @@ -56,7 +56,7 @@ func parseRpmArchive(_ context.Context, _ file.Resolver, _ *generic.Environment, Files: mapFiles(files, digestAlgorithm), } - return []pkg.Package{newArchivePackage(reader.Location, metadata, licenses)}, nil, nil + return []pkg.Package{newArchivePackage(ctx, reader.Location, metadata, licenses)}, nil, nil } func getDigestAlgorithm(location file.Location, header *rpmutils.RpmHeader) string { diff --git a/syft/pkg/cataloger/redhat/parse_rpm_archive_test.go b/syft/pkg/cataloger/redhat/parse_rpm_archive_test.go index 287c63c0b..c7e5ba0fc 100644 --- a/syft/pkg/cataloger/redhat/parse_rpm_archive_test.go +++ b/syft/pkg/cataloger/redhat/parse_rpm_archive_test.go @@ -1,6 +1,7 @@ package redhat import ( + "context" "testing" "github.com/anchore/syft/syft/file" @@ -9,6 +10,7 @@ import ( ) func TestParseRpmFiles(t *testing.T) { + ctx := context.TODO() abcRpmLocation := file.NewLocation("abc-1.01-9.hg20160905.el7.x86_64.rpm") zorkRpmLocation := file.NewLocation("zork-1.0.3-1.el7.x86_64.rpm") tests := []struct { @@ -26,7 +28,7 @@ func TestParseRpmFiles(t *testing.T) { FoundBy: "rpm-archive-cataloger", Type: pkg.RpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", abcRpmLocation), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", abcRpmLocation), ), Metadata: pkg.RpmArchive{ Name: "abc", @@ -54,7 +56,7 @@ func TestParseRpmFiles(t *testing.T) { FoundBy: "rpm-archive-cataloger", Type: pkg.RpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("Public Domain", zorkRpmLocation), + pkg.NewLicenseFromLocationsWithContext(ctx, "Public Domain", zorkRpmLocation), ), Metadata: pkg.RpmArchive{ Name: "zork", diff --git a/syft/pkg/cataloger/redhat/parse_rpm_db.go b/syft/pkg/cataloger/redhat/parse_rpm_db.go index 323c286ec..856315425 100644 --- a/syft/pkg/cataloger/redhat/parse_rpm_db.go +++ b/syft/pkg/cataloger/redhat/parse_rpm_db.go @@ -20,7 +20,7 @@ import ( // parseRpmDB parses an "Packages" RPM DB and returns the Packages listed within it. // //nolint:funlen -func parseRpmDB(_ context.Context, resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parseRpmDB(ctx context.Context, resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { f, err := os.CreateTemp("", "rpmdb") if err != nil { return nil, nil, fmt.Errorf("failed to create temp rpmdb file: %w", err) @@ -85,6 +85,7 @@ func parseRpmDB(_ context.Context, resolver file.Resolver, env *generic.Environm } p := newDBPackage( + ctx, reader.Location, metadata, distro, diff --git a/syft/pkg/cataloger/redhat/parse_rpm_db_test.go b/syft/pkg/cataloger/redhat/parse_rpm_db_test.go index 7572642c8..02afa6641 100644 --- a/syft/pkg/cataloger/redhat/parse_rpm_db_test.go +++ b/syft/pkg/cataloger/redhat/parse_rpm_db_test.go @@ -80,6 +80,7 @@ func (r *rpmdbTestFileResolverMock) FilesByMIMEType(...string) ([]file.Location, } func TestParseRpmDB(t *testing.T) { + ctx := context.TODO() packagesLocation := file.NewLocation("test-fixtures/Packages") tests := []struct { fixture string @@ -98,7 +99,7 @@ func TestParseRpmDB(t *testing.T) { Locations: file.NewLocationSet(file.NewLocation("test-fixtures/Packages")), Type: pkg.RpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", packagesLocation), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", packagesLocation), ), Metadata: pkg.RpmDBEntry{ Name: "dive", @@ -128,7 +129,7 @@ func TestParseRpmDB(t *testing.T) { Locations: file.NewLocationSet(packagesLocation), Type: pkg.RpmPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", packagesLocation), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", packagesLocation), ), Metadata: pkg.RpmDBEntry{ Name: "dive", diff --git a/syft/pkg/cataloger/redhat/parse_rpm_manifest.go b/syft/pkg/cataloger/redhat/parse_rpm_manifest.go index 28f918aa2..928c0ed38 100644 --- a/syft/pkg/cataloger/redhat/parse_rpm_manifest.go +++ b/syft/pkg/cataloger/redhat/parse_rpm_manifest.go @@ -16,7 +16,7 @@ import ( ) // Parses an RPM manifest file, as used in Mariner distroless containers, and returns the Packages listed -func parseRpmManifest(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parseRpmManifest(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { r := bufio.NewReader(reader) allPkgs := make([]pkg.Package, 0) @@ -44,7 +44,7 @@ func parseRpmManifest(_ context.Context, _ file.Resolver, _ *generic.Environment continue } - p := newDBPackage(reader.Location, *metadata, nil, nil) + p := newDBPackage(ctx, reader.Location, *metadata, nil, nil) if !pkg.IsValid(&p) { continue diff --git a/syft/pkg/cataloger/ruby/package.go b/syft/pkg/cataloger/ruby/package.go index 349fd5949..b5eb48b42 100644 --- a/syft/pkg/cataloger/ruby/package.go +++ b/syft/pkg/cataloger/ruby/package.go @@ -1,6 +1,8 @@ package ruby import ( + "context" + "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" @@ -21,12 +23,12 @@ func newGemfileLockPackage(name, version string, locations ...file.Location) pkg return p } -func newGemspecPackage(m gemData, gemSpecLocation file.Location) pkg.Package { +func newGemspecPackage(ctx context.Context, m gemData, gemSpecLocation file.Location) pkg.Package { p := pkg.Package{ Name: m.Name, Version: m.Version, Locations: file.NewLocationSet(gemSpecLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), - Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(gemSpecLocation, m.Licenses...)...), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocationWithContext(ctx, gemSpecLocation, m.Licenses...)...), PURL: packageURL(m.Name, m.Version), Language: pkg.Ruby, Type: pkg.GemPkg, diff --git a/syft/pkg/cataloger/ruby/parse_gemspec.go b/syft/pkg/cataloger/ruby/parse_gemspec.go index 8fb9fb183..b62b67244 100644 --- a/syft/pkg/cataloger/ruby/parse_gemspec.go +++ b/syft/pkg/cataloger/ruby/parse_gemspec.go @@ -66,7 +66,7 @@ func processList(s string) []string { } // parseGemSpecEntries parses the gemspec file and returns the packages and relationships found. -func parseGemSpecEntries(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parseGemSpecEntries(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { var pkgs []pkg.Package var fields = make(map[string]interface{}) scanner := bufio.NewScanner(reader) @@ -105,6 +105,7 @@ func parseGemSpecEntries(_ context.Context, _ file.Resolver, _ *generic.Environm pkgs = append( pkgs, newGemspecPackage( + ctx, metadata, reader.Location, ), diff --git a/syft/pkg/cataloger/ruby/parse_gemspec_test.go b/syft/pkg/cataloger/ruby/parse_gemspec_test.go index f5ca64e95..b0b9fd56a 100644 --- a/syft/pkg/cataloger/ruby/parse_gemspec_test.go +++ b/syft/pkg/cataloger/ruby/parse_gemspec_test.go @@ -1,6 +1,7 @@ package ruby import ( + "context" "testing" "github.com/anchore/syft/syft/file" @@ -10,7 +11,7 @@ import ( func TestParseGemspec(t *testing.T) { fixture := "test-fixtures/bundler.gemspec" - + ctx := context.TODO() locations := file.NewLocationSet(file.NewLocation(fixture)) var expectedPkg = pkg.Package{ @@ -20,7 +21,7 @@ func TestParseGemspec(t *testing.T) { Locations: locations, Type: pkg.GemPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("MIT", file.NewLocation(fixture)), + pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation(fixture)), ), Language: pkg.Ruby, Metadata: pkg.RubyGemspec{ diff --git a/syft/pkg/cataloger/sbom/cataloger_test.go b/syft/pkg/cataloger/sbom/cataloger_test.go index e53b14ae7..68a6bc35c 100644 --- a/syft/pkg/cataloger/sbom/cataloger_test.go +++ b/syft/pkg/cataloger/sbom/cataloger_test.go @@ -1,6 +1,7 @@ package sbom import ( + "context" "testing" "github.com/stretchr/testify/require" @@ -20,13 +21,14 @@ func mustCPEs(s ...string) (c []cpe.CPE) { } func Test_parseSBOM(t *testing.T) { + ctx := context.TODO() expectedPkgs := []pkg.Package{ { Name: "alpine-baselayout", Version: "3.2.0-r23", Type: "apk", Locations: file.NewLocationSet(file.NewLocation("sbom.syft.json")), - Licenses: pkg.NewLicenseSet(pkg.NewLicense("GPL-2.0-only")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "GPL-2.0-only")), FoundBy: "sbom-cataloger", PURL: "pkg:apk/alpine/alpine-baselayout@3.2.0-r23?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.16.3", CPEs: mustCPEs( @@ -43,7 +45,7 @@ func Test_parseSBOM(t *testing.T) { Version: "3.2.0-r23", Type: "apk", Locations: file.NewLocationSet(file.NewLocation("sbom.syft.json")), - Licenses: pkg.NewLicenseSet(pkg.NewLicense("GPL-2.0-only")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "GPL-2.0-only")), FoundBy: "sbom-cataloger", PURL: "pkg:apk/alpine/alpine-baselayout-data@3.2.0-r23?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.16.3", CPEs: mustCPEs( @@ -64,7 +66,7 @@ func Test_parseSBOM(t *testing.T) { Version: "2.4-r1", Type: "apk", Locations: file.NewLocationSet(file.NewLocation("sbom.syft.json")), - Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "MIT")), FoundBy: "sbom-cataloger", PURL: "pkg:apk/alpine/alpine-keys@2.4-r1?arch=x86_64&upstream=alpine-keys&distro=alpine-3.16.3", CPEs: mustCPEs( @@ -81,7 +83,7 @@ func Test_parseSBOM(t *testing.T) { Version: "2.12.9-r3", Type: "apk", Locations: file.NewLocationSet(file.NewLocation("sbom.syft.json")), - Licenses: pkg.NewLicenseSet(pkg.NewLicense("GPL-2.0-only")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "GPL-2.0-only")), FoundBy: "sbom-cataloger", PURL: "pkg:apk/alpine/apk-tools@2.12.9-r3?arch=x86_64&upstream=apk-tools&distro=alpine-3.16.3", CPEs: mustCPEs( @@ -98,7 +100,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.35.0-r17", Type: "apk", Locations: file.NewLocationSet(file.NewLocation("sbom.syft.json")), - Licenses: pkg.NewLicenseSet(pkg.NewLicense("GPL-2.0-only")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "GPL-2.0-only")), FoundBy: "sbom-cataloger", PURL: "pkg:apk/alpine/busybox@1.35.0-r17?arch=x86_64&upstream=busybox&distro=alpine-3.16.3", CPEs: mustCPEs( @@ -111,8 +113,8 @@ func Test_parseSBOM(t *testing.T) { Type: "apk", Locations: file.NewLocationSet(file.NewLocation("sbom.syft.json")), Licenses: pkg.NewLicenseSet( - pkg.NewLicense("MPL-2.0"), - pkg.NewLicense("MIT"), + pkg.NewLicenseWithContext(ctx, "MPL-2.0"), + pkg.NewLicenseWithContext(ctx, "MIT"), ), FoundBy: "sbom-cataloger", PURL: "pkg:apk/alpine/ca-certificates-bundle@20220614-r0?arch=x86_64&upstream=ca-certificates&distro=alpine-3.16.3", @@ -135,8 +137,8 @@ func Test_parseSBOM(t *testing.T) { Type: "apk", Locations: file.NewLocationSet(file.NewLocation("sbom.syft.json")), Licenses: pkg.NewLicenseSet( - pkg.NewLicense("BSD-2-Clause"), - pkg.NewLicense("BSD-3-Clause"), + pkg.NewLicenseWithContext(ctx, "BSD-2-Clause"), + pkg.NewLicenseWithContext(ctx, "BSD-3-Clause"), ), FoundBy: "sbom-cataloger", PURL: "pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&upstream=libc-dev&distro=alpine-3.16.3", @@ -154,7 +156,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.1.1s-r0", Type: "apk", Locations: file.NewLocationSet(file.NewLocation("sbom.syft.json")), - Licenses: pkg.NewLicenseSet(pkg.NewLicense("OpenSSL")), // SPDX expression is not set + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "OpenSSL")), // SPDX expression is not set FoundBy: "sbom-cataloger", PURL: "pkg:apk/alpine/libcrypto1.1@1.1.1s-r0?arch=x86_64&upstream=openssl&distro=alpine-3.16.3", CPEs: mustCPEs( @@ -166,7 +168,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.1.1s-r0", Type: "apk", Locations: file.NewLocationSet(file.NewLocation("sbom.syft.json")), - Licenses: pkg.NewLicenseSet(pkg.NewLicense("OpenSSL")), // SPDX expression is not set + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "OpenSSL")), // SPDX expression is not set FoundBy: "sbom-cataloger", PURL: "pkg:apk/alpine/libssl1.1@1.1.1s-r0?arch=x86_64&upstream=openssl&distro=alpine-3.16.3", CPEs: mustCPEs( @@ -178,7 +180,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.2.3-r1", Type: "apk", Locations: file.NewLocationSet(file.NewLocation("sbom.syft.json")), - Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")), // SPDX expression is not set + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "MIT")), // SPDX expression is not set FoundBy: "sbom-cataloger", PURL: "pkg:apk/alpine/musl@1.2.3-r1?arch=x86_64&upstream=musl&distro=alpine-3.16.3", CPEs: mustCPEs( @@ -191,9 +193,9 @@ func Test_parseSBOM(t *testing.T) { Type: "apk", Locations: file.NewLocationSet(file.NewLocation("sbom.syft.json")), Licenses: pkg.NewLicenseSet( - pkg.NewLicense("MIT"), - pkg.NewLicense("BSD"), - pkg.NewLicense("GPL2+"), // SPDX expression is not set + pkg.NewLicenseWithContext(ctx, "MIT"), + pkg.NewLicenseWithContext(ctx, "BSD"), + pkg.NewLicenseWithContext(ctx, "GPL2+"), // SPDX expression is not set ), FoundBy: "sbom-cataloger", PURL: "pkg:apk/alpine/musl-utils@1.2.3-r1?arch=x86_64&upstream=musl&distro=alpine-3.16.3", @@ -212,7 +214,7 @@ func Test_parseSBOM(t *testing.T) { Type: "apk", Locations: file.NewLocationSet(file.NewLocation("sbom.syft.json")), Licenses: pkg.NewLicenseSet( - pkg.NewLicense("GPL-2.0-only"), + pkg.NewLicenseWithContext(ctx, "GPL-2.0-only"), ), FoundBy: "sbom-cataloger", PURL: "pkg:apk/alpine/scanelf@1.3.4-r0?arch=x86_64&upstream=pax-utils&distro=alpine-3.16.3", @@ -226,7 +228,7 @@ func Test_parseSBOM(t *testing.T) { Type: "apk", Locations: file.NewLocationSet(file.NewLocation("sbom.syft.json")), Licenses: pkg.NewLicenseSet( - pkg.NewLicense("GPL-2.0-only"), + pkg.NewLicenseWithContext(ctx, "GPL-2.0-only"), ), FoundBy: "sbom-cataloger", PURL: "pkg:apk/alpine/ssl_client@1.35.0-r17?arch=x86_64&upstream=busybox&distro=alpine-3.16.3", @@ -245,7 +247,7 @@ func Test_parseSBOM(t *testing.T) { Type: "apk", Locations: file.NewLocationSet(file.NewLocation("sbom.syft.json")), Licenses: pkg.NewLicenseSet( - pkg.NewLicense("Zlib"), + pkg.NewLicenseWithContext(ctx, "Zlib"), ), FoundBy: "sbom-cataloger", PURL: "pkg:apk/alpine/zlib@1.2.12-r3?arch=x86_64&upstream=zlib&distro=alpine-3.16.3", @@ -269,7 +271,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.1.1s-r0", Type: "apk", Locations: apkgdbLocation, - Licenses: pkg.NewLicenseSet(pkg.NewLicense("OpenSSL")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "OpenSSL")), FoundBy: "apkdb-cataloger", PURL: "pkg:apk/alpine/libssl1.1@1.1.1s-r0?arch=x86_64&upstream=openssl&distro=alpine-3.16.3", CPEs: mustCPEs( @@ -282,7 +284,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.35.0-r17", Type: "apk", Locations: apkgdbLocation, - Licenses: pkg.NewLicenseSet(pkg.NewLicense("GPL-2.0-only")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "GPL-2.0-only")), FoundBy: "apkdb-cataloger", PURL: "pkg:apk/alpine/ssl_client@1.35.0-r17?arch=x86_64&upstream=busybox&distro=alpine-3.16.3", CPEs: mustCPEs( @@ -300,7 +302,7 @@ func Test_parseSBOM(t *testing.T) { Version: "3.2.0-r23", Type: "apk", Locations: apkgdbLocation, - Licenses: pkg.NewLicenseSet(pkg.NewLicense("GPL-2.0-only")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "GPL-2.0-only")), FoundBy: "apkdb-cataloger", PURL: "pkg:apk/alpine/alpine-baselayout@3.2.0-r23?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.16.3", CPEs: mustCPEs( @@ -318,7 +320,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.35.0-r17", Type: "apk", Locations: apkgdbLocation, - Licenses: pkg.NewLicenseSet(pkg.NewLicense("GPL-2.0-only")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "GPL-2.0-only")), FoundBy: "apkdb-cataloger", PURL: "pkg:apk/alpine/busybox@1.35.0-r17?arch=x86_64&upstream=busybox&distro=alpine-3.16.3", CPEs: mustCPEs( @@ -331,7 +333,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.2.3-r1", Type: "apk", Locations: apkgdbLocation, - Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")), + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "MIT")), FoundBy: "apkdb-cataloger", PURL: "pkg:apk/alpine/musl@1.2.3-r1?arch=x86_64&upstream=musl&distro=alpine-3.16.3", CPEs: mustCPEs( diff --git a/syft/pkg/cataloger/wordpress/package.go b/syft/pkg/cataloger/wordpress/package.go index 596ccfb5a..7a06317c7 100644 --- a/syft/pkg/cataloger/wordpress/package.go +++ b/syft/pkg/cataloger/wordpress/package.go @@ -1,11 +1,13 @@ package wordpress import ( + "context" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" ) -func newWordpressPluginPackage(name, version string, m pluginData, location file.Location) pkg.Package { +func newWordpressPluginPackage(ctx context.Context, name, version string, m pluginData, location file.Location) pkg.Package { meta := pkg.WordpressPluginEntry{ PluginInstallDirectory: m.PluginInstallDirectory, Author: m.Author, @@ -22,7 +24,7 @@ func newWordpressPluginPackage(name, version string, m pluginData, location file } if len(m.Licenses) > 0 { - p.Licenses = pkg.NewLicenseSet(pkg.NewLicense(m.Licenses[0])) + p.Licenses = pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, m.Licenses[0])) } p.SetID() diff --git a/syft/pkg/cataloger/wordpress/parse_plugin.go b/syft/pkg/cataloger/wordpress/parse_plugin.go index 713058f19..83607bdfd 100644 --- a/syft/pkg/cataloger/wordpress/parse_plugin.go +++ b/syft/pkg/cataloger/wordpress/parse_plugin.go @@ -38,7 +38,7 @@ type pluginData struct { pkg.WordpressPluginEntry `mapstructure:",squash" json:",inline"` } -func parseWordpressPluginFiles(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parseWordpressPluginFiles(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { var pkgs []pkg.Package buffer := make([]byte, contentBufferSize) @@ -80,6 +80,7 @@ func parseWordpressPluginFiles(_ context.Context, _ file.Resolver, _ *generic.En pkgs = append( pkgs, newWordpressPluginPackage( + ctx, name.(string), version.(string), metadata, diff --git a/syft/pkg/cataloger/wordpress/parse_plugin_test.go b/syft/pkg/cataloger/wordpress/parse_plugin_test.go index a9e10f289..4b95fd91e 100644 --- a/syft/pkg/cataloger/wordpress/parse_plugin_test.go +++ b/syft/pkg/cataloger/wordpress/parse_plugin_test.go @@ -1,6 +1,7 @@ package wordpress import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -13,14 +14,14 @@ import ( func TestParseWordpressPluginFiles(t *testing.T) { fixture := "test-fixtures/glob-paths/wp-content/plugins/akismet/akismet.php" locations := file.NewLocationSet(file.NewLocation(fixture)) - + ctx := context.TODO() var expectedPkg = pkg.Package{ Name: "Akismet Anti-spam: Spam Protection", Version: "5.3", Locations: locations, Type: pkg.WordpressPluginPkg, Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("GPLv2"), + pkg.NewLicenseFromLocationsWithContext(ctx, "GPLv2"), ), Language: pkg.PHP, Metadata: pkg.WordpressPluginEntry{ diff --git a/syft/pkg/collection_test.go b/syft/pkg/collection_test.go index 5fee8d8f6..14f698c15 100644 --- a/syft/pkg/collection_test.go +++ b/syft/pkg/collection_test.go @@ -1,6 +1,7 @@ package pkg import ( + "context" "testing" "github.com/scylladb/go-set/strset" @@ -18,6 +19,7 @@ type expectedIndexes struct { } func TestCatalogMergePackageLicenses(t *testing.T) { + ctx := context.TODO() tests := []struct { name string pkgs []Package @@ -29,13 +31,13 @@ func TestCatalogMergePackageLicenses(t *testing.T) { { id: "equal", Licenses: NewLicenseSet( - NewLicensesFromValues("foo", "baq", "quz")..., + NewLicensesFromValuesWithContext(ctx, "foo", "baq", "quz")..., ), }, { id: "equal", Licenses: NewLicenseSet( - NewLicensesFromValues("bar", "baz", "foo", "qux")..., + NewLicensesFromValuesWithContext(ctx, "bar", "baz", "foo", "qux")..., ), }, }, @@ -43,7 +45,7 @@ func TestCatalogMergePackageLicenses(t *testing.T) { { id: "equal", Licenses: NewLicenseSet( - NewLicensesFromValues("foo", "baq", "quz", "qux", "bar", "baz")..., + NewLicensesFromValuesWithContext(ctx, "foo", "baq", "quz", "qux", "bar", "baz")..., ), }, }, diff --git a/syft/pkg/license.go b/syft/pkg/license.go index d00da3425..e0bbae8da 100644 --- a/syft/pkg/license.go +++ b/syft/pkg/license.go @@ -1,13 +1,18 @@ package pkg import ( + "crypto/sha256" + "encoding/hex" "fmt" + "io" "net/url" "sort" "strings" "github.com/scylladb/go-set/strset" + "golang.org/x/net/context" + "github.com/anchore/syft/internal/licenses" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" @@ -16,7 +21,7 @@ import ( var _ sort.Interface = (*Licenses)(nil) -// License represents an SPDX Expression or license value extracted from a packages metadata +// License represents an SPDX Expression or license value extracted from a package's metadata // We want to ignore URLs and Location since we merge these fields across equal licenses. // A License is a unique combination of value, expression and type, where // its sources are always considered merged and additions to the evidence @@ -24,18 +29,17 @@ var _ sort.Interface = (*Licenses)(nil) // This is different from how we treat a package since we consider package paths // in order to distinguish if packages should be kept separate // this is different for licenses since we're only looking for evidence -// of where a license was declared/concluded for a given package -// If a license is given as it's full text in the metadata rather than it's value or SPDX expression - -// The Contents field is used to represent this data -// A Concluded License type is the license the SBOM creator believes governs the package (human crafted or altered SBOM) +// of where a license was declared/concluded for a given package. +// If a license is given as it's full text in the metadata rather than it's value or SPDX expression. +// The Contents field is used to represent this data. +// A Concluded License type is the license the SBOM creator believes governs the package (human crafted or altered SBOM). // The Declared License is what the authors of a project believe govern the package. This is the default type syft declares. type License struct { SPDXExpression string Value string Type license.Type - Contents string - URLs []string `hash:"ignore"` + Contents string `hash:"ignore"` // we want to ignore the contents here so we can drop contents in the post-processing step + URLs []string `hash:"ignore"` // TODO: there is such thing as a url-only license, but we aren't hashing on this, which means overwriting could occur in the license set Locations file.LocationSet `hash:"ignore"` } @@ -54,8 +58,8 @@ func (l Licenses) Less(i, j int) bool { // returning true here reduces the number of swaps // while keeping a consistent sort order of // the order that they appear in the list initially - // If users in the future have preference to sorting based - // on the slice representation of either field we can update this code + // If users in the future have a preference to sorting based + // on the slice representation of either field, we can update this code return true } return l[i].Contents < l[j].Contents @@ -71,87 +75,50 @@ func (l Licenses) Swap(i, j int) { l[i], l[j] = l[j], l[i] } -func NewLicense(value string) License { - return NewLicenseFromType(value, license.Declared) +func NewLicensesFromReadCloserWithContext(ctx context.Context, closer file.LocationReadCloser) []License { + //Definition: The license that the auditor or scanning tool concludes applies, based on the actual contents of the files. + //Source: Derived from analyzing the source code, license headers, and full license texts in the files. + // Given we are scanning the contents of the file, we should use the Concluded License type. + return newLicenseBuilder().WithContents(closer).WithLocations(closer.Location).WithType(license.Concluded).Build(ctx).ToSlice() } -func NewLicenseFromType(value string, t license.Type) License { - var ( - spdxExpression string - fullText string - ) - // Check parsed value for newline character to see if it's the full license text - // License: - // DO we want to also submit file name when determining fulltext - if strings.Contains(strings.TrimSpace(value), "\n") { - fullText = value - } else { - var err error - spdxExpression, err = license.ParseExpression(value) - if err != nil { - log.WithFields("error", err, "expression", value).Trace("unable to parse license expression") - } - } - - if fullText != "" { - return License{ - Contents: fullText, - Type: t, - Locations: file.NewLocationSet(), - } - } - - return License{ - Value: value, - SPDXExpression: spdxExpression, - Type: t, - Locations: file.NewLocationSet(), - } +func NewLicenseWithContext(ctx context.Context, value string) License { + return NewLicenseFromTypeWithContext(ctx, value, license.Declared) } -func NewLicensesFromValues(values ...string) (licenses []License) { - for _, v := range values { - licenses = append(licenses, NewLicense(v)) +func NewLicenseFromTypeWithContext(ctx context.Context, value string, t license.Type) License { + lics := newLicenseBuilder().WithValues(value).WithType(t).Build(ctx).ToSlice() + if len(lics) > 0 { + return lics[0] } - return + // TODO: this is not ideal, but also not expected given the input of "value" + return License{} } -func NewLicensesFromLocation(location file.Location, values ...string) (licenses []License) { - for _, v := range values { - if v == "" { - continue - } - licenses = append(licenses, NewLicenseFromLocations(v, location)) - } - return licenses +func NewLicensesFromValuesWithContext(ctx context.Context, values ...string) []License { + return newLicenseBuilder().WithValues(values...).Build(ctx).ToSlice() } -func NewLicenseFromLocations(value string, locations ...file.Location) License { - l := NewLicense(value) - for _, loc := range locations { - l.Locations.Add(loc) - } - return l +func NewLicensesFromLocationWithContext(ctx context.Context, location file.Location, values ...string) []License { + return newLicenseBuilder().WithValues(values...).WithLocations(location).Build(ctx).ToSlice() } -func NewLicenseFromURLs(value string, urls ...string) License { - l := NewLicense(value) - s := strset.New() - for _, url := range urls { - if url != "" { - sanitizedURL, err := stripUnwantedCharacters(url) - if err != nil { - log.Tracef("unable to sanitize url=%q: %s", url, err) - continue - } - s.Add(sanitizedURL) - } +func NewLicenseFromLocationsWithContext(ctx context.Context, value string, locations ...file.Location) License { + lics := newLicenseBuilder().WithValues(value).WithLocations(locations...).Build(ctx).ToSlice() + if len(lics) > 0 { + return lics[0] } + // TODO: this is not ideal, but also not expected given the input of "value" + return License{} +} - l.URLs = s.List() - sort.Strings(l.URLs) - - return l +func NewLicenseFromURLsWithContext(ctx context.Context, value string, urls ...string) License { + lics := newLicenseBuilder().WithValues(value).WithURLs(urls...).Build(ctx).ToSlice() + if len(lics) > 0 { + return lics[0] + } + // TODO: this is not ideal, but also not expected given the input of "value" + return License{} } func stripUnwantedCharacters(rawURL string) (string, error) { @@ -164,25 +131,17 @@ func stripUnwantedCharacters(rawURL string) (string, error) { return cleanedURL, nil } -func NewLicenseFromFields(value, url string, location *file.Location) License { - l := NewLicense(value) - if location != nil { - l.Locations.Add(*location) +func NewLicenseFromFieldsWithContext(ctx context.Context, value, url string, location *file.Location) License { + lics := newLicenseBuilder().WithValues(value).WithURLs(url).WithOptionalLocation(location).Build(ctx).ToSlice() + if len(lics) > 0 { + return lics[0] } - if url != "" { - sanitizedURL, err := stripUnwantedCharacters(url) - if err != nil { - log.Tracef("unable to sanitize url=%q: %s", url, err) - } else { - l.URLs = append(l.URLs, sanitizedURL) - } - } - - return l + // TODO: this is not ideal, but also not expected given the input of "value" + return License{} } func (s License) Empty() bool { - return s.Value == "" && s.SPDXExpression == "" && s.Contents == "" + return s.Value == "" && s.SPDXExpression == "" && s.Contents == "" && len(s.URLs) == 0 } // Merge two licenses into a new license object. If the merge is not possible due to unmergeable fields @@ -215,10 +174,216 @@ func (s License) Merge(l License) (*License, error) { return &s, nil } - // since the set instance has a reference type (map) we must make a new instance + // since the set instance has a reference type (map), we must make a new instance locations := file.NewLocationSet(s.Locations.ToSlice()...) locations.Add(l.Locations.ToSlice()...) s.Locations = locations return &s, nil } + +type licenseBuilder struct { + values []string + contents []io.ReadCloser + locations []file.Location + urls []string + tp license.Type +} + +func newLicenseBuilder() *licenseBuilder { + return &licenseBuilder{ + tp: license.Declared, + } +} + +func (b *licenseBuilder) WithValues(expr ...string) *licenseBuilder { + for _, v := range expr { + if v == "" { + continue + } + b.values = append(b.values, v) + } + return b +} + +func (b *licenseBuilder) WithOptionalLocation(location *file.Location) *licenseBuilder { + if location != nil { + b.locations = append(b.locations, *location) + } + return b +} + +func (b *licenseBuilder) WithURLs(urls ...string) *licenseBuilder { + s := strset.New() + for _, u := range urls { + if u != "" { + sanitizedURL, err := stripUnwantedCharacters(u) + if err != nil { + log.Tracef("unable to sanitize url=%q: %s", u, err) + continue + } + s.Add(sanitizedURL) + } + } + + b.urls = append(b.urls, s.List()...) + sort.Strings(b.urls) + return b +} + +func (b *licenseBuilder) WithLocations(locations ...file.Location) *licenseBuilder { + for _, loc := range locations { + if loc.Path() != "" { + b.locations = append(b.locations, loc) + } + } + return b +} + +func (b *licenseBuilder) WithContents(contents ...io.ReadCloser) *licenseBuilder { + for _, content := range contents { + if content != nil { + b.contents = append(b.contents, content) + } + } + return b +} + +func (b *licenseBuilder) WithType(t license.Type) *licenseBuilder { + b.tp = t // last one wins, multiple is not valid + return b +} + +func (b *licenseBuilder) Build(ctx context.Context) LicenseSet { + // for every value make a license with all locations + // or for every reader make a license with all locations + // if given a reader and a value, this is invalid + + locations := file.NewLocationSet(b.locations...) + + set := NewLicenseSet() + for _, v := range b.values { + if strings.Contains(v, "\n") { + var loc file.Location + if len(b.locations) > 0 { + loc = b.locations[0] + } + b.contents = append(b.contents, file.NewLocationReadCloser(loc, io.NopCloser(strings.NewReader(v)))) + continue + } + + // we want to check if the SPDX field should be set + var expression string + if ex, err := license.ParseExpression(v); err == nil { + expression = ex + } + + set.Add(License{ + SPDXExpression: expression, + Value: strings.TrimSpace(v), + Type: b.tp, + URLs: b.urls, + Locations: locations, + }) + } + + // we have some readers (with no values); let's try to turn into licenses if we can + for _, content := range b.contents { + set.Add(b.buildFromContents(ctx, content)...) + } + + if set.Empty() && len(b.urls) > 0 { + // if we have no values or contents, but we do have URLs, let's make a license with the URLs + set.Add(License{ + Type: b.tp, + URLs: b.urls, + Locations: locations, + }) + } + + return set +} + +func (b *licenseBuilder) buildFromContents(ctx context.Context, contents io.ReadCloser) []License { + if !licenses.IsContextLicenseScannerSet(ctx) { + // we do not have a scanner; we don't want to create one; we sha256 the content and populate the value + internal, err := contentFromReader(contents) + if err != nil { + log.WithFields("error", err).Trace("could not read content") + return nil + } + return []License{b.licenseFromContentHash(internal)} + } + + scanner, err := licenses.ContextLicenseScanner(ctx) + if err != nil { + log.WithFields("error", err).Trace("could not find license scanner") + internal, err := contentFromReader(contents) + if err != nil { + log.WithFields("error", err).Trace("could not read content") + return nil + } + return []License{b.licenseFromContentHash(internal)} + } + + evidence, content, err := scanner.FindEvidence(ctx, contents) + if err != nil { + log.WithFields("error", err).Trace("scanner failed to scan contents") + return nil + } + + if len(evidence) > 0 { + // we have some ID and offsets to apply to our content; let's make some detailed licenses + return b.licensesFromEvidenceAndContent(evidence, content) + } + // scanner couldn't find anything, but we still have the file contents; sha256 and send it back with value + return []License{b.licenseFromContentHash(string(content))} +} + +func (b *licenseBuilder) licensesFromEvidenceAndContent(evidence []licenses.Evidence, content []byte) []License { + ls := make([]License, 0) + for _, e := range evidence { + // basic license + candidate := License{ + Value: e.ID, + Locations: file.NewLocationSet(b.locations...), + Type: b.tp, + } + // get content offset + if e.Start >= 0 && e.End <= len(content) && e.Start <= e.End { + candidate.Contents = string(content[e.Start:e.End]) + } + // check for SPDX Validity + if ex, err := license.ParseExpression(e.ID); err == nil { + candidate.SPDXExpression = ex + } + + ls = append(ls, candidate) + } + return ls +} + +func (b *licenseBuilder) licenseFromContentHash(content string) License { + hash := sha256HexFromString(content) + value := "LicenseRef-sha256:" + hash + + return License{ + Value: value, + Contents: content, + Type: b.tp, + Locations: file.NewLocationSet(b.locations...), + } +} + +func contentFromReader(r io.Reader) (string, error) { + bytes, err := io.ReadAll(r) + if err != nil { + return "", err + } + return strings.TrimSpace(string(bytes)), nil +} + +func sha256HexFromString(s string) string { + hash := sha256.Sum256([]byte(s)) + return hex.EncodeToString(hash[:]) +} diff --git a/syft/pkg/license_deprecated.go b/syft/pkg/license_deprecated.go new file mode 100644 index 000000000..a1e3cc7b4 --- /dev/null +++ b/syft/pkg/license_deprecated.go @@ -0,0 +1,112 @@ +package pkg + +import ( + "sort" + "strings" + + "github.com/scylladb/go-set/strset" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/license" +) + +func NewLicense(value string) License { + return NewLicenseFromType(value, license.Declared) +} + +func NewLicenseFromType(value string, t license.Type) License { + var ( + spdxExpression string + fullText string + ) + // Check parsed value for newline character to see if it's the full license text + // License: + // DO we want to also submit file name when determining fulltext + if strings.Contains(strings.TrimSpace(value), "\n") { + fullText = value + } else { + var err error + spdxExpression, err = license.ParseExpression(value) + if err != nil { + log.WithFields("error", err, "expression", value).Trace("unable to parse license expression") + } + } + + if fullText != "" { + return License{ + Contents: fullText, + Type: t, + Locations: file.NewLocationSet(), + } + } + + return License{ + Value: value, + SPDXExpression: spdxExpression, + Type: t, + Locations: file.NewLocationSet(), + } +} + +func NewLicensesFromValues(values ...string) (licenses []License) { + for _, v := range values { + licenses = append(licenses, NewLicense(v)) + } + return +} + +func NewLicensesFromLocation(location file.Location, values ...string) (licenses []License) { + for _, v := range values { + if v == "" { + continue + } + licenses = append(licenses, NewLicenseFromLocations(v, location)) + } + return licenses +} + +func NewLicenseFromLocations(value string, locations ...file.Location) License { + l := NewLicense(value) + for _, loc := range locations { + l.Locations.Add(loc) + } + return l +} + +func NewLicenseFromURLs(value string, urls ...string) License { + l := NewLicense(value) + s := strset.New() + for _, url := range urls { + if url != "" { + sanitizedURL, err := stripUnwantedCharacters(url) + if err != nil { + log.Tracef("unable to sanitize url=%q: %s", url, err) + continue + } + s.Add(sanitizedURL) + } + } + + l.URLs = s.List() + sort.Strings(l.URLs) + + return l +} + +func NewLicenseFromFields(value, url string, location *file.Location) License { + l := NewLicense(value) + if location != nil { + l.Locations.Add(*location) + } + if url != "" { + sanitizedURL, err := stripUnwantedCharacters(url) + if err != nil { + log.Tracef("unable to sanitize url=%q: %s", url, err) + } else { + l.URLs = append(l.URLs, sanitizedURL) + } + } + + return l +} diff --git a/syft/pkg/license_set_test.go b/syft/pkg/license_set_test.go index 00c9a1f27..ada5502fa 100644 --- a/syft/pkg/license_set_test.go +++ b/syft/pkg/license_set_test.go @@ -1,16 +1,22 @@ package pkg import ( + "context" "os" "testing" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "github.com/anchore/syft/internal/licenses" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/license" ) func TestLicenseSet_Add(t *testing.T) { + scanner, err := licenses.NewDefaultScanner() + require.NoError(t, err) + ctx := licenses.SetContextLicenseScanner(context.Background(), scanner) tests := []struct { name string licenses []License @@ -19,51 +25,52 @@ func TestLicenseSet_Add(t *testing.T) { { name: "add one simple license", licenses: []License{ - NewLicense("MIT"), + NewLicenseWithContext(ctx, "MIT"), }, want: []License{ - NewLicense("MIT"), + NewLicenseWithContext(ctx, "MIT"), }, }, { name: "add multiple simple licenses", licenses: []License{ - NewLicense("MIT"), - NewLicense("MIT"), - NewLicense("Apache-2.0"), + NewLicenseWithContext(ctx, "MIT"), + NewLicenseWithContext(ctx, "MIT"), + NewLicenseWithContext(ctx, "Apache-2.0"), }, want: []License{ - NewLicense("Apache-2.0"), - NewLicense("MIT"), + NewLicenseWithContext(ctx, "Apache-2.0"), + NewLicenseWithContext(ctx, "MIT"), }, }, { name: "attempt to add a license with no name", licenses: []License{ - NewLicense(""), + NewLicenseWithContext(ctx, ""), }, want: []License{}, }, { name: "keep multiple licenses sorted", licenses: []License{ - NewLicense("MIT"), - NewLicense("Apache-2.0"), + NewLicenseWithContext(ctx, "MIT"), + NewLicenseWithContext(ctx, "Apache-2.0"), }, want: []License{ - NewLicense("Apache-2.0"), - NewLicense("MIT"), + NewLicenseWithContext(ctx, "Apache-2.0"), + NewLicenseWithContext(ctx, "MIT"), }, }, { name: "deduplicate licenses with locations", licenses: []License{ - NewLicenseFromLocations("MIT", file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/place", FileSystemID: "1"})), - NewLicenseFromLocations("MIT", file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/place", FileSystemID: "1"})), - NewLicenseFromLocations("MIT", file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/place", FileSystemID: "2"})), + NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/place", FileSystemID: "1"})), + NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/place", FileSystemID: "1"})), + NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/place", FileSystemID: "2"})), }, want: []License{ - NewLicenseFromLocations( + NewLicenseFromLocationsWithContext( + ctx, "MIT", file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/place", FileSystemID: "1"}), file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/place", FileSystemID: "2"}), @@ -73,12 +80,13 @@ func TestLicenseSet_Add(t *testing.T) { { name: "same licenses with different locations", licenses: []License{ - NewLicense("MIT"), - NewLicenseFromLocations("MIT", file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/place", FileSystemID: "2"})), - NewLicenseFromLocations("MIT", file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/place", FileSystemID: "1"})), + NewLicenseWithContext(ctx, "MIT"), + NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/place", FileSystemID: "2"})), + NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/place", FileSystemID: "1"})), }, want: []License{ - NewLicenseFromLocations( + NewLicenseFromLocationsWithContext( + ctx, "MIT", file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/place", FileSystemID: "1"}), file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/place", FileSystemID: "2"}), @@ -88,9 +96,9 @@ func TestLicenseSet_Add(t *testing.T) { { name: "same license from different sources", licenses: []License{ - NewLicense("MIT"), - NewLicenseFromLocations("MIT", file.NewLocation("/place")), - NewLicenseFromURLs("MIT", "https://example.com"), + NewLicenseWithContext(ctx, "MIT"), + NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("/place")), + NewLicenseFromURLsWithContext(ctx, "MIT", "https://example.com"), }, want: []License{ { @@ -105,10 +113,10 @@ func TestLicenseSet_Add(t *testing.T) { { name: "different licenses from different sources with different types constitute two licenses", licenses: []License{ - NewLicenseFromType("MIT", license.Concluded), - NewLicenseFromType("MIT", license.Declared), - NewLicenseFromLocations("MIT", file.NewLocation("/place")), - NewLicenseFromURLs("MIT", "https://example.com"), + NewLicenseFromTypeWithContext(ctx, "MIT", license.Concluded), + NewLicenseFromTypeWithContext(ctx, "MIT", license.Declared), + NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("/place")), + NewLicenseFromURLsWithContext(ctx, "MIT", "https://example.com"), }, want: []License{ { @@ -129,15 +137,19 @@ func TestLicenseSet_Add(t *testing.T) { { name: "licenses that are unknown with different contents can exist in the same set", licenses: []License{ - NewLicense(readFileAsString("../../internal/licenses/test-fixtures/nvidia-software-and-cuda-supplement")), - NewLicense(readFileAsString("../../internal/licenses/test-fixtures/apache-license-2.0")), + NewLicenseWithContext(ctx, readFileAsString("../../internal/licenses/test-fixtures/nvidia-software-and-cuda-supplement")), + NewLicenseWithContext(ctx, readFileAsString("../../internal/licenses/test-fixtures/apache-license-2.0")), }, want: []License{ { - Contents: readFileAsString("../../internal/licenses/test-fixtures/apache-license-2.0"), - Type: license.Declared, + SPDXExpression: "Apache-2.0", + Value: "Apache-2.0", + Type: license.Declared, + Contents: readFileAsString("../../internal/licenses/test-fixtures/apache-license-2.0"), + Locations: file.NewLocationSet(), }, { + Value: "LicenseRef-sha256:eebcea3ab1d1a28e671de90119ffcfb35fe86951e4af1b17af52b7a82fcf7d0a", Contents: readFileAsString("../../internal/licenses/test-fixtures/nvidia-software-and-cuda-supplement"), Type: license.Declared, }, diff --git a/syft/pkg/license_test.go b/syft/pkg/license_test.go index e076cb1bf..101b8adeb 100644 --- a/syft/pkg/license_test.go +++ b/syft/pkg/license_test.go @@ -1,6 +1,7 @@ package pkg import ( + "context" "sort" "testing" @@ -13,13 +14,14 @@ import ( ) func Test_Hash(t *testing.T) { + ctx := context.TODO() loc1 := file.NewLocation("place!") loc1.FileSystemID = "fs1" loc2 := file.NewLocation("place!") loc2.FileSystemID = "fs2" // important! there is a different file system ID - lic1 := NewLicenseFromFields("MIT", "foo", &loc1) - lic2 := NewLicenseFromFields("MIT", "bar", &loc2) + lic1 := NewLicenseFromFieldsWithContext(ctx, "MIT", "foo", &loc1) + lic2 := NewLicenseFromFieldsWithContext(ctx, "MIT", "bar", &loc2) hash1, err := artifact.IDByHash(lic1) require.NoError(t, err) @@ -31,6 +33,7 @@ func Test_Hash(t *testing.T) { } func Test_Sort(t *testing.T) { + ctx := context.TODO() tests := []struct { name string licenses Licenses @@ -44,57 +47,57 @@ func Test_Sort(t *testing.T) { { name: "single", licenses: []License{ - NewLicenseFromLocations("MIT", file.NewLocation("place!")), + NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("place!")), }, expected: []License{ - NewLicenseFromLocations("MIT", file.NewLocation("place!")), + NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("place!")), }, }, { name: "multiple", licenses: []License{ - NewLicenseFromLocations("MIT", file.NewLocation("place!")), - NewLicenseFromURLs("MIT", "https://github.com/anchore/syft/blob/main/LICENSE"), - NewLicenseFromLocations("Apache", file.NewLocation("area!")), - NewLicenseFromLocations("gpl2+", file.NewLocation("area!")), + NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("place!")), + NewLicenseFromURLsWithContext(ctx, "MIT", "https://github.com/anchore/syft/blob/main/LICENSE"), + NewLicenseFromLocationsWithContext(ctx, "Apache", file.NewLocation("area!")), + NewLicenseFromLocationsWithContext(ctx, "gpl2+", file.NewLocation("area!")), }, expected: Licenses{ - NewLicenseFromLocations("Apache", file.NewLocation("area!")), - NewLicenseFromURLs("MIT", "https://github.com/anchore/syft/blob/main/LICENSE"), - NewLicenseFromLocations("MIT", file.NewLocation("place!")), - NewLicenseFromLocations("gpl2+", file.NewLocation("area!")), + NewLicenseFromLocationsWithContext(ctx, "Apache", file.NewLocation("area!")), + NewLicenseFromURLsWithContext(ctx, "MIT", "https://github.com/anchore/syft/blob/main/LICENSE"), + NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("place!")), + NewLicenseFromLocationsWithContext(ctx, "gpl2+", file.NewLocation("area!")), }, }, { name: "multiple with location variants", licenses: []License{ - NewLicenseFromLocations("MIT", file.NewLocation("place!")), - NewLicenseFromLocations("MIT", file.NewLocation("park!")), - NewLicense("MIT"), - NewLicense("AAL"), - NewLicense("Adobe-2006"), - NewLicenseFromLocations("Apache", file.NewLocation("area!")), + NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("place!")), + NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("park!")), + NewLicenseWithContext(ctx, "MIT"), + NewLicenseWithContext(ctx, "AAL"), + NewLicenseWithContext(ctx, "Adobe-2006"), + NewLicenseFromLocationsWithContext(ctx, "Apache", file.NewLocation("area!")), }, expected: Licenses{ - NewLicense("AAL"), - NewLicense("Adobe-2006"), - NewLicenseFromLocations("Apache", file.NewLocation("area!")), - NewLicense("MIT"), - NewLicenseFromLocations("MIT", file.NewLocation("park!")), - NewLicenseFromLocations("MIT", file.NewLocation("place!")), + NewLicenseWithContext(ctx, "AAL"), + NewLicenseWithContext(ctx, "Adobe-2006"), + NewLicenseFromLocationsWithContext(ctx, "Apache", file.NewLocation("area!")), + NewLicenseWithContext(ctx, "MIT"), + NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("park!")), + NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation("place!")), }, }, { - name: "multiple licenses with only contents", + name: "multiple licenses with only contents are still sorted by their computed lic.value references", licenses: []License{ - NewLicense(readFileAsString("../../internal/licenses/test-fixtures/nvidia-software-and-cuda-supplement")), - NewLicense(readFileAsString("../../internal/licenses/test-fixtures/Knuth-CTAN")), - NewLicense(readFileAsString("../../internal/licenses/test-fixtures/apache-license-2.0")), + NewLicenseWithContext(ctx, readFileAsString("../../internal/licenses/test-fixtures/nvidia-software-and-cuda-supplement")), + NewLicenseWithContext(ctx, readFileAsString("../../internal/licenses/test-fixtures/Knuth-CTAN")), + NewLicenseWithContext(ctx, readFileAsString("../../internal/licenses/test-fixtures/apache-license-2.0")), }, expected: Licenses{ - NewLicense(readFileAsString("../../internal/licenses/test-fixtures/apache-license-2.0")), - NewLicense(readFileAsString("../../internal/licenses/test-fixtures/nvidia-software-and-cuda-supplement")), - NewLicense(readFileAsString("../../internal/licenses/test-fixtures/Knuth-CTAN")), + NewLicenseWithContext(ctx, readFileAsString("../../internal/licenses/test-fixtures/apache-license-2.0")), + NewLicenseWithContext(ctx, readFileAsString("../../internal/licenses/test-fixtures/nvidia-software-and-cuda-supplement")), + NewLicenseWithContext(ctx, readFileAsString("../../internal/licenses/test-fixtures/Knuth-CTAN")), }, }, } @@ -240,6 +243,7 @@ func TestLicense_Merge(t *testing.T) { } func TestFullText(t *testing.T) { + ctx := context.TODO() fullText := `I am a license with full text my authors put new line characters in metadata for labeling a license` tests := []struct { @@ -248,10 +252,10 @@ func TestFullText(t *testing.T) { want License }{ { - name: "Full Text field is populated with the correct full text", + name: "Full Text field is populated with the correct full text and contents are given a sha256 as value", value: fullText, want: License{ - Value: "", + Value: "LicenseRef-sha256:108067fa71229a2b98b9696af0ce21cd11d9639634c8bc94bda70ebedf291e5a", Type: license.Declared, Contents: fullText, }, @@ -260,13 +264,14 @@ func TestFullText(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := NewLicense(tt.value) + got := NewLicenseWithContext(ctx, tt.value) assert.Equal(t, tt.want, got) }) } } func TestLicenseConstructors(t *testing.T) { + ctx := context.TODO() type input struct { value string urls []string @@ -291,11 +296,22 @@ func TestLicenseConstructors(t *testing.T) { URLs: []string{"http://user-agent-utils.googlecode.com/svn/trunk/UserAgentUtils/LICENSE.txt"}, }, }, + { + name: "License URLs without value", + input: input{ + value: "", + urls: []string{"http://user-agent-utils.googlecode.com/svn/trunk/UserAgentUtils/LICENSE.txt"}, + }, + expected: License{ + Type: license.Declared, + URLs: []string{"http://user-agent-utils.googlecode.com/svn/trunk/UserAgentUtils/LICENSE.txt"}, + }, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got := NewLicenseFromURLs(test.input.value, test.input.urls...) + got := NewLicenseFromURLsWithContext(ctx, test.input.value, test.input.urls...) assert.Equal(t, test.expected, got) }) } diff --git a/syft/pkg/package_test.go b/syft/pkg/package_test.go index 94896d652..4c2ad5249 100644 --- a/syft/pkg/package_test.go +++ b/syft/pkg/package_test.go @@ -1,6 +1,7 @@ package pkg import ( + "context" "testing" "github.com/google/go-cmp/cmp" @@ -12,6 +13,7 @@ import ( ) func TestIDUniqueness(t *testing.T) { + ctx := context.TODO() originalLocation := file.NewVirtualLocationFromCoordinates( file.Coordinates{ RealPath: "39.0742° N, 21.8243° E", @@ -28,8 +30,8 @@ func TestIDUniqueness(t *testing.T) { originalLocation, ), Licenses: NewLicenseSet( - NewLicense("MIT"), - NewLicense("cc0-1.0"), + NewLicenseWithContext(ctx, "MIT"), + NewLicenseWithContext(ctx, "cc0-1.0"), ), Language: "math", Type: PythonPkg, @@ -82,8 +84,8 @@ func TestIDUniqueness(t *testing.T) { transform: func(pkg Package) Package { // note: same as the original package, only a different order pkg.Licenses = NewLicenseSet( - NewLicense("cc0-1.0"), - NewLicense("MIT"), + NewLicenseWithContext(ctx, "cc0-1.0"), + NewLicenseWithContext(ctx, "MIT"), ) return pkg }, @@ -110,7 +112,7 @@ func TestIDUniqueness(t *testing.T) { { name: "licenses is reflected", transform: func(pkg Package) Package { - pkg.Licenses = NewLicenseSet(NewLicense("new!")) + pkg.Licenses = NewLicenseSet(NewLicenseWithContext(ctx, "new!")) return pkg }, expectedIDComparison: assert.NotEqual, diff --git a/test/cli/license_test.go b/test/cli/license_test.go index 260e8fdeb..eec433420 100644 --- a/test/cli/license_test.go +++ b/test/cli/license_test.go @@ -20,9 +20,9 @@ func Test_Licenses(t *testing.T) { assertSuccessfulReturnCode, }, }, - + // deprecated LICENSE_INCLUDE_UNKNOWN_LICENSE_CONTENT { - name: "licenses with content", + name: "licenses with content works without deprecated LICENSE_INCLUDE_UNKNOWN_LICENSE_CONTENT", args: []string{"scan", "-o", "json", testImage, "--from", "docker-archive"}, env: map[string]string{"SYFT_FORMAT_PRETTY": "true", "SYFT_LICENSE_INCLUDE_UNKNOWN_LICENSE_CONTENT": "true"}, assertions: []traitAssertion{ @@ -31,6 +31,17 @@ func Test_Licenses(t *testing.T) { assertSuccessfulReturnCode, }, }, + // use new license content configuration + { + name: "licenses with content works with new CONTENT configuration", + args: []string{"scan", "-o", "json", testImage, "--from", "docker-archive"}, + env: map[string]string{"SYFT_FORMAT_PRETTY": "true", "SYFT_LICENSE_CONTENT": "unknown"}, + assertions: []traitAssertion{ + assertJsonReport, + assertUnknownLicenseContent(true), + assertSuccessfulReturnCode, + }, + }, } for _, test := range tests {