From 42fa9e4965fcf930f8058c50ea0fa4465c048a37 Mon Sep 17 00:00:00 2001 From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com> Date: Mon, 15 May 2023 16:23:39 -0400 Subject: [PATCH] feat: update syft license concept to complex struct (#1743) this PR makes the following changes to update the underlying license model to have more expressive capabilities it also provides some guarantee's surrounding the license values themselves - Licenses are updated from string -> pkg.LicenseSet which contain pkg.License with the following fields: - original `Value` read by syft - If it's possible to construct licenses will always have a valid SPDX expression for downstream consumption - the above is run against a generated list of SPDX license ID to try and find the correct ID - SPDX concluded vs declared is added to the new struct - URL source for license is added to the new struct - Location source is added to the new struct to show where the expression was pulled from --- go.mod | 1 + go.sum | 2 + internal/constants.go | 2 +- internal/licenses/parser.go | 26 +- internal/stringset.go | 22 +- schema/json/generate.go | 1 - schema/json/schema-8.0.0.json | 1870 +++++++++++++++++ syft/file/license.go | 32 + .../common/cyclonedxhelpers/component.go | 2 +- .../common/cyclonedxhelpers/component_test.go | 2 - .../common/cyclonedxhelpers/decoder_test.go | 3 +- .../external_references_test.go | 2 +- .../common/cyclonedxhelpers/licenses.go | 206 +- .../common/cyclonedxhelpers/licenses_test.go | 242 ++- syft/formats/common/spdxhelpers/license.go | 66 +- .../common/spdxhelpers/license_test.go | 125 +- .../common/spdxhelpers/to_format_model.go | 23 +- .../spdxhelpers/to_format_model_test.go | 34 +- .../common/spdxhelpers/to_syft_model.go | 43 +- .../TestCycloneDxDirectoryEncoder.golden | 10 +- .../snapshot/TestCycloneDxImageEncoder.golden | 16 +- .../stereoscope-fixture-image-simple.golden | Bin 15360 -> 15360 bytes syft/formats/cyclonedxxml/decoder_test.go | 5 +- .../TestCycloneDxDirectoryEncoder.golden | 6 +- .../snapshot/TestCycloneDxImageEncoder.golden | 12 +- .../stereoscope-fixture-image-simple.golden | Bin 15360 -> 15360 bytes syft/formats/internal/testutils/utils.go | 12 +- .../TestSPDXJSONDirectoryEncoder.golden | 12 +- .../snapshot/TestSPDXJSONImageEncoder.golden | 12 +- .../snapshot/TestSPDXRelationshipOrder.golden | 24 +- .../stereoscope-fixture-image-simple.golden | Bin 15360 -> 15360 bytes .../snapshot/TestSPDXJSONSPDXIDs.golden | 16 +- .../snapshot/TestSPDXRelationshipOrder.golden | 24 +- .../TestSPDXTagValueDirectoryEncoder.golden | 12 +- .../TestSPDXTagValueImageEncoder.golden | 12 +- .../stereoscope-fixture-image-simple.golden | Bin 15360 -> 15360 bytes syft/formats/syftjson/encoder_test.go | 2 +- syft/formats/syftjson/model/package.go | 41 +- syft/formats/syftjson/model/package_test.go | 105 +- .../snapshot/TestDirectoryEncoder.golden | 15 +- .../TestEncodeFullJSONDocument.golden | 15 +- .../snapshot/TestImageEncoder.golden | 31 +- .../stereoscope-fixture-image-simple.golden | Bin 15360 -> 15360 bytes syft/formats/syftjson/to_format_model.go | 26 +- syft/formats/syftjson/to_syft_model.go | 16 +- syft/license/license.go | 35 + syft/license/license_test.go | 42 + syft/pkg/alpm_metadata.go | 13 +- syft/pkg/apk_metadata.go | 1 - syft/pkg/apk_metadata_test.go | 2 - syft/pkg/catalog_test.go | 43 + syft/pkg/cataloger/alpm/cataloger_test.go | 18 +- syft/pkg/cataloger/alpm/package.go | 16 +- syft/pkg/cataloger/alpm/package_test.go | 68 +- syft/pkg/cataloger/alpm/parse_alpm_db.go | 36 +- syft/pkg/cataloger/apkdb/package.go | 12 +- syft/pkg/cataloger/apkdb/package_test.go | 81 +- syft/pkg/cataloger/apkdb/parse_apk_db.go | 19 +- syft/pkg/cataloger/apkdb/parse_apk_db_test.go | 1112 +++++----- syft/pkg/cataloger/deb/cataloger_test.go | 13 +- syft/pkg/cataloger/deb/package.go | 9 +- syft/pkg/cataloger/deb/parse_dpkg_db_test.go | 1 + syft/pkg/cataloger/golang/licenses.go | 16 +- syft/pkg/cataloger/golang/licenses_test.go | 71 +- syft/pkg/cataloger/golang/package.go | 2 +- .../cataloger/golang/parse_go_binary_test.go | 3 - syft/pkg/cataloger/golang/parse_go_mod.go | 4 +- .../pkg/cataloger/golang/parse_go_mod_test.go | 12 - .../internal/pkgtest/test_generic_parser.go | 91 + syft/pkg/cataloger/java/archive_parser.go | 4 +- .../pkg/cataloger/java/archive_parser_test.go | 12 +- .../pkg/cataloger/java/parse_java_manifest.go | 2 +- .../cataloger/javascript/cataloger_test.go | 36 +- syft/pkg/cataloger/javascript/package.go | 25 +- .../javascript/parse_package_json.go | 10 +- .../javascript/parse_package_json_test.go | 87 +- .../javascript/parse_package_lock.go | 66 +- .../javascript/parse_package_lock_test.go | 126 +- syft/pkg/cataloger/kernel/cataloger_test.go | 11 +- syft/pkg/cataloger/kernel/package.go | 17 +- .../kernel/parse_linux_kernel_file.go | 2 +- .../kernel/parse_linux_kernel_module_file.go | 2 +- syft/pkg/cataloger/php/package.go | 9 +- syft/pkg/cataloger/php/package_test.go | 29 +- syft/pkg/cataloger/php/parse_composer_lock.go | 14 +- .../cataloger/php/parse_composer_lock_test.go | 30 +- .../pkg/cataloger/php/parse_installed_json.go | 6 +- .../php/parse_installed_json_test.go | 29 +- syft/pkg/cataloger/portage/cataloger_test.go | 6 +- .../portage/parse_portage_contents.go | 7 +- syft/pkg/cataloger/python/cataloger_test.go | 75 +- syft/pkg/cataloger/python/package.go | 14 +- syft/pkg/cataloger/python/parse_wheel_egg.go | 26 +- .../python/parse_wheel_egg_metadata.go | 26 +- .../python/parse_wheel_egg_metadata_test.go | 55 +- syft/pkg/cataloger/r/cataloger_test.go | 4 +- syft/pkg/cataloger/r/package.go | 99 +- syft/pkg/cataloger/r/package_test.go | 98 +- syft/pkg/cataloger/rpm/package.go | 73 +- syft/pkg/cataloger/rpm/parse_rpm.go | 27 +- syft/pkg/cataloger/rpm/parse_rpm_db.go | 2 +- syft/pkg/cataloger/rpm/parse_rpm_db_test.go | 16 +- syft/pkg/cataloger/rpm/parse_rpm_test.go | 16 +- syft/pkg/cataloger/ruby/package.go | 8 +- syft/pkg/cataloger/ruby/parse_gemspec.go | 12 +- syft/pkg/cataloger/ruby/parse_gemspec_test.go | 15 +- .../cataloger/rust/parse_cargo_lock_test.go | 20 +- syft/pkg/cataloger/sbom/cataloger_test.go | 79 +- .../alpine/syft-json/sbom.syft.json | 2 - syft/pkg/gem_metadata.go | 1 - syft/pkg/license.go | 151 ++ syft/pkg/license_set.go | 88 + syft/pkg/license_set_test.go | 139 ++ syft/pkg/license_test.go | 99 + syft/pkg/npm_package_json_metadata.go | 15 +- syft/pkg/package.go | 4 +- syft/pkg/package_test.go | 81 +- syft/pkg/php_composer_json_metadata.go | 2 +- syft/pkg/python_package_metadata.go | 1 - syft/pkg/rpm_metadata.go | 1 - syft/sbom/sbom.go | 1 + syft/source/location.go | 6 + syft/source/location_set.go | 7 + syft/source/unindexed_directory_resolver.go | 18 +- test/integration/encode_decode_cycle_test.go | 8 +- .../integration/package_deduplication_test.go | 1 + 126 files changed, 5165 insertions(+), 1503 deletions(-) create mode 100644 schema/json/schema-8.0.0.json create mode 100644 syft/file/license.go create mode 100644 syft/license/license.go create mode 100644 syft/license/license_test.go create mode 100644 syft/pkg/license.go create mode 100644 syft/pkg/license_set.go create mode 100644 syft/pkg/license_set_test.go create mode 100644 syft/pkg/license_test.go diff --git a/go.mod b/go.mod index 8c988e1bc..83b563b73 100644 --- a/go.mod +++ b/go.mod @@ -56,6 +56,7 @@ require ( github.com/anchore/stereoscope v0.0.0-20230412183729-8602f1afc574 github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da github.com/docker/docker v23.0.6+incompatible + github.com/github/go-spdx/v2 v2.1.2 github.com/go-git/go-billy/v5 v5.4.1 github.com/go-git/go-git/v5 v5.6.1 github.com/google/go-containerregistry v0.15.1 diff --git a/go.sum b/go.sum index f7c61e853..dc8c1a634 100644 --- a/go.sum +++ b/go.sum @@ -207,6 +207,8 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro= github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/github/go-spdx/v2 v2.1.2 h1:p+Tv0yMgcuO0/vnMe9Qh4tmUgYhI6AsLVlakZ/Sx+DM= +github.com/github/go-spdx/v2 v2.1.2/go.mod h1:hMCrsFgT0QnCwn7G8gxy/MxMpy67WgZrwFeISTn0o6w= github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= diff --git a/internal/constants.go b/internal/constants.go index 177d316c3..b7be28240 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -6,5 +6,5 @@ const ( // JSONSchemaVersion is the current schema version output by the JSON encoder // This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment. - JSONSchemaVersion = "7.1.6" + JSONSchemaVersion = "8.0.0" ) diff --git a/internal/licenses/parser.go b/internal/licenses/parser.go index cd20bf0b6..b5cde28f7 100644 --- a/internal/licenses/parser.go +++ b/internal/licenses/parser.go @@ -4,7 +4,10 @@ import ( "io" "github.com/google/licensecheck" - "golang.org/x/exp/slices" + + "github.com/anchore/syft/syft/license" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" ) const ( @@ -13,21 +16,24 @@ const ( ) // Parse scans the contents of a license file to attempt to determine the type of license it is -func Parse(reader io.Reader) (licenses []string, err error) { +func Parse(reader io.Reader, l source.Location) (licenses []pkg.License, err error) { + licenses = make([]pkg.License, 0) contents, err := io.ReadAll(reader) if err != nil { return nil, err } cov := licensecheck.Scan(contents) + if cov.Percent < coverageThreshold { + // unknown or no licenses here? + return licenses, nil + } - if cov.Percent < float64(coverageThreshold) { - licenses = append(licenses, unknownLicenseType) - } for _, m := range cov.Match { - if slices.Contains(licenses, m.ID) { - continue - } - licenses = append(licenses, m.ID) + lic := pkg.NewLicenseFromLocations(m.ID, l) + lic.Type = license.Concluded + + licenses = append(licenses, lic) } - return + + return licenses, nil } diff --git a/internal/stringset.go b/internal/stringset.go index 536e0a8a7..1d44652ec 100644 --- a/internal/stringset.go +++ b/internal/stringset.go @@ -15,8 +15,10 @@ func NewStringSet(start ...string) StringSet { } // Add a string to the set. -func (s StringSet) Add(i string) { - s[i] = struct{}{} +func (s StringSet) Add(i ...string) { + for _, str := range i { + s[str] = struct{}{} + } } // Remove a string from the set. @@ -41,3 +43,19 @@ func (s StringSet) ToSlice() []string { sort.Strings(ret) return ret } + +func (s StringSet) Equals(o StringSet) bool { + if len(s) != len(o) { + return false + } + for k := range s { + if !o.Contains(k) { + return false + } + } + return true +} + +func (s StringSet) Empty() bool { + return len(s) < 1 +} diff --git a/schema/json/generate.go b/schema/json/generate.go index e81cff020..169e3c22f 100644 --- a/schema/json/generate.go +++ b/schema/json/generate.go @@ -78,7 +78,6 @@ func build() *jsonschema.Schema { } documentSchema := reflector.ReflectFromType(reflect.TypeOf(&syftjsonModel.Document{})) metadataSchema := reflector.ReflectFromType(reflect.TypeOf(&artifactMetadataContainer{})) - // TODO: inject source definitions // inject the definitions of all metadatas into the schema definitions diff --git a/schema/json/schema-8.0.0.json b/schema/json/schema-8.0.0.json new file mode 100644 index 000000000..74157826c --- /dev/null +++ b/schema/json/schema-8.0.0.json @@ -0,0 +1,1870 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/anchore/syft/syft/formats/syftjson/model/document", + "$ref": "#/$defs/Document", + "$defs": { + "AlpmFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "gid": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "size": { + "type": "string" + }, + "link": { + "type": "string" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object" + }, + "AlpmMetadata": { + "properties": { + "basepackage": { + "type": "string" + }, + "package": { + "type": "string" + }, + "version": { + "type": "string" + }, + "description": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "packager": { + "type": "string" + }, + "url": { + "type": "string" + }, + "validation": { + "type": "string" + }, + "reason": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + }, + "backup": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "url", + "validation", + "reason", + "files", + "backup" + ] + }, + "ApkFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "ownerUid": { + "type": "string" + }, + "ownerGid": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "ApkMetadata": { + "properties": { + "package": { + "type": "string" + }, + "originPackage": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "version": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "installedSize": { + "type": "integer" + }, + "pullDependencies": { + "items": { + "type": "string" + }, + "type": "array" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "pullChecksum": { + "type": "string" + }, + "gitCommitOfApkPort": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/ApkFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "provides", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ] + }, + "BinaryMetadata": { + "properties": { + "matches": { + "items": { + "$ref": "#/$defs/ClassifierMatch" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "matches" + ] + }, + "CargoPackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "type": "string" + }, + "checksum": { + "type": "string" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ] + }, + "ClassifierMatch": { + "properties": { + "classifier": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Location" + } + }, + "type": "object", + "required": [ + "classifier", + "location" + ] + }, + "CocoapodsMetadata": { + "properties": { + "checksum": { + "type": "string" + } + }, + "type": "object", + "required": [ + "checksum" + ] + }, + "ConanLockMetadata": { + "properties": { + "ref": { + "type": "string" + }, + "package_id": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "requires": { + "type": "string" + }, + "build_requires": { + "type": "string" + }, + "py_requires": { + "type": "string" + }, + "options": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "path": { + "type": "string" + }, + "context": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "ConanMetadata": { + "properties": { + "ref": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "Coordinates": { + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "DartPubMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "hosted_url": { + "type": "string" + }, + "vcs_url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "Descriptor": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "configuration": true + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "Digest": { + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ] + }, + "Document": { + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/Package" + }, + "type": "array" + }, + "artifactRelationships": { + "items": { + "$ref": "#/$defs/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/File" + }, + "type": "array" + }, + "secrets": { + "items": { + "$ref": "#/$defs/Secrets" + }, + "type": "array" + }, + "source": { + "$ref": "#/$defs/Source" + }, + "distro": { + "$ref": "#/$defs/LinuxRelease" + }, + "descriptor": { + "$ref": "#/$defs/Descriptor" + }, + "schema": { + "$ref": "#/$defs/Schema" + } + }, + "type": "object", + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ] + }, + "DotnetDepsMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "path": { + "type": "string" + }, + "sha512": { + "type": "string" + }, + "hashPath": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ] + }, + "DpkgFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + }, + "isConfigFile": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "path", + "isConfigFile" + ] + }, + "DpkgMetadata": { + "properties": { + "package": { + "type": "string" + }, + "source": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "installedSize": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ] + }, + "File": { + "properties": { + "id": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Coordinates" + }, + "metadata": { + "$ref": "#/$defs/FileMetadataEntry" + }, + "contents": { + "type": "string" + }, + "digests": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "id", + "location" + ] + }, + "FileMetadataEntry": { + "properties": { + "mode": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "linkDestination": { + "type": "string" + }, + "userID": { + "type": "integer" + }, + "groupID": { + "type": "integer" + }, + "mimeType": { + "type": "string" + }, + "size": { + "type": "integer" + } + }, + "type": "object", + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType", + "size" + ] + }, + "GemMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "homepage": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "GolangBinMetadata": { + "properties": { + "goBuildSettings": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "goCompiledVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "h1Digest": { + "type": "string" + }, + "mainModule": { + "type": "string" + } + }, + "type": "object", + "required": [ + "goCompiledVersion", + "architecture" + ] + }, + "GolangModMetadata": { + "properties": { + "h1Digest": { + "type": "string" + } + }, + "type": "object" + }, + "HackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "snapshotURL": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "IDLikes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "JavaManifest": { + "properties": { + "main": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "namedSections": { + "patternProperties": { + ".*": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "JavaMetadata": { + "properties": { + "virtualPath": { + "type": "string" + }, + "manifest": { + "$ref": "#/$defs/JavaManifest" + }, + "pomProperties": { + "$ref": "#/$defs/PomProperties" + }, + "pomProject": { + "$ref": "#/$defs/PomProject" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "virtualPath" + ] + }, + "KbPackageMetadata": { + "properties": { + "product_id": { + "type": "string" + }, + "kb": { + "type": "string" + } + }, + "type": "object", + "required": [ + "product_id", + "kb" + ] + }, + "License": { + "properties": { + "value": { + "type": "string" + }, + "spdxExpression": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "items": { + "type": "string" + }, + "type": "array" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type", + "url", + "locations" + ] + }, + "LinuxKernelMetadata": { + "properties": { + "name": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "version": { + "type": "string" + }, + "extendedVersion": { + "type": "string" + }, + "buildTime": { + "type": "string" + }, + "author": { + "type": "string" + }, + "format": { + "type": "string" + }, + "rwRootFS": { + "type": "boolean" + }, + "swapDevice": { + "type": "integer" + }, + "rootDevice": { + "type": "integer" + }, + "videoMode": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "architecture", + "version" + ] + }, + "LinuxKernelModuleMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "license": { + "type": "string" + }, + "kernelVersion": { + "type": "string" + }, + "versionMagic": { + "type": "string" + }, + "parameters": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/LinuxKernelModuleParameter" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "LinuxKernelModuleParameter": { + "properties": { + "type": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "type": "object" + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "idLike": { + "$ref": "#/$defs/IDLikes" + }, + "version": { + "type": "string" + }, + "versionID": { + "type": "string" + }, + "versionCodename": { + "type": "string" + }, + "buildID": { + "type": "string" + }, + "imageID": { + "type": "string" + }, + "imageVersion": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "variantID": { + "type": "string" + }, + "homeURL": { + "type": "string" + }, + "supportURL": { + "type": "string" + }, + "bugReportURL": { + "type": "string" + }, + "privacyPolicyURL": { + "type": "string" + }, + "cpeName": { + "type": "string" + }, + "supportEnd": { + "type": "string" + } + }, + "type": "object" + }, + "Location": { + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + }, + "annotations": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "MixLockMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "NixStoreMetadata": { + "properties": { + "outputHash": { + "type": "string" + }, + "output": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "outputHash", + "files" + ] + }, + "NpmPackageJSONMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "private": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "homepage", + "description", + "url", + "private" + ] + }, + "NpmPackageLockJSONMetadata": { + "properties": { + "resolved": { + "type": "string" + }, + "integrity": { + "type": "string" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity" + ] + }, + "Package": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + }, + "licenses": { + "$ref": "#/$defs/licenses" + }, + "language": { + "type": "string" + }, + "cpes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/AlpmMetadata" + }, + { + "$ref": "#/$defs/ApkMetadata" + }, + { + "$ref": "#/$defs/BinaryMetadata" + }, + { + "$ref": "#/$defs/CargoPackageMetadata" + }, + { + "$ref": "#/$defs/CocoapodsMetadata" + }, + { + "$ref": "#/$defs/ConanLockMetadata" + }, + { + "$ref": "#/$defs/ConanMetadata" + }, + { + "$ref": "#/$defs/DartPubMetadata" + }, + { + "$ref": "#/$defs/DotnetDepsMetadata" + }, + { + "$ref": "#/$defs/DpkgMetadata" + }, + { + "$ref": "#/$defs/GemMetadata" + }, + { + "$ref": "#/$defs/GolangBinMetadata" + }, + { + "$ref": "#/$defs/GolangModMetadata" + }, + { + "$ref": "#/$defs/HackageMetadata" + }, + { + "$ref": "#/$defs/JavaMetadata" + }, + { + "$ref": "#/$defs/KbPackageMetadata" + }, + { + "$ref": "#/$defs/LinuxKernelMetadata" + }, + { + "$ref": "#/$defs/LinuxKernelModuleMetadata" + }, + { + "$ref": "#/$defs/MixLockMetadata" + }, + { + "$ref": "#/$defs/NixStoreMetadata" + }, + { + "$ref": "#/$defs/NpmPackageJSONMetadata" + }, + { + "$ref": "#/$defs/NpmPackageLockJSONMetadata" + }, + { + "$ref": "#/$defs/PhpComposerJSONMetadata" + }, + { + "$ref": "#/$defs/PortageMetadata" + }, + { + "$ref": "#/$defs/PythonPackageMetadata" + }, + { + "$ref": "#/$defs/PythonPipfileLockMetadata" + }, + { + "$ref": "#/$defs/PythonRequirementsMetadata" + }, + { + "$ref": "#/$defs/RDescriptionFileMetadata" + }, + { + "$ref": "#/$defs/RebarLockMetadata" + }, + { + "$ref": "#/$defs/RpmMetadata" + } + ] + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ] + }, + "PhpComposerAuthors": { + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "homepage": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name" + ] + }, + "PhpComposerExternalReference": { + "properties": { + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "shasum": { + "type": "string" + } + }, + "type": "object", + "required": [ + "type", + "url", + "reference" + ] + }, + "PhpComposerJSONMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "require": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "provide": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "require-dev": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "suggest": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "type": "string" + }, + "notification-url": { + "type": "string" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array" + }, + "time": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ] + }, + "PomParent": { + "properties": { + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object", + "required": [ + "groupId", + "artifactId", + "version" + ] + }, + "PomProject": { + "properties": { + "path": { + "type": "string" + }, + "parent": { + "$ref": "#/$defs/PomParent" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ] + }, + "PomProperties": { + "properties": { + "path": { + "type": "string" + }, + "name": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "extraFields": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version" + ] + }, + "PortageFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "PortageMetadata": { + "properties": { + "installedSize": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/PortageFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "installedSize", + "files" + ] + }, + "PythonDirectURLOriginInfo": { + "properties": { + "url": { + "type": "string" + }, + "commitId": { + "type": "string" + }, + "vcs": { + "type": "string" + } + }, + "type": "object", + "required": [ + "url" + ] + }, + "PythonFileDigest": { + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ] + }, + "PythonFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest" + }, + "size": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "PythonPackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array" + }, + "sitePackagesRootPath": { + "type": "string" + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "directUrlOrigin": { + "$ref": "#/$defs/PythonDirectURLOriginInfo" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ] + }, + "PythonPipfileLockMetadata": { + "properties": { + "hashes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "index": { + "type": "string" + } + }, + "type": "object", + "required": [ + "hashes", + "index" + ] + }, + "PythonRequirementsMetadata": { + "properties": { + "name": { + "type": "string" + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array" + }, + "versionConstraint": { + "type": "string" + }, + "url": { + "type": "string" + }, + "markers": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "name", + "extras", + "versionConstraint", + "url", + "markers" + ] + }, + "RDescriptionFileMetadata": { + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "url": { + "items": { + "type": "string" + }, + "type": "array" + }, + "repository": { + "type": "string" + }, + "built": { + "type": "string" + }, + "needsCompilation": { + "type": "boolean" + }, + "imports": { + "items": { + "type": "string" + }, + "type": "array" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array" + }, + "suggests": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "RebarLockMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "Relationship": { + "properties": { + "parent": { + "type": "string" + }, + "child": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "parent", + "child", + "type" + ] + }, + "RpmMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "epoch": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string" + }, + "release": { + "type": "string" + }, + "sourceRpm": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "vendor": { + "type": "string" + }, + "modularityLabel": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/RpmdbFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "modularityLabel", + "files" + ] + }, + "RpmdbFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "mode": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "digest": { + "$ref": "#/$defs/Digest" + }, + "userName": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "flags": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ] + }, + "Schema": { + "properties": { + "version": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "version", + "url" + ] + }, + "SearchResult": { + "properties": { + "classification": { + "type": "string" + }, + "lineNumber": { + "type": "integer" + }, + "lineOffset": { + "type": "integer" + }, + "seekPosition": { + "type": "integer" + }, + "length": { + "type": "integer" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "classification", + "lineNumber", + "lineOffset", + "seekPosition", + "length" + ] + }, + "Secrets": { + "properties": { + "location": { + "$ref": "#/$defs/Coordinates" + }, + "secrets": { + "items": { + "$ref": "#/$defs/SearchResult" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "location", + "secrets" + ] + }, + "Source": { + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "target": true + }, + "type": "object", + "required": [ + "id", + "type", + "target" + ] + }, + "licenses": { + "items": { + "$ref": "#/$defs/License" + }, + "type": "array" + } + } +} diff --git a/syft/file/license.go b/syft/file/license.go new file mode 100644 index 000000000..12d487d54 --- /dev/null +++ b/syft/file/license.go @@ -0,0 +1,32 @@ +package file + +import ( + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/license" +) + +type License struct { + Value string + SPDXExpression string + Type license.Type + LicenseEvidence *LicenseEvidence // evidence from license classifier +} + +type LicenseEvidence struct { + Confidence int + Offset int + Extent int +} + +func NewLicense(value string) License { + spdxExpression, err := license.ParseExpression(value) + if err != nil { + log.Trace("unable to parse license expression: %s, %w", value, err) + } + + return License{ + Value: value, + SPDXExpression: spdxExpression, + Type: license.Concluded, + } +} diff --git a/syft/formats/common/cyclonedxhelpers/component.go b/syft/formats/common/cyclonedxhelpers/component.go index cf3516ced..e51c9d112 100644 --- a/syft/formats/common/cyclonedxhelpers/component.go +++ b/syft/formats/common/cyclonedxhelpers/component.go @@ -78,7 +78,7 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package { Name: c.Name, Version: c.Version, Locations: decodeLocations(values), - Licenses: decodeLicenses(c), + Licenses: pkg.NewLicenseSet(decodeLicenses(c)...), CPEs: decodeCPEs(c), PURL: c.PackageURL, } diff --git a/syft/formats/common/cyclonedxhelpers/component_test.go b/syft/formats/common/cyclonedxhelpers/component_test.go index 248d8ca46..4ee69aa0c 100644 --- a/syft/formats/common/cyclonedxhelpers/component_test.go +++ b/syft/formats/common/cyclonedxhelpers/component_test.go @@ -36,7 +36,6 @@ func Test_encodeComponentProperties(t *testing.T) { OriginPackage: "libc-dev", Maintainer: "Natanael Copa ", Version: "0.7.2-r0", - License: "BSD", Architecture: "x86_64", URL: "http://alpinelinux.org", Description: "Meta package to pull in correct libc", @@ -140,7 +139,6 @@ func Test_encodeComponentProperties(t *testing.T) { Version: "0.9.2", SourceRpm: "dive-0.9.2-1.src.rpm", Size: 12406784, - License: "MIT", Vendor: "", Files: []pkg.RpmdbFileRecord{}, }, diff --git a/syft/formats/common/cyclonedxhelpers/decoder_test.go b/syft/formats/common/cyclonedxhelpers/decoder_test.go index 0014988f0..7559dc02d 100644 --- a/syft/formats/common/cyclonedxhelpers/decoder_test.go +++ b/syft/formats/common/cyclonedxhelpers/decoder_test.go @@ -322,8 +322,7 @@ func Test_missingDataDecode(t *testing.T) { }, }, }) - - assert.Len(t, pkg.Licenses, 0) + assert.Equal(t, pkg.Licenses.Empty(), true) } func Test_missingComponentsDecode(t *testing.T) { diff --git a/syft/formats/common/cyclonedxhelpers/external_references_test.go b/syft/formats/common/cyclonedxhelpers/external_references_test.go index 0dd879557..c6ce0355b 100644 --- a/syft/formats/common/cyclonedxhelpers/external_references_test.go +++ b/syft/formats/common/cyclonedxhelpers/external_references_test.go @@ -50,7 +50,7 @@ func Test_encodeExternalReferences(t *testing.T) { Language: pkg.Rust, Type: pkg.RustPkg, MetadataType: pkg.RustCargoPackageMetadataType, - Licenses: nil, + Licenses: pkg.NewLicenseSet(), Metadata: pkg.CargoPackageMetadata{ Name: "ansi_term", Version: "0.12.1", diff --git a/syft/formats/common/cyclonedxhelpers/licenses.go b/syft/formats/common/cyclonedxhelpers/licenses.go index ed2580fd1..a2e0772bf 100644 --- a/syft/formats/common/cyclonedxhelpers/licenses.go +++ b/syft/formats/common/cyclonedxhelpers/licenses.go @@ -1,53 +1,205 @@ package cyclonedxhelpers import ( + "fmt" + "strings" + "github.com/CycloneDX/cyclonedx-go" "github.com/anchore/syft/internal/spdxlicense" "github.com/anchore/syft/syft/pkg" ) +// This should be a function that just surfaces licenses already validated in the package struct func encodeLicenses(p pkg.Package) *cyclonedx.Licenses { - lc := cyclonedx.Licenses{} - for _, licenseName := range p.Licenses { - if value, exists := spdxlicense.ID(licenseName); exists { - lc = append(lc, cyclonedx.LicenseChoice{ + spdxc, otherc, ex := separateLicenses(p) + if len(otherc) > 0 { + // found non spdx related licenses + // build individual license choices for each + // complex expressions are not combined and set as NAME fields + for _, e := range ex { + otherc = append(otherc, cyclonedx.LicenseChoice{ + License: &cyclonedx.License{ + Name: e, + }, + }) + } + otherc = append(otherc, spdxc...) + return &otherc + } + + if len(spdxc) > 0 { + for _, l := range ex { + spdxc = append(spdxc, cyclonedx.LicenseChoice{ + License: &cyclonedx.License{ + Name: l, + }, + }) + } + return &spdxc + } + + if len(ex) > 0 { + // only expressions found + var expressions cyclonedx.Licenses + expressions = append(expressions, cyclonedx.LicenseChoice{ + Expression: mergeSPDX(ex), + }) + return &expressions + } + + return nil +} + +func decodeLicenses(c *cyclonedx.Component) []pkg.License { + licenses := make([]pkg.License, 0) + if c == nil || c.Licenses == nil { + return licenses + } + + for _, l := range *c.Licenses { + if l.License == nil { + continue + } + // these fields are mutually exclusive in the spec + switch { + case l.License.ID != "": + licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.ID, l.License.URL)) + case l.License.Name != "": + licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.Name, l.License.URL)) + case l.Expression != "": + licenses = append(licenses, pkg.NewLicenseFromURLs(l.Expression, l.License.URL)) + default: + } + } + + return licenses +} + +// nolint:funlen +func separateLicenses(p pkg.Package) (spdx, other cyclonedx.Licenses, expressions []string) { + ex := make([]string, 0) + spdxc := cyclonedx.Licenses{} + otherc := cyclonedx.Licenses{} + /* + pkg.License can be a couple of things: see above declarations + - Complex SPDX expression + - Some other Valid license ID + - Some non-standard non spdx license + + To determine if an expression is a singular ID we first run it against the SPDX license list. + + The weird case we run into is if there is a package with a license that is not a valid SPDX expression + and a license that is a valid complex expression. In this case we will surface the valid complex expression + as a license choice and the invalid expression as a license string. + + */ + seen := make(map[string]bool) + for _, l := range p.Licenses.ToSlice() { + // singular expression case + // only ID field here since we guarantee that the license is valid + if value, exists := spdxlicense.ID(l.SPDXExpression); exists { + if !l.URL.Empty() { + processLicenseURLs(l, value, &spdxc) + continue + } + + if _, exists := seen[value]; exists { + continue + } + // try making set of license choices to avoid duplicates + // only update if the license has more information + spdxc = append(spdxc, cyclonedx.LicenseChoice{ License: &cyclonedx.License{ ID: value, }, }) + seen[value] = true + // we have added the license to the SPDX license list check next license continue } - // not found so append the licenseName as is - lc = append(lc, cyclonedx.LicenseChoice{ + if l.SPDXExpression != "" { + // COMPLEX EXPRESSION CASE + ex = append(ex, l.SPDXExpression) + continue + } + + // license string that are not valid spdx expressions or ids + // we only use license Name here since we cannot guarantee that the license is a valid SPDX expression + if !l.URL.Empty() { + processLicenseURLs(l, "", &otherc) + continue + } + otherc = append(otherc, cyclonedx.LicenseChoice{ License: &cyclonedx.License{ - Name: licenseName, + Name: l.Value, }, }) } - if len(lc) > 0 { - return &lc - } - return nil + return spdxc, otherc, ex } -func decodeLicenses(c *cyclonedx.Component) (out []string) { - if c.Licenses != nil { - for _, l := range *c.Licenses { - if l.License != nil { - var lic string - switch { - case l.License.ID != "": - lic = l.License.ID - case l.License.Name != "": - lic = l.License.Name - default: - continue - } - out = append(out, lic) - } +func processLicenseURLs(l pkg.License, spdxID string, populate *cyclonedx.Licenses) { + for _, url := range l.URL.ToSlice() { + if spdxID == "" { + *populate = append(*populate, cyclonedx.LicenseChoice{ + License: &cyclonedx.License{ + URL: url, + Name: l.Value, + }, + }) + } else { + *populate = append(*populate, cyclonedx.LicenseChoice{ + License: &cyclonedx.License{ + ID: spdxID, + URL: url, + }, + }) } } - return +} + +func mergeSPDX(ex []string) string { + var candidate []string + for _, e := range ex { + // if the expression does not have balanced parens add them + if !strings.HasPrefix(e, "(") && !strings.HasSuffix(e, ")") { + e = "(" + e + ")" + candidate = append(candidate, e) + } + } + + if len(candidate) == 1 { + return reduceOuter(strings.Join(candidate, " AND ")) + } + + return strings.Join(candidate, " AND ") +} + +func reduceOuter(expression string) string { + var ( + sb strings.Builder + openCount int + ) + + for _, c := range expression { + if string(c) == "(" && openCount > 0 { + fmt.Fprintf(&sb, "%c", c) + } + if string(c) == "(" { + openCount++ + continue + } + if string(c) == ")" && openCount > 1 { + fmt.Fprintf(&sb, "%c", c) + } + if string(c) == ")" { + openCount-- + continue + } + fmt.Fprintf(&sb, "%c", c) + } + + return sb.String() } diff --git a/syft/formats/common/cyclonedxhelpers/licenses_test.go b/syft/formats/common/cyclonedxhelpers/licenses_test.go index 64999e60c..b3a390abc 100644 --- a/syft/formats/common/cyclonedxhelpers/licenses_test.go +++ b/syft/formats/common/cyclonedxhelpers/licenses_test.go @@ -6,6 +6,8 @@ import ( "github.com/CycloneDX/cyclonedx-go" "github.com/stretchr/testify/assert" + "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft/license" "github.com/anchore/syft/syft/pkg" ) @@ -23,60 +25,170 @@ func Test_encodeLicense(t *testing.T) { { name: "no SPDX licenses", input: pkg.Package{ - Licenses: []string{ - "made-up", - }, + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("RandomLicense"), + ), }, expected: &cyclonedx.Licenses{ - {License: &cyclonedx.License{Name: "made-up"}}, + { + License: &cyclonedx.License{ + Name: "RandomLicense", + }, + }, }, }, { - name: "with SPDX license", + name: "single SPDX ID and Non SPDX ID", input: pkg.Package{ - Licenses: []string{ - "MIT", - }, + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("mit"), + pkg.NewLicense("FOOBAR"), + ), }, expected: &cyclonedx.Licenses{ - {License: &cyclonedx.License{ID: "MIT"}}, + { + License: &cyclonedx.License{ + Name: "FOOBAR", + }, + }, + { + License: &cyclonedx.License{ + ID: "MIT", + }, + }, }, }, { - name: "with SPDX license expression", + name: "with complex SPDX license expression", input: pkg.Package{ - Licenses: []string{ - "MIT", - "GPL-3.0", - }, + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("MIT AND GPL-3.0-only"), + ), }, expected: &cyclonedx.Licenses{ - {License: &cyclonedx.License{ID: "MIT"}}, - {License: &cyclonedx.License{ID: "GPL-3.0-only"}}, + { + Expression: "MIT AND GPL-3.0-only", + }, }, }, { - name: "cap insensitive", + name: "with multiple complex SPDX license expression", input: pkg.Package{ - Licenses: []string{ - "gpl-3.0", - }, + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("MIT AND GPL-3.0-only"), + pkg.NewLicense("MIT AND GPL-3.0-only WITH Classpath-exception-2.0"), + ), }, expected: &cyclonedx.Licenses{ - {License: &cyclonedx.License{ID: "GPL-3.0-only"}}, + { + Expression: "(MIT AND GPL-3.0-only) AND (MIT AND GPL-3.0-only WITH Classpath-exception-2.0)", + }, }, }, { - name: "debian to spdx conversion", + name: "with multiple URLs and expressions", input: pkg.Package{ - Licenses: []string{ - "GPL-2", - }, + 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"), + ), }, expected: &cyclonedx.Licenses{ - {License: &cyclonedx.License{ID: "GPL-2.0-only"}}, + { + License: &cyclonedx.License{ + Name: "FakeLicense", + URL: "htts://someurl.com", + }, + }, + { + License: &cyclonedx.License{ + Name: "MIT AND GPL-3.0-only", + }, + }, + { + License: &cyclonedx.License{ + ID: "MIT", + URL: "https://opensource.org/licenses/MIT", + }, + }, + { + License: &cyclonedx.License{ + ID: "MIT", + URL: "https://spdx.org/licenses/MIT.html", + }, + }, }, }, + { + name: "with multiple values licenses are deduplicated", + input: pkg.Package{ + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("Apache-2"), + pkg.NewLicense("Apache-2.0"), + ), + }, + expected: &cyclonedx.Licenses{ + { + License: &cyclonedx.License{ + ID: "Apache-2.0", + }, + }, + }, + }, + { + name: "with multiple URLs and single with no URL", + 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"), + ), + }, + expected: &cyclonedx.Licenses{ + { + License: &cyclonedx.License{ + ID: "MIT", + URL: "https://opensource.org/licenses/MIT", + }, + }, + { + License: &cyclonedx.License{ + ID: "MIT", + URL: "https://spdx.org/licenses/MIT.html", + }, + }, + { + License: &cyclonedx.License{ + Name: "MIT AND GPL-3.0-only", + }, + }, + }, + }, + // TODO: do we drop the non SPDX ID license and do a single expression + // OR do we keep the non SPDX ID license and do multiple licenses where the complex + // expressions are set as the NAME field? + //{ + // name: "with multiple complex SPDX license expression and a non spdx id", + // input: pkg.Package{ + // Licenses: []pkg.License{ + // { + // SPDXExpression: "MIT AND GPL-3.0-only", + // }, + // { + // SPDXExpression: "MIT AND GPL-3.0-only WITH Classpath-exception-2.0", + // }, + // { + // Value: "FOOBAR", + // }, + // }, + // }, + // expected: &cyclonedx.Licenses{ + // { + // Expression: "(MIT AND GPL-3.0-only) AND (MIT AND GPL-3.0-only WITH Classpath-exception-2.0)", + // }, + // }, + //}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -84,3 +196,81 @@ func Test_encodeLicense(t *testing.T) { }) } } + +func TestDecodeLicenses(t *testing.T) { + tests := []struct { + name string + input *cyclonedx.Component + expected []pkg.License + }{ + { + name: "no licenses", + input: &cyclonedx.Component{}, + expected: []pkg.License{}, + }, + { + name: "no SPDX license ID or expression", + input: &cyclonedx.Component{ + Licenses: &cyclonedx.Licenses{ + { + License: &cyclonedx.License{ + Name: "RandomLicense", + }, + }, + }, + }, + expected: []pkg.License{ + { + Value: "RandomLicense", + // CycloneDX specification doesn't give a field for determining the license type + Type: license.Declared, + URL: internal.NewStringSet(), + }, + }, + }, + { + name: "with SPDX license ID", + input: &cyclonedx.Component{ + Licenses: &cyclonedx.Licenses{ + { + License: &cyclonedx.License{ + ID: "MIT", + }, + }, + }, + }, + expected: []pkg.License{ + { + Value: "MIT", + SPDXExpression: "MIT", + Type: license.Declared, + URL: internal.NewStringSet(), + }, + }, + }, + { + name: "with complex SPDX license expression", + input: &cyclonedx.Component{ + Licenses: &cyclonedx.Licenses{ + { + License: &cyclonedx.License{}, + Expression: "MIT AND GPL-3.0-only WITH Classpath-exception-2.0", + }, + }, + }, + expected: []pkg.License{ + { + Value: "MIT AND GPL-3.0-only WITH Classpath-exception-2.0", + SPDXExpression: "MIT AND GPL-3.0-only WITH Classpath-exception-2.0", + Type: license.Declared, + URL: internal.NewStringSet(), + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, decodeLicenses(test.input)) + }) + } +} diff --git a/syft/formats/common/spdxhelpers/license.go b/syft/formats/common/spdxhelpers/license.go index 9494d3b07..4b0d896a1 100644 --- a/syft/formats/common/spdxhelpers/license.go +++ b/syft/formats/common/spdxhelpers/license.go @@ -4,10 +4,11 @@ import ( "strings" "github.com/anchore/syft/internal/spdxlicense" + "github.com/anchore/syft/syft/license" "github.com/anchore/syft/syft/pkg" ) -func License(p pkg.Package) string { +func License(p pkg.Package) (concluded, declared string) { // source: https://spdx.github.io/spdx-spec/3-package-information/#313-concluded-license // The options to populate this field are limited to: // A valid SPDX License Expression as defined in Appendix IV; @@ -17,35 +18,70 @@ func License(p pkg.Package) string { // (ii) the SPDX file creator has made no attempt to determine this field; or // (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so). - if len(p.Licenses) == 0 { - return NONE + if p.Licenses.Empty() { + return NOASSERTION, NOASSERTION } - // take all licenses and assume an AND expression; for information about license expressions see https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/ - parsedLicenses := parseLicenses(p.Licenses) + // 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()) - for i, v := range parsedLicenses { + for i, v := range pc { if strings.HasPrefix(v, spdxlicense.LicenseRefPrefix) { - parsedLicenses[i] = SanitizeElementID(v) + pc[i] = SanitizeElementID(v) } } - if len(parsedLicenses) == 0 { + for i, v := range pd { + if strings.HasPrefix(v, spdxlicense.LicenseRefPrefix) { + pd[i] = SanitizeElementID(v) + } + } + + return joinLicenses(pc), joinLicenses(pd) +} + +func joinLicenses(licenses []string) string { + if len(licenses) == 0 { return NOASSERTION } - return strings.Join(parsedLicenses, " AND ") + var newLicenses []string + + for _, v := range licenses { + // check if license does not start or end with parens + if !strings.HasPrefix(v, "(") && !strings.HasSuffix(v, ")") { + // if license contains AND, OR, or WITH, then wrap in parens + if strings.Contains(v, " AND ") || + strings.Contains(v, " OR ") || + strings.Contains(v, " WITH ") { + newLicenses = append(newLicenses, "("+v+")") + continue + } + } + newLicenses = append(newLicenses, v) + } + + return strings.Join(newLicenses, " AND ") } -func parseLicenses(raw []string) (parsedLicenses []string) { +func parseLicenses(raw []pkg.License) (concluded, declared []string) { for _, l := range raw { - if value, exists := spdxlicense.ID(l); exists { - parsedLicenses = append(parsedLicenses, value) + var candidate string + if l.SPDXExpression != "" { + candidate = l.SPDXExpression } else { // we did not find a valid SPDX license ID so treat as separate license - otherLicense := spdxlicense.LicenseRefPrefix + l - parsedLicenses = append(parsedLicenses, otherLicense) + candidate = spdxlicense.LicenseRefPrefix + l.Value + } + + switch l.Type { + case license.Concluded: + concluded = append(concluded, candidate) + case license.Declared: + declared = append(declared, candidate) } } - return + return concluded, declared } diff --git a/syft/formats/common/spdxhelpers/license_test.go b/syft/formats/common/spdxhelpers/license_test.go index 980c0f0dd..cb4e614e4 100644 --- a/syft/formats/common/spdxhelpers/license_test.go +++ b/syft/formats/common/spdxhelpers/license_test.go @@ -9,77 +9,120 @@ import ( ) func Test_License(t *testing.T) { + type expected struct { + concluded string + declared string + } tests := []struct { name string input pkg.Package - expected string + expected expected }{ { - name: "no licenses", - input: pkg.Package{}, - expected: NONE, + name: "no licenses", + input: pkg.Package{}, + expected: expected{ + concluded: "NOASSERTION", + declared: "NOASSERTION", + }, }, { name: "no SPDX licenses", input: pkg.Package{ - Licenses: []string{ - "made-up", - }, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("made-up")), + }, + expected: expected{ + concluded: "NOASSERTION", + declared: "LicenseRef-made-up", }, - expected: "LicenseRef-made-up", }, { name: "with SPDX license", input: pkg.Package{ - Licenses: []string{ - "MIT", - }, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")), + }, + expected: struct { + concluded string + declared string + }{ + concluded: "NOASSERTION", + declared: "MIT", }, - expected: "MIT", }, { name: "with SPDX license expression", input: pkg.Package{ - Licenses: []string{ - "MIT", - "GPL-3.0", - }, + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("MIT"), + pkg.NewLicense("GPL-3.0-only"), + ), }, - expected: "MIT AND GPL-3.0-only", - }, - { - name: "cap insensitive", - input: pkg.Package{ - Licenses: []string{ - "gpl-3.0", - }, + expected: expected{ + concluded: "NOASSERTION", + // because we sort licenses alphabetically GPL ends up at the start + declared: "GPL-3.0-only AND MIT", }, - expected: "GPL-3.0-only", - }, - { - name: "debian to spdx conversion", - input: pkg.Package{ - Licenses: []string{ - "GPL-2", - }, - }, - expected: "GPL-2.0-only", }, { name: "includes valid LicenseRef-", input: pkg.Package{ - Licenses: []string{ - "one thing first", - "two things/#$^second", - "MIT", - }, + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("one thing first"), + pkg.NewLicense("two things/#$^second"), + pkg.NewLicense("MIT"), + ), + }, + expected: expected{ + concluded: "NOASSERTION", + // because we separate licenses between valid SPDX and non valid, valid ID always end at the front + declared: "MIT AND LicenseRef-one-thing-first AND LicenseRef-two-things----second", + }, + }, + { + 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"), + ), + }, + expected: expected{ + concluded: "NOASSERTION", + // because we separate licenses between valid SPDX and non valid, valid ID always end at the front + declared: "(MIT AND GPL-3.0-only) AND (MIT OR APACHE-2.0) AND LicenseRef-one-thing-first", }, - expected: "LicenseRef-one-thing-first AND LicenseRef-two-things----second AND MIT", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, License(test.input)) + c, d := License(test.input) + assert.Equal(t, test.expected.concluded, c) + assert.Equal(t, test.expected.declared, d) + }) + } +} + +func Test_joinLicenses(t *testing.T) { + tests := []struct { + name string + args []string + want string + }{ + { + name: "multiple licenses", + args: []string{"MIT", "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"}, + want: "(MIT AND Apache) AND GPL-3.0-only", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, joinLicenses(tt.args), "joinLicenses(%v)", tt.args) }) } } diff --git a/syft/formats/common/spdxhelpers/to_format_model.go b/syft/formats/common/spdxhelpers/to_format_model.go index a6877bcaa..4c39dbe3b 100644 --- a/syft/formats/common/spdxhelpers/to_format_model.go +++ b/syft/formats/common/spdxhelpers/to_format_model.go @@ -170,7 +170,8 @@ func toPackages(catalog *pkg.Collection, sbom sbom.SBOM) (results []*spdx.Packag // 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. - license := License(p) + // extract these correctly to the spdx license format + concluded, declared := License(p) // two ways to get filesAnalyzed == true: // 1. syft has generated a sha1 digest for the package itself - usually in the java cataloger @@ -274,7 +275,7 @@ func toPackages(catalog *pkg.Collection, sbom sbom.SBOM) (results []*spdx.Packag // Cardinality: mandatory, one // Purpose: Contain the license the SPDX file creator has concluded as governing the // package or alternative values, if the governing license cannot be determined. - PackageLicenseConcluded: license, + PackageLicenseConcluded: concluded, // 7.14: All Licenses Info from Files: SPDX License Expression, "NONE" or "NOASSERTION" // Cardinality: mandatory, one or many if filesAnalyzed is true / omitted; @@ -286,7 +287,7 @@ func toPackages(catalog *pkg.Collection, sbom sbom.SBOM) (results []*spdx.Packag // Purpose: List the licenses that have been declared by the authors of the package. // Any license information that does not originate from the package authors, e.g. license // information from a third party repository, should not be included in this field. - PackageLicenseDeclared: license, + PackageLicenseDeclared: declared, // 7.16: Comments on License // Cardinality: optional, one @@ -534,10 +535,18 @@ func toFileTypes(metadata *source.FileMetadata) (ty []string) { return ty } +// other licenses are for licenses from the pkg.Package that do not have an SPDXExpression +// field. The spdxexpression field is only filled given a validated Value field. func toOtherLicenses(catalog *pkg.Collection) []*spdx.OtherLicense { licenses := map[string]bool{} for _, p := range catalog.Sorted() { - for _, license := range parseLicenses(p.Licenses) { + declaredLicenses, concludedLicenses := parseLicenses(p.Licenses.ToSlice()) + for _, license := range declaredLicenses { + if strings.HasPrefix(license, spdxlicense.LicenseRefPrefix) { + licenses[license] = true + } + } + for _, license := range concludedLicenses { if strings.HasPrefix(license, spdxlicense.LicenseRefPrefix) { licenses[license] = true } @@ -549,12 +558,12 @@ func toOtherLicenses(catalog *pkg.Collection) []*spdx.OtherLicense { sorted := maps.Keys(licenses) slices.Sort(sorted) for _, license := range sorted { - // separate the actual ID from the prefix + // separate the found value from the prefix + // this only contains licenses that are not found on the SPDX License List name := strings.TrimPrefix(license, spdxlicense.LicenseRefPrefix) result = append(result, &spdx.OtherLicense{ LicenseIdentifier: SanitizeElementID(license), - LicenseName: name, - ExtractedText: NONE, // we probably should have some extracted text here, but this is good enough for now + ExtractedText: name, }) } return result diff --git a/syft/formats/common/spdxhelpers/to_format_model_test.go b/syft/formats/common/spdxhelpers/to_format_model_test.go index f0b4f42ce..170de95ea 100644 --- a/syft/formats/common/spdxhelpers/to_format_model_test.go +++ b/syft/formats/common/spdxhelpers/to_format_model_test.go @@ -448,46 +448,40 @@ func Test_OtherLicenses(t *testing.T) { { name: "no licenseRef", pkg: pkg.Package{ - Licenses: []string{ - "MIT", - }, + Licenses: pkg.NewLicenseSet(), }, expected: nil, }, { name: "single licenseRef", pkg: pkg.Package{ - Licenses: []string{ - "un known", - }, + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("foobar"), + ), }, expected: []*spdx.OtherLicense{ { - LicenseIdentifier: "LicenseRef-un-known", - LicenseName: "un known", - ExtractedText: NONE, + LicenseIdentifier: "LicenseRef-foobar", + ExtractedText: "foobar", }, }, }, { name: "multiple licenseRef", pkg: pkg.Package{ - Licenses: []string{ - "un known", - "not known %s", - "MIT", - }, + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("internal made up license name"), + pkg.NewLicense("new apple license 2.0"), + ), }, expected: []*spdx.OtherLicense{ { - LicenseIdentifier: "LicenseRef-not-known--s", - LicenseName: "not known %s", - ExtractedText: NONE, + LicenseIdentifier: "LicenseRef-internal-made-up-license-name", + ExtractedText: "internal made up license name", }, { - LicenseIdentifier: "LicenseRef-un-known", - LicenseName: "un known", - ExtractedText: NONE, + LicenseIdentifier: "LicenseRef-new-apple-license-2.0", + ExtractedText: "new apple license 2.0", }, }, }, diff --git a/syft/formats/common/spdxhelpers/to_syft_model.go b/syft/formats/common/spdxhelpers/to_syft_model.go index aa03ce996..a31cee812 100644 --- a/syft/formats/common/spdxhelpers/to_syft_model.go +++ b/syft/formats/common/spdxhelpers/to_syft_model.go @@ -14,6 +14,7 @@ import ( "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/formats/common/util" + "github.com/anchore/syft/syft/license" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" @@ -279,7 +280,7 @@ func toSyftPackage(p *spdx.Package) *pkg.Package { Type: info.typ, Name: p.PackageName, Version: p.PackageVersion, - Licenses: parseLicense(p.PackageLicenseDeclared), + Licenses: pkg.NewLicenseSet(parseSPDXLicenses(p)...), CPEs: extractCPEs(p), PURL: info.purl.String(), Language: info.lang, @@ -292,6 +293,33 @@ func toSyftPackage(p *spdx.Package) *pkg.Package { return &sP } +func parseSPDXLicenses(p *spdx.Package) []pkg.License { + licenses := make([]pkg.License, 0) + + // concluded + if p.PackageLicenseConcluded != NOASSERTION && p.PackageLicenseConcluded != NONE && p.PackageLicenseConcluded != "" { + l := pkg.NewLicense(cleanSPDXID(p.PackageLicenseConcluded)) + l.Type = license.Concluded + licenses = append(licenses, l) + } + + // declared + if p.PackageLicenseDeclared != NOASSERTION && p.PackageLicenseDeclared != NONE && p.PackageLicenseDeclared != "" { + l := pkg.NewLicense(cleanSPDXID(p.PackageLicenseDeclared)) + l.Type = license.Declared + licenses = append(licenses, l) + } + + return licenses +} + +func cleanSPDXID(id string) string { + if strings.HasPrefix(id, "LicenseRef-") { + return strings.TrimPrefix(id, "LicenseRef-") + } + return id +} + //nolint:funlen func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface{}) { arch := info.qualifierValue(pkg.PURLQualifierArch) @@ -317,7 +345,6 @@ func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface OriginPackage: upstreamName, Maintainer: supplier, Version: p.PackageVersion, - License: p.PackageLicenseDeclared, Architecture: arch, URL: p.PackageHomePage, Description: p.PackageDescription, @@ -330,17 +357,12 @@ func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface } else { epoch = &converted } - license := p.PackageLicenseDeclared - if license == "" { - license = p.PackageLicenseConcluded - } return pkg.RpmMetadataType, pkg.RpmMetadata{ Name: p.PackageName, Version: p.PackageVersion, Epoch: epoch, Arch: arch, SourceRpm: upstreamValue, - License: license, Vendor: originator, } case pkg.DebPkg: @@ -400,10 +422,3 @@ func extractCPEs(p *spdx.Package) (cpes []cpe.CPE) { } return cpes } - -func parseLicense(l string) []string { - if l == NOASSERTION || l == NONE { - return nil - } - return strings.Split(l, " AND ") -} diff --git a/syft/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/syft/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden index a40a1ff52..3cab5be0e 100644 --- a/syft/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden +++ b/syft/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -2,10 +2,10 @@ "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.4", - "serialNumber": "urn:uuid:redacted", + "serialNumber": "urn:uuid:1b71a5b4-4bc5-4548-a51a-212e631976cd", "version": 1, "metadata": { - "timestamp": "timestamp:redacted", + "timestamp": "2023-05-08T14:40:32-04:00", "tools": [ { "vendor": "anchore", @@ -14,14 +14,14 @@ } ], "component": { - "bom-ref": "redacted", + "bom-ref": "163686ac6e30c752", "type": "file", "name": "/some/path" } }, "components": [ { - "bom-ref": "redacted", + "bom-ref": "8c7e1242588c971a", "type": "library", "name": "package-1", "version": "1.0.1", @@ -58,7 +58,7 @@ ] }, { - "bom-ref": "redacted", + "bom-ref": "pkg:deb/debian/package-2@2.0.1?package-id=db4abfe497c180d3", "type": "library", "name": "package-2", "version": "2.0.1", diff --git a/syft/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/syft/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden index aa71909a6..4d4765f54 100644 --- a/syft/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden +++ b/syft/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden @@ -2,10 +2,10 @@ "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.4", - "serialNumber": "urn:uuid:redacted", + "serialNumber": "urn:uuid:1695d6ae-0ddf-4e77-9c9d-74df1bdd8d5b", "version": 1, "metadata": { - "timestamp": "timestamp:redacted", + "timestamp": "2023-05-08T14:40:32-04:00", "tools": [ { "vendor": "anchore", @@ -14,15 +14,15 @@ } ], "component": { - "bom-ref": "redacted", + "bom-ref": "38160ebc2a6876e8", "type": "container", "name": "user-image-input", - "version": "sha256:redacted" + "version": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368" } }, "components": [ { - "bom-ref": "redacted", + "bom-ref": "ec2e0c93617507ef", "type": "library", "name": "package-1", "version": "1.0.1", @@ -54,7 +54,7 @@ }, { "name": "syft:location:0:layerID", - "value": "sha256:redacted" + "value": "sha256:ab62016f9bec7286af65604081564cadeeb364a48faca2346c3f5a5a1f5ef777" }, { "name": "syft:location:0:path", @@ -63,7 +63,7 @@ ] }, { - "bom-ref": "redacted", + "bom-ref": "pkg:deb/debian/package-2@2.0.1?package-id=958443e2d9304af4", "type": "library", "name": "package-2", "version": "2.0.1", @@ -84,7 +84,7 @@ }, { "name": "syft:location:0:layerID", - "value": "sha256:redacted" + "value": "sha256:f1803845b6747d94d6e4ecce2331457e5f1c4fb97de5216f392a76f4582f63b2" }, { "name": "syft:location:0:path", diff --git a/syft/formats/cyclonedxjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/syft/formats/cyclonedxjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index 11a1958c8935029370bb6a48ad18899403776949..f4aa1e7bb74e9d8c2cf36a69cd7a6d910a9a7087 100644 GIT binary patch literal 15360 zcmeHOTW{Mo6wdR0g{Qs7Ht&=S>>+Ccv_P>8ZPoz`f`H;B(PB%6Bo_$`|Mwlqj$^w^ zv_!U(q7)36*F*9p$TkutO) zGMZRoBVzjp%@0BhNzM<@{Mr1kQV%d=1aU4!RSrUg9&~uK==LQhMX7S=Qd>!zgKH~q zPxWzI{eL<6^!dZ-?@x6XVT1%@n|i;G19Egj|CkGW&}prcr#f|?_oM5-w-=P}(f0pu z41w16|4)&n4P$Dsf6U3|`i~hV-u@e4-AeW}NK7yY+D?PuFc_=crX|SYM54%0$w{n2 zA$c5QCv6l*1`9{9V-`8eonl!GO+a+Yy<@!;(r!4pXVHvrP9}* zOOvF*z^P$j4})b|Hi5xxCfXdTE)vhmyJh%vY}0i)b?h|N(1}I)lk{saNGHSLVeoDQ zW19y*jMUYlqhGS&WMqr`oWHXCCP}Z3lSOY&(lWmrXGsO}yzXiG+awt-MSWDU9WJWR zYGmF%9_PUyM`!PU?N5rlKg_fm_KP@42P^Sq^dX~8nnxiRT)YI(KFo~gBC2wIos?FW zle|)y8X3VrOa_<-!z7*D2D7agd{D0oFwgZ>em_ZItqO5x~tfH0O`|0@9QFgVwJr+SOR+q?LAhfR6W z9s!SlN5CWSh7f3bSlf~RmlEmyzx$Xbt@Xbf|1TBX@BjCqf9(Cg`+)h)w$a-D+xvf! zAb9ip55chh@8!$H5B~jU;s0$r4Jt?M(r1e!iMj~G?+l?q{qvsMr#!W<=s;|%_#OHG zF!SGk_VUhH`v352`v1f?{yzr%55NBJehQS=NztjCfSqeqEZu;+Z`Zfr5%36j1Uv$- zi-4k9V52c*hB%62hPmd55@sb288OmDSP;mHR2VT4T8<+cQi@{g3{|ez{``jt+dTgv z#+iTr?_*|6gGWdW4{Vy6x3VRk#u}%kqF}61SSr%W@)X&~{DGZIndQL2IiEIFLsDSM zIj@hQ^A;{+s2Lm`AJ@B|ZoU2O^w+n;onJa}MX_G{j?ClF>~2I(3k`lv+s^j zvk#-)VjClsCe9Y+|5?Xh+H4d5gLAsV1xGdi!}C91iuUvB!P@z5!J7=WvKIjB|76G6 znYx;kQrQ`x@|-v75Nb*#yq21SE~R=bAQM!E%t`E2$!Eoco+AMpry L1Uv%Y4FdlHH$Ie? literal 15360 zcmeHOZExE)5ccQ&3Qzl**nFpCU>~}sKnoO0(Pka6qA2)sOtjjPA<0F9Apdm%%?PJlu^wTR3f}9JyT$INc2+m0LY2Bo z+kCjL@($GhDlwJ2$Qy9Ytb$~en2A>+IZ9kvg#0`iT_ul-cgg5g^6bIcA**Z}Z~(B(HF&Ar5m|{4vYNJ{c9W@%S2tt-v&Z z<%AK&j!Ak<=oup;l8qP}NRkOgHD&)`*NePv%IXqzyV}xCq&m%{q*>c-Il>!@J(C9~ zC;gC*;g}AZ_f3CTmlIei|JJvyc5m}$Zjs3ZiUxaqxH<5pdka}WC)5Q(m;?gGWhr0tP6t%jdrI6?%$4P|rP|4?pM&h_=5YAJNDFF!+e z=O#b-`rq0AFCRUB@#OT+yZo?a|3~-pb^kM>h>ZLHT{hiS9UXLze0HTul=BrV(0Tsv zApVzZWBq4Da24nOAzp{8ByWT5uCD>cd{a{`qhswgO%cpt?3XzyKzi#9rAlcoiC`%g zUKpNQNsP1tFy!MF(O;Tr$E;^4>mDcoaY|$I0;)H+h~;rvH-(wBfi4O=DJR(Aw)|HO zE&9Nk2Tfz##f!!xI2i>qu3_a~Im1wY^Cnc-b$RowK?Je9KAl3bE@xgkEgUMbY%|AE zEzCJe9!{2s=O}qHG3QG~UzX$91TYj|G+M(-sO_VosV=8w-a?Q?i#70clsq*S#>?(4 zl)6E1->^M)ge&iF&=!2&*P@?*$}3RoyMrrTC;o7bBnK^r^2y(p_)mNk|Fby_6#1V| zT6A|6H-yKWXV{(GRY_a?k3>Mb{}ZMtG5}Qf2&I}M{yRuy`jg|oIoZYf?JkPDcm3 z?))!NY%~7@`QP~c=OFLQlK<`c8X%c=%h}RQ6HMD^GdBokZnS^%#EZz(ejsi)nA)#M zAa-^9-uOQf`JY2905|;KJOOGyBW~ya)JOaurVB>?Z$Iu>liFYw-8=)Xe_`qy8gQ*i zEJO=L3q%V<3;ci&PqIn?_abld!Ax4Gf-TyUKK3XcKB_mIfGV=884#+q6MM_z9S3#2P@x!egFUf diff --git a/syft/formats/cyclonedxxml/decoder_test.go b/syft/formats/cyclonedxxml/decoder_test.go index ca0622abc..c0ab823ac 100644 --- a/syft/formats/cyclonedxxml/decoder_test.go +++ b/syft/formats/cyclonedxxml/decoder_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_decodeXML(t *testing.T) { @@ -34,7 +35,7 @@ func Test_decodeXML(t *testing.T) { for _, test := range tests { t.Run(test.file, func(t *testing.T) { reader, err := os.Open("test-fixtures/" + test.file) - assert.NoError(t, err) + require.NoError(t, err) if test.err { err = Format().Validate(reader) @@ -44,7 +45,7 @@ func Test_decodeXML(t *testing.T) { bom, err := Format().Decode(reader) - assert.NoError(t, err) + require.NoError(t, err) split := strings.SplitN(test.distro, ":", 2) name := split[0] diff --git a/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden index 095d2c3b8..32eaf2747 100644 --- a/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden +++ b/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -1,7 +1,7 @@ - + - 2022-11-07T09:11:06-05:00 + 2023-05-08T14:40:52-04:00 anchore @@ -14,7 +14,7 @@ - + package-1 1.0.1 diff --git a/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden index 814962bed..67ad5f052 100644 --- a/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden +++ b/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden @@ -1,7 +1,7 @@ - + - 2022-11-07T09:11:06-05:00 + 2023-05-08T14:40:52-04:00 anchore @@ -9,13 +9,13 @@ v0.42.0-bogus - + user-image-input sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368 - + package-1 1.0.1 @@ -30,7 +30,7 @@ python PythonPackageMetadata python - sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59 + sha256:ab62016f9bec7286af65604081564cadeeb364a48faca2346c3f5a5a1f5ef777 /somefile-1.txt @@ -43,7 +43,7 @@ the-cataloger-2 DpkgMetadata deb - sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec + sha256:f1803845b6747d94d6e4ecce2331457e5f1c4fb97de5216f392a76f4582f63b2 /somefile-2.txt 0 diff --git a/syft/formats/cyclonedxxml/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/syft/formats/cyclonedxxml/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index 11a1958c8935029370bb6a48ad18899403776949..f4aa1e7bb74e9d8c2cf36a69cd7a6d910a9a7087 100644 GIT binary patch literal 15360 zcmeHOTW{Mo6wdR0g{Qs7Ht&=S>>+Ccv_P>8ZPoz`f`H;B(PB%6Bo_$`|Mwlqj$^w^ zv_!U(q7)36*F*9p$TkutO) zGMZRoBVzjp%@0BhNzM<@{Mr1kQV%d=1aU4!RSrUg9&~uK==LQhMX7S=Qd>!zgKH~q zPxWzI{eL<6^!dZ-?@x6XVT1%@n|i;G19Egj|CkGW&}prcr#f|?_oM5-w-=P}(f0pu z41w16|4)&n4P$Dsf6U3|`i~hV-u@e4-AeW}NK7yY+D?PuFc_=crX|SYM54%0$w{n2 zA$c5QCv6l*1`9{9V-`8eonl!GO+a+Yy<@!;(r!4pXVHvrP9}* zOOvF*z^P$j4})b|Hi5xxCfXdTE)vhmyJh%vY}0i)b?h|N(1}I)lk{saNGHSLVeoDQ zW19y*jMUYlqhGS&WMqr`oWHXCCP}Z3lSOY&(lWmrXGsO}yzXiG+awt-MSWDU9WJWR zYGmF%9_PUyM`!PU?N5rlKg_fm_KP@42P^Sq^dX~8nnxiRT)YI(KFo~gBC2wIos?FW zle|)y8X3VrOa_<-!z7*D2D7agd{D0oFwgZ>em_ZItqO5x~tfH0O`|0@9QFgVwJr+SOR+q?LAhfR6W z9s!SlN5CWSh7f3bSlf~RmlEmyzx$Xbt@Xbf|1TBX@BjCqf9(Cg`+)h)w$a-D+xvf! zAb9ip55chh@8!$H5B~jU;s0$r4Jt?M(r1e!iMj~G?+l?q{qvsMr#!W<=s;|%_#OHG zF!SGk_VUhH`v352`v1f?{yzr%55NBJehQS=NztjCfSqeqEZu;+Z`Zfr5%36j1Uv$- zi-4k9V52c*hB%62hPmd55@sb288OmDSP;mHR2VT4T8<+cQi@{g3{|ez{``jt+dTgv z#+iTr?_*|6gGWdW4{Vy6x3VRk#u}%kqF}61SSr%W@)X&~{DGZIndQL2IiEIFLsDSM zIj@hQ^A;{+s2Lm`AJ@B|ZoU2O^w+n;onJa}MX_G{j?ClF>~2I(3k`lv+s^j zvk#-)VjClsCe9Y+|5?Xh+H4d5gLAsV1xGdi!}C91iuUvB!P@z5!J7=WvKIjB|76G6 znYx;kQrQ`x@|-v75Nb*#yq21SE~R=bAQM!E%t`E2$!Eoco+AMpry L1Uv%Y4FdlHH$Ie? literal 15360 zcmeHOZExE)5ccQ&3Qzl**nFpCU>~}sKnoO0(Pka6qA2)sOtjjPA<0F9Apdm%%?PJlu^wTR3f}9JyT$INc2+m0LY2Bo z+kCjL@($GhDlwJ2$Qy9Ytb$~en2A>+IZ9kvg#0`iT_ul-cgg5g^6bIcA**Z}Z~(B(HF&Ar5m|{4vYNJ{c9W@%S2tt-v&Z z<%AK&j!Ak<=oup;l8qP}NRkOgHD&)`*NePv%IXqzyV}xCq&m%{q*>c-Il>!@J(C9~ zC;gC*;g}AZ_f3CTmlIei|JJvyc5m}$Zjs3ZiUxaqxH<5pdka}WC)5Q(m;?gGWhr0tP6t%jdrI6?%$4P|rP|4?pM&h_=5YAJNDFF!+e z=O#b-`rq0AFCRUB@#OT+yZo?a|3~-pb^kM>h>ZLHT{hiS9UXLze0HTul=BrV(0Tsv zApVzZWBq4Da24nOAzp{8ByWT5uCD>cd{a{`qhswgO%cpt?3XzyKzi#9rAlcoiC`%g zUKpNQNsP1tFy!MF(O;Tr$E;^4>mDcoaY|$I0;)H+h~;rvH-(wBfi4O=DJR(Aw)|HO zE&9Nk2Tfz##f!!xI2i>qu3_a~Im1wY^Cnc-b$RowK?Je9KAl3bE@xgkEgUMbY%|AE zEzCJe9!{2s=O}qHG3QG~UzX$91TYj|G+M(-sO_VosV=8w-a?Q?i#70clsq*S#>?(4 zl)6E1->^M)ge&iF&=!2&*P@?*$}3RoyMrrTC;o7bBnK^r^2y(p_)mNk|Fby_6#1V| zT6A|6H-yKWXV{(GRY_a?k3>Mb{}ZMtG5}Qf2&I}M{yRuy`jg|oIoZYf?JkPDcm3 z?))!NY%~7@`QP~c=OFLQlK<`c8X%c=%h}RQ6HMD^GdBokZnS^%#EZz(ejsi)nA)#M zAa-^9-uOQf`JY2905|;KJOOGyBW~ya)JOaurVB>?Z$Iu>liFYw-8=)Xe_`qy8gQ*i zEJO=L3q%V<3;ci&PqIn?_abld!Ax4Gf-TyUKK3XcKB_mIfGV=884#+q6MM_z9S3#2P@x!egFUf diff --git a/syft/formats/internal/testutils/utils.go b/syft/formats/internal/testutils/utils.go index b72ccfa6f..7ddf94217 100644 --- a/syft/formats/internal/testutils/utils.go +++ b/syft/formats/internal/testutils/utils.go @@ -162,7 +162,9 @@ func populateImageCatalog(catalog *pkg.Collection, img *image.Image) { FoundBy: "the-cataloger-1", Language: pkg.Python, MetadataType: pkg.PythonPackageMetadataType, - Licenses: []string{"MIT"}, + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("MIT"), + ), Metadata: pkg.PythonPackageMetadata{ Name: "package-1", Version: "1.0.1", @@ -268,7 +270,9 @@ func newDirectoryCatalog() *pkg.Collection { ), Language: pkg.Python, MetadataType: pkg.PythonPackageMetadataType, - Licenses: []string{"MIT"}, + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("MIT"), + ), Metadata: pkg.PythonPackageMetadata{ Name: "package-1", Version: "1.0.1", @@ -319,7 +323,9 @@ func newDirectoryCatalogWithAuthorField() *pkg.Collection { ), Language: pkg.Python, MetadataType: pkg.PythonPackageMetadataType, - Licenses: []string{"MIT"}, + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("MIT"), + ), Metadata: pkg.PythonPackageMetadata{ Name: "package-1", Version: "1.0.1", diff --git a/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden b/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden index 60e4c4f49..de775cd15 100644 --- a/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden +++ b/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden @@ -3,23 +3,23 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "/some/path", - "documentNamespace": "https://anchore.com/syft/dir/some/path-4029b5ec-6d70-4c0c-aedf-b61c8f5ea93c", + "documentNamespace": "https://anchore.com/syft/dir/some/path-5ea40e59-d91a-4682-a016-da45ddd540e4", "creationInfo": { "licenseListVersion": "3.20", "creators": [ "Organization: Anchore, Inc", "Tool: syft-v0.42.0-bogus" ], - "created": "2023-05-02T18:24:17Z" + "created": "2023-05-09T17:11:26Z" }, "packages": [ { "name": "package-1", - "SPDXID": "SPDXRef-Package-python-package-1-1b1d0be59ac59d2c", + "SPDXID": "SPDXRef-Package-python-package-1-9265397e5e15168a", "versionInfo": "1.0.1", "downloadLocation": "NOASSERTION", "sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1", - "licenseConcluded": "MIT", + "licenseConcluded": "NOASSERTION", "licenseDeclared": "MIT", "copyrightText": "NOASSERTION", "externalRefs": [ @@ -41,8 +41,8 @@ "versionInfo": "2.0.1", "downloadLocation": "NOASSERTION", "sourceInfo": "acquired package info from DPKG DB: /some/path/pkg1", - "licenseConcluded": "NONE", - "licenseDeclared": "NONE", + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", "copyrightText": "NOASSERTION", "externalRefs": [ { diff --git a/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden b/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden index 51eab30a6..8b08d7608 100644 --- a/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden +++ b/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden @@ -3,23 +3,23 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "user-image-input", - "documentNamespace": "https://anchore.com/syft/image/user-image-input-6b0c6ff8-0f5f-4d95-8c1b-eb966d400804", + "documentNamespace": "https://anchore.com/syft/image/user-image-input-2cc737fb-af51-4e4b-9395-cceabcc305eb", "creationInfo": { "licenseListVersion": "3.20", "creators": [ "Organization: Anchore, Inc", "Tool: syft-v0.42.0-bogus" ], - "created": "2023-05-02T18:24:18Z" + "created": "2023-05-09T17:11:26Z" }, "packages": [ { "name": "package-1", - "SPDXID": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "SPDXID": "SPDXRef-Package-python-package-1-125840abc1c66dd7", "versionInfo": "1.0.1", "downloadLocation": "NOASSERTION", "sourceInfo": "acquired package info from installed python package manifest file: /somefile-1.txt", - "licenseConcluded": "MIT", + "licenseConcluded": "NOASSERTION", "licenseDeclared": "MIT", "copyrightText": "NOASSERTION", "externalRefs": [ @@ -41,8 +41,8 @@ "versionInfo": "2.0.1", "downloadLocation": "NOASSERTION", "sourceInfo": "acquired package info from DPKG DB: /somefile-2.txt", - "licenseConcluded": "NONE", - "licenseDeclared": "NONE", + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", "copyrightText": "NOASSERTION", "externalRefs": [ { diff --git a/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden b/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden index 74481255a..e269ed535 100644 --- a/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden +++ b/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden @@ -3,23 +3,23 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "user-image-input", - "documentNamespace": "https://anchore.com/syft/image/user-image-input-ec2f9b25-22ca-46b8-b7f4-484994fe126c", + "documentNamespace": "https://anchore.com/syft/image/user-image-input-1de3ac0e-5829-4294-9198-8d8fcdb5dd51", "creationInfo": { "licenseListVersion": "3.20", "creators": [ "Organization: Anchore, Inc", "Tool: syft-v0.42.0-bogus" ], - "created": "2023-05-02T18:24:18Z" + "created": "2023-05-09T17:11:26Z" }, "packages": [ { "name": "package-1", - "SPDXID": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "SPDXID": "SPDXRef-Package-python-package-1-125840abc1c66dd7", "versionInfo": "1.0.1", "downloadLocation": "NOASSERTION", "sourceInfo": "acquired package info from installed python package manifest file: /somefile-1.txt", - "licenseConcluded": "MIT", + "licenseConcluded": "NOASSERTION", "licenseDeclared": "MIT", "copyrightText": "NOASSERTION", "externalRefs": [ @@ -41,8 +41,8 @@ "versionInfo": "2.0.1", "downloadLocation": "NOASSERTION", "sourceInfo": "acquired package info from DPKG DB: /somefile-2.txt", - "licenseConcluded": "NONE", - "licenseDeclared": "NONE", + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", "copyrightText": "NOASSERTION", "externalRefs": [ { @@ -152,32 +152,32 @@ ], "relationships": [ { - "spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7", "relatedSpdxElement": "SPDXRef-File-f1-5265a4dde3edbf7c", "relationshipType": "CONTAINS" }, { - "spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7", "relatedSpdxElement": "SPDXRef-File-z1-f5-839d99ee67d9d174", "relationshipType": "CONTAINS" }, { - "spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7", "relatedSpdxElement": "SPDXRef-File-a1-f6-9c2f7510199b17f6", "relationshipType": "CONTAINS" }, { - "spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7", "relatedSpdxElement": "SPDXRef-File-d2-f4-c641caa71518099f", "relationshipType": "CONTAINS" }, { - "spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7", "relatedSpdxElement": "SPDXRef-File-d1-f3-c6f5b29dca12661f", "relationshipType": "CONTAINS" }, { - "spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7", "relatedSpdxElement": "SPDXRef-File-f2-f9e49132a4b96ccd", "relationshipType": "CONTAINS" }, diff --git a/syft/formats/spdxjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/syft/formats/spdxjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index b9ddf6e33390ab196d24a941bdda1dc969b30a0e..f4aa1e7bb74e9d8c2cf36a69cd7a6d910a9a7087 100644 GIT binary patch literal 15360 zcmeHOTW{Mo6wdR0g{Qs7Ht&=S>>+Ccv_P>8ZPoz`f`H;B(PB%6Bo_$`|Mwlqj$^w^ zv_!U(q7)36*F*9p$TkutO) zGMZRoBVzjp%@0BhNzM<@{Mr1kQV%d=1aU4!RSrUg9&~uK==LQhMX7S=Qd>!zgKH~q zPxWzI{eL<6^!dZ-?@x6XVT1%@n|i;G19Egj|CkGW&}prcr#f|?_oM5-w-=P}(f0pu z41w16|4)&n4P$Dsf6U3|`i~hV-u@e4-AeW}NK7yY+D?PuFc_=crX|SYM54%0$w{n2 zA$c5QCv6l*1`9{9V-`8eonl!GO+a+Yy<@!;(r!4pXVHvrP9}* zOOvF*z^P$j4})b|Hi5xxCfXdTE)vhmyJh%vY}0i)b?h|N(1}I)lk{saNGHSLVeoDQ zW19y*jMUYlqhGS&WMqr`oWHXCCP}Z3lSOY&(lWmrXGsO}yzXiG+awt-MSWDU9WJWR zYGmF%9_PUyM`!PU?N5rlKg_fm_KP@42P^Sq^dX~8nnxiRT)YI(KFo~gBC2wIos?FW zle|)y8X3VrOa_<-!z7*D2D7agd{D0oFwgZ>em_ZItqO5x~tfH0O`|0@9QFgVwJr+SOR+q?LAhfR6W z9s!SlN5CWSh7f3bSlf~RmlEmyzx$Xbt@Xbf|1TBX@BjCqf9(Cg`+)h)w$a-D+xvf! zAb9ip55chh@8!$H5B~jU;s0$r4Jt?M(r1e!iMj~G?+l?q{qvsMr#!W<=s;|%_#OHG zF!SGk_VUhH`v352`v1f?{yzr%55NBJehQS=NztjCfSqeqEZu;+Z`Zfr5%36j1Uv$- zi-4k9V52c*hB%62hPmd55@sb288OmDSP;mHR2VT4T8<+cQi@{g3{|ez{``jt+dTgv z#+iTr?_*|6gGWdW4{Vy6x3VRk#u}%kqF}61SSr%W@)X&~{DGZIndQL2IiEIFLsDSM zIj@hQ^A;{+s2Lm`AJ@B|ZoU2O^w+n;onJa}MX_G{j?ClF>~2I(3k`lv+s^j zvk#-)VjClsCe9Y+|5?Xh+H4d5gLAsV1xGdi!}C91iuUvB!P@z5!J7=WvKIjB|76G6 znYx;kQrQ`x@|-v75Nb*#yq21SE~R=bAQM!E%t`E2$!Eoco+AMpry L1Uv%Y4FdlHH$Ie? literal 15360 zcmeHOZExE)5ccQ&3QzkQ+a$#|8Q6!eDbNDNQnXnItSAb;CR%ODkmMpkkpI4u>^QNz zR!d|nDMrD7#kV8%9PfNQJ&u_WL{sC4wG4YAF?W_&oG8wCEIH;@I^mcSRx68*XAI#) zS|o9zEfbSL`-Koig0TTwUAy0P?Eur5Au0rp;{hTFBRm+gcG2B?EXzt2u%xb%b|0>* zyq?C#UG@L|#oKqUPJaL1azUGjQ@X47n=&95@8}=nSPX`pb@IJM-L(DB^^eEBpnQXN z_6o6%e<^IHFq!`dK~9-=x|&9qjbfp`(yV%`5m_9dET%h~KG zdOn9|Tiw2ztF!e&-{-UC+?6*if9A#KG&?&^*Qb25t zwjO@nAoKL`_z=B$cKY(y$+9dav)riJq||9Py;I&4ZyTDbRS|BZj}HNK0z2crh^oSz zrA6l z|6xLR&wm8LYrt^+>tS15Mc*Jb+_GsJ-pZbM8e5!}nu4)KVW~+gn^R;f^9LfADldS8 z^Wmyx7?D6RgT~UWm<1Z+>K5ARYHgoA;7t*|kzq624`JM{_M$DZvKrLZwZg1@1)1d9wZf_kg5$BsxsiS)4r zwt4jN`iCRb^kB35oMNuB)Vs3!FMj-?^S1Fn7z57#?bYxRh5KIPz;`}h^CqLc z!~$UbZ`~q4Rc9-oQn|wArO6j=*SvY_orI``@723)J?{d9eROsbfp-RaCZEXdN|lHqt#X%&)JXrUh-m P843Y`fIvXthe6;Ux}S+Y diff --git a/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXJSONSPDXIDs.golden b/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXJSONSPDXIDs.golden index 6a7e5f7bb..ca1775a5d 100644 --- a/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXJSONSPDXIDs.golden +++ b/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXJSONSPDXIDs.golden @@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.3 DataLicense: CC0-1.0 SPDXID: SPDXRef-DOCUMENT DocumentName: foobar/baz -DocumentNamespace: https://anchore.com/syft/dir/foobar/baz-9c1f31fb-7c72-40a6-8c81-3a08590000a2 +DocumentNamespace: https://anchore.com/syft/dir/foobar/baz-1813dede-1ac5-4c44-a640-4c56e213d575 LicenseListVersion: 3.20 Creator: Organization: Anchore, Inc Creator: Tool: syft-v0.42.0-bogus -Created: 2023-05-02T18:24:33Z +Created: 2023-05-09T17:11:49Z ##### Package: @at-sign @@ -15,8 +15,8 @@ SPDXID: SPDXRef-Package---at-sign-3732f7a5679bdec4 PackageDownloadLocation: NOASSERTION FilesAnalyzed: false PackageSourceInfo: acquired package info from the following paths: -PackageLicenseConcluded: NONE -PackageLicenseDeclared: NONE +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION PackageCopyrightText: NOASSERTION ##### Package: some/slashes @@ -26,8 +26,8 @@ SPDXID: SPDXRef-Package--some-slashes-1345166d4801153b PackageDownloadLocation: NOASSERTION FilesAnalyzed: false PackageSourceInfo: acquired package info from the following paths: -PackageLicenseConcluded: NONE -PackageLicenseDeclared: NONE +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION PackageCopyrightText: NOASSERTION ##### Package: under_scores @@ -37,8 +37,8 @@ SPDXID: SPDXRef-Package--under-scores-290d5c77210978c1 PackageDownloadLocation: NOASSERTION FilesAnalyzed: false PackageSourceInfo: acquired package info from the following paths: -PackageLicenseConcluded: NONE -PackageLicenseDeclared: NONE +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION PackageCopyrightText: NOASSERTION ##### Relationships diff --git a/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden b/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden index b9fd089b4..339b17c2c 100644 --- a/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden +++ b/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden @@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.3 DataLicense: CC0-1.0 SPDXID: SPDXRef-DOCUMENT DocumentName: user-image-input -DocumentNamespace: https://anchore.com/syft/image/user-image-input-5be37b11-b99a-47ff-8725-3984e323d129 +DocumentNamespace: https://anchore.com/syft/image/user-image-input-96ea886a-3297-4847-b211-6da405ff1f8f LicenseListVersion: 3.20 Creator: Organization: Anchore, Inc Creator: Tool: syft-v0.42.0-bogus -Created: 2023-05-02T18:24:33Z +Created: 2023-05-09T17:11:49Z ##### Unpackaged files @@ -54,8 +54,8 @@ PackageVersion: 2.0.1 PackageDownloadLocation: NOASSERTION FilesAnalyzed: false PackageSourceInfo: acquired package info from DPKG DB: /somefile-2.txt -PackageLicenseConcluded: NONE -PackageLicenseDeclared: NONE +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION PackageCopyrightText: NOASSERTION ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:* ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1 @@ -63,12 +63,12 @@ ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1 ##### Package: package-1 PackageName: package-1 -SPDXID: SPDXRef-Package-python-package-1-66ba429119b8bec6 +SPDXID: SPDXRef-Package-python-package-1-125840abc1c66dd7 PackageVersion: 1.0.1 PackageDownloadLocation: NOASSERTION FilesAnalyzed: false PackageSourceInfo: acquired package info from installed python package manifest file: /somefile-1.txt -PackageLicenseConcluded: MIT +PackageLicenseConcluded: NOASSERTION PackageLicenseDeclared: MIT PackageCopyrightText: NOASSERTION ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:1:*:*:*:*:*:*:* @@ -76,11 +76,11 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-1 ##### Relationships -Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-File-f1-5265a4dde3edbf7c -Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-File-z1-f5-839d99ee67d9d174 -Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-File-a1-f6-9c2f7510199b17f6 -Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-File-d2-f4-c641caa71518099f -Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-File-d1-f3-c6f5b29dca12661f -Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-File-f2-f9e49132a4b96ccd +Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-f1-5265a4dde3edbf7c +Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-z1-f5-839d99ee67d9d174 +Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-a1-f6-9c2f7510199b17f6 +Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-d2-f4-c641caa71518099f +Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-d1-f3-c6f5b29dca12661f +Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-f2-f9e49132a4b96ccd Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DOCUMENT diff --git a/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden b/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden index ae9062bb8..818d62e7d 100644 --- a/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden +++ b/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden @@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.3 DataLicense: CC0-1.0 SPDXID: SPDXRef-DOCUMENT DocumentName: /some/path -DocumentNamespace: https://anchore.com/syft/dir/some/path-0f346656-6d10-4dec-b549-a256468cbd35 +DocumentNamespace: https://anchore.com/syft/dir/some/path-f7bdb1ee-7fef-48e7-a386-6ee3836d4a28 LicenseListVersion: 3.20 Creator: Organization: Anchore, Inc Creator: Tool: syft-v0.42.0-bogus -Created: 2023-05-02T18:24:33Z +Created: 2023-05-09T17:11:49Z ##### Package: package-2 @@ -16,8 +16,8 @@ PackageVersion: 2.0.1 PackageDownloadLocation: NOASSERTION FilesAnalyzed: false PackageSourceInfo: acquired package info from DPKG DB: /some/path/pkg1 -PackageLicenseConcluded: NONE -PackageLicenseDeclared: NONE +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION PackageCopyrightText: NOASSERTION ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:* ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1 @@ -25,12 +25,12 @@ ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1 ##### Package: package-1 PackageName: package-1 -SPDXID: SPDXRef-Package-python-package-1-1b1d0be59ac59d2c +SPDXID: SPDXRef-Package-python-package-1-9265397e5e15168a PackageVersion: 1.0.1 PackageDownloadLocation: NOASSERTION FilesAnalyzed: false PackageSourceInfo: acquired package info from installed python package manifest file: /some/path/pkg1 -PackageLicenseConcluded: MIT +PackageLicenseConcluded: NOASSERTION PackageLicenseDeclared: MIT PackageCopyrightText: NOASSERTION ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:* diff --git a/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden b/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden index 88fbe92b8..867e8e039 100644 --- a/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden +++ b/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden @@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.3 DataLicense: CC0-1.0 SPDXID: SPDXRef-DOCUMENT DocumentName: user-image-input -DocumentNamespace: https://anchore.com/syft/image/user-image-input-4ce1e7c7-642f-4428-bb44-1b48b8edf74d +DocumentNamespace: https://anchore.com/syft/image/user-image-input-44d44a85-2207-4b51-bd73-d0c7b080f6d3 LicenseListVersion: 3.20 Creator: Organization: Anchore, Inc Creator: Tool: syft-v0.42.0-bogus -Created: 2023-05-02T18:24:33Z +Created: 2023-05-09T17:11:49Z ##### Package: package-2 @@ -16,8 +16,8 @@ PackageVersion: 2.0.1 PackageDownloadLocation: NOASSERTION FilesAnalyzed: false PackageSourceInfo: acquired package info from DPKG DB: /somefile-2.txt -PackageLicenseConcluded: NONE -PackageLicenseDeclared: NONE +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION PackageCopyrightText: NOASSERTION ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:* ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1 @@ -25,12 +25,12 @@ ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1 ##### Package: package-1 PackageName: package-1 -SPDXID: SPDXRef-Package-python-package-1-66ba429119b8bec6 +SPDXID: SPDXRef-Package-python-package-1-125840abc1c66dd7 PackageVersion: 1.0.1 PackageDownloadLocation: NOASSERTION FilesAnalyzed: false PackageSourceInfo: acquired package info from installed python package manifest file: /somefile-1.txt -PackageLicenseConcluded: MIT +PackageLicenseConcluded: NOASSERTION PackageLicenseDeclared: MIT PackageCopyrightText: NOASSERTION ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:1:*:*:*:*:*:*:* diff --git a/syft/formats/spdxtagvalue/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/syft/formats/spdxtagvalue/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index b9ddf6e33390ab196d24a941bdda1dc969b30a0e..f4aa1e7bb74e9d8c2cf36a69cd7a6d910a9a7087 100644 GIT binary patch literal 15360 zcmeHOTW{Mo6wdR0g{Qs7Ht&=S>>+Ccv_P>8ZPoz`f`H;B(PB%6Bo_$`|Mwlqj$^w^ zv_!U(q7)36*F*9p$TkutO) zGMZRoBVzjp%@0BhNzM<@{Mr1kQV%d=1aU4!RSrUg9&~uK==LQhMX7S=Qd>!zgKH~q zPxWzI{eL<6^!dZ-?@x6XVT1%@n|i;G19Egj|CkGW&}prcr#f|?_oM5-w-=P}(f0pu z41w16|4)&n4P$Dsf6U3|`i~hV-u@e4-AeW}NK7yY+D?PuFc_=crX|SYM54%0$w{n2 zA$c5QCv6l*1`9{9V-`8eonl!GO+a+Yy<@!;(r!4pXVHvrP9}* zOOvF*z^P$j4})b|Hi5xxCfXdTE)vhmyJh%vY}0i)b?h|N(1}I)lk{saNGHSLVeoDQ zW19y*jMUYlqhGS&WMqr`oWHXCCP}Z3lSOY&(lWmrXGsO}yzXiG+awt-MSWDU9WJWR zYGmF%9_PUyM`!PU?N5rlKg_fm_KP@42P^Sq^dX~8nnxiRT)YI(KFo~gBC2wIos?FW zle|)y8X3VrOa_<-!z7*D2D7agd{D0oFwgZ>em_ZItqO5x~tfH0O`|0@9QFgVwJr+SOR+q?LAhfR6W z9s!SlN5CWSh7f3bSlf~RmlEmyzx$Xbt@Xbf|1TBX@BjCqf9(Cg`+)h)w$a-D+xvf! zAb9ip55chh@8!$H5B~jU;s0$r4Jt?M(r1e!iMj~G?+l?q{qvsMr#!W<=s;|%_#OHG zF!SGk_VUhH`v352`v1f?{yzr%55NBJehQS=NztjCfSqeqEZu;+Z`Zfr5%36j1Uv$- zi-4k9V52c*hB%62hPmd55@sb288OmDSP;mHR2VT4T8<+cQi@{g3{|ez{``jt+dTgv z#+iTr?_*|6gGWdW4{Vy6x3VRk#u}%kqF}61SSr%W@)X&~{DGZIndQL2IiEIFLsDSM zIj@hQ^A;{+s2Lm`AJ@B|ZoU2O^w+n;onJa}MX_G{j?ClF>~2I(3k`lv+s^j zvk#-)VjClsCe9Y+|5?Xh+H4d5gLAsV1xGdi!}C91iuUvB!P@z5!J7=WvKIjB|76G6 znYx;kQrQ`x@|-v75Nb*#yq21SE~R=bAQM!E%t`E2$!Eoco+AMpry L1Uv%Y4FdlHH$Ie? literal 15360 zcmeHOZExE)5ccQ&3QzkQ+a$#|8Q6!eDbNDNQnXnItSAb;CR%ODkmMpkkpI4u>^QNz zR!d|nDMrD7#kV8%9PfNQJ&u_WL{sC4wG4YAF?W_&oG8wCEIH;@I^mcSRx68*XAI#) zS|o9zEfbSL`-Koig0TTwUAy0P?Eur5Au0rp;{hTFBRm+gcG2B?EXzt2u%xb%b|0>* zyq?C#UG@L|#oKqUPJaL1azUGjQ@X47n=&95@8}=nSPX`pb@IJM-L(DB^^eEBpnQXN z_6o6%e<^IHFq!`dK~9-=x|&9qjbfp`(yV%`5m_9dET%h~KG zdOn9|Tiw2ztF!e&-{-UC+?6*if9A#KG&?&^*Qb25t zwjO@nAoKL`_z=B$cKY(y$+9dav)riJq||9Py;I&4ZyTDbRS|BZj}HNK0z2crh^oSz zrA6l z|6xLR&wm8LYrt^+>tS15Mc*Jb+_GsJ-pZbM8e5!}nu4)KVW~+gn^R;f^9LfADldS8 z^Wmyx7?D6RgT~UWm<1Z+>K5ARYHgoA;7t*|kzq624`JM{_M$DZvKrLZwZg1@1)1d9wZf_kg5$BsxsiS)4r zwt4jN`iCRb^kB35oMNuB)Vs3!FMj-?^S1Fn7z57#?bYxRh5KIPz;`}h^CqLc z!~$UbZ`~q4Rc9-oQn|wArO6j=*SvY_orI``@723)J?{d9eROsbfp-RaCZEXdN|lHqt#X%&)JXrUh-m P843Y`fIvXthe6;Ux}S+Y diff --git a/syft/formats/syftjson/encoder_test.go b/syft/formats/syftjson/encoder_test.go index 6f627baf6..c42cc75c4 100644 --- a/syft/formats/syftjson/encoder_test.go +++ b/syft/formats/syftjson/encoder_test.go @@ -61,7 +61,7 @@ func TestEncodeFullJSONDocument(t *testing.T) { FoundBy: "the-cataloger-1", Language: pkg.Python, MetadataType: pkg.PythonPackageMetadataType, - Licenses: []string{"MIT"}, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")), Metadata: pkg.PythonPackageMetadata{ Name: "package-1", Version: "1.0.1", diff --git a/syft/formats/syftjson/model/package.go b/syft/formats/syftjson/model/package.go index 608b2be3c..c739cdada 100644 --- a/syft/formats/syftjson/model/package.go +++ b/syft/formats/syftjson/model/package.go @@ -7,6 +7,7 @@ import ( "reflect" "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/license" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) @@ -27,12 +28,50 @@ type PackageBasicData struct { Type pkg.Type `json:"type"` FoundBy string `json:"foundBy"` Locations []source.Location `json:"locations"` - Licenses []string `json:"licenses"` + Licenses licenses `json:"licenses"` Language pkg.Language `json:"language"` CPEs []string `json:"cpes"` PURL string `json:"purl"` } +type licenses []License + +type License struct { + Value string `json:"value"` + SPDXExpression string `json:"spdxExpression"` + Type license.Type `json:"type"` + URL []string `json:"url"` + Location []source.Location `json:"locations"` +} + +func newModelLicensesFromValues(licenses []string) (ml []License) { + for _, v := range licenses { + expression, err := license.ParseExpression(v) + if err != nil { + log.Trace("could not find valid spdx expression for %s: %w", v, err) + } + ml = append(ml, License{ + Value: v, + SPDXExpression: expression, + Type: license.Declared, + }) + } + return ml +} + +func (f *licenses) UnmarshalJSON(b []byte) error { + var licenses []License + if err := json.Unmarshal(b, &licenses); err != nil { + var simpleLicense []string + if err := json.Unmarshal(b, &simpleLicense); err != nil { + return fmt.Errorf("unable to unmarshal license: %w", err) + } + licenses = newModelLicensesFromValues(simpleLicense) + } + *f = licenses + return nil +} + // PackageCustomData contains ambiguous values (type-wise) from pkg.Package. type PackageCustomData struct { MetadataType pkg.MetadataType `json:"metadataType,omitempty"` diff --git a/syft/formats/syftjson/model/package_test.go b/syft/formats/syftjson/model/package_test.go index 2aa2cda05..8027764d8 100644 --- a/syft/formats/syftjson/model/package_test.go +++ b/syft/formats/syftjson/model/package_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/anchore/syft/syft/license" "github.com/anchore/syft/syft/pkg" ) @@ -30,7 +31,14 @@ func TestUnmarshalPackageGolang(t *testing.T) { "path": "/Users/hal/go/bin/syft" } ], - "licenses": [], + "licenses": [ + { + "value": "MIT", + "spdxExpression": "MIT", + "type": "declared", + "url": [] + } + ], "language": "go", "cpes": [], "purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0", @@ -61,7 +69,20 @@ func TestUnmarshalPackageGolang(t *testing.T) { "path": "/Users/hal/go/bin/syft" } ], - "licenses": [], + "licenses": [ + { + "value": "MIT", + "spdxExpression": "MIT", + "type": "declared", + "url": ["https://www.github.com"] + }, + { + "value": "MIT", + "spdxExpression": "MIT", + "type": "declared", + "locations": [{"path": "/Users/hal/go/bin/syft"}] + } + ], "language": "go", "cpes": [], "purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0" @@ -71,6 +92,83 @@ func TestUnmarshalPackageGolang(t *testing.T) { assert.Empty(t, p.Metadata) }, }, + { + name: "can handle package with []string licenses", + packageData: []byte(`{ + "id": "8b594519bc23da50", + "name": "gopkg.in/square/go-jose.v2", + "version": "v2.6.0", + "type": "go-module", + "foundBy": "go-mod-cataloger", + "locations": [ + { + "path": "/Users/hal/go/bin/syft" + } + ], + "licenses": ["MIT", "Apache-2.0"], + "language": "go", + "cpes": [], + "purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0" + }`), + assert: func(p *Package) { + assert.Equal(t, licenses{ + { + Value: "MIT", + SPDXExpression: "MIT", + Type: license.Declared, + }, + { + Value: "Apache-2.0", + SPDXExpression: "Apache-2.0", + Type: license.Declared, + }, + }, p.Licenses) + }, + }, + { + name: "can handle package with []pkg.License licenses", + packageData: []byte(`{ + "id": "8b594519bc23da50", + "name": "gopkg.in/square/go-jose.v2", + "version": "v2.6.0", + "type": "go-module", + "foundBy": "go-mod-cataloger", + "locations": [ + { + "path": "/Users/hal/go/bin/syft" + } + ], + "licenses": [ + { + "value": "MIT", + "spdxExpression": "MIT", + "type": "declared" + }, + { + "value": "Apache-2.0", + "spdxExpression": "Apache-2.0", + "type": "declared" + } + ], + "language": "go", + "cpes": [], + "purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0" + }`), + assert: func(p *Package) { + assert.Equal(t, licenses{ + { + Value: "MIT", + SPDXExpression: "MIT", + Type: license.Declared, + }, + { + Value: "Apache-2.0", + SPDXExpression: "Apache-2.0", + Type: license.Declared, + }, + }, p.Licenses) + }, + }, } for _, test := range tests { @@ -152,9 +250,6 @@ func Test_unpackMetadata(t *testing.T) { "layerID": "sha256:74ddd0ec08fa43d09f32636ba91a0a3053b02cb4627c35051aff89f853606b59" } ], - "licenses": [ - "GPLv2+" - ], "language": "", "cpes": [ "cpe:2.3:a:centos:acl:2.2.53-1.el8:*:*:*:*:*:*:*", diff --git a/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden b/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden index 5b03f3e9e..1336756c8 100644 --- a/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden +++ b/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden @@ -1,7 +1,7 @@ { "artifacts": [ { - "id": "1b1d0be59ac59d2c", + "id": "9265397e5e15168a", "name": "package-1", "version": "1.0.1", "type": "python", @@ -12,7 +12,13 @@ } ], "licenses": [ - "MIT" + { + "value": "MIT", + "spdxExpression": "MIT", + "type": "declared", + "url": [], + "locations": [] + } ], "language": "python", "cpes": [ @@ -23,7 +29,6 @@ "metadata": { "name": "package-1", "version": "1.0.1", - "license": "", "author": "", "authorEmail": "", "platform": "", @@ -87,5 +92,9 @@ "configuration": { "config-key": "config-value" } + }, + "schema": { + "version": "8.0.0", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-8.0.0.json" } } diff --git a/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden index c8305e5b3..923ff4f51 100644 --- a/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden +++ b/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden @@ -1,7 +1,7 @@ { "artifacts": [ { - "id": "304a5a8e5958a49d", + "id": "271e49ba46e0b601", "name": "package-1", "version": "1.0.1", "type": "python", @@ -12,7 +12,13 @@ } ], "licenses": [ - "MIT" + { + "value": "MIT", + "spdxExpression": "MIT", + "type": "declared", + "url": [], + "locations": [] + } ], "language": "python", "cpes": [ @@ -23,7 +29,6 @@ "metadata": { "name": "package-1", "version": "1.0.1", - "license": "", "author": "", "authorEmail": "", "platform": "", @@ -187,5 +192,9 @@ "configuration": { "config-key": "config-value" } + }, + "schema": { + "version": "8.0.0", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-8.0.0.json" } } diff --git a/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden b/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden index 93c74ea44..efee678a5 100644 --- a/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden +++ b/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden @@ -1,7 +1,7 @@ { "artifacts": [ { - "id": "66ba429119b8bec6", + "id": "125840abc1c66dd7", "name": "package-1", "version": "1.0.1", "type": "python", @@ -9,11 +9,17 @@ "locations": [ { "path": "/somefile-1.txt", - "layerID": "sha256:7e139310bd6ce0956d65a70d26a6d31b240a4f47094a831638f05d381b6c424a" + "layerID": "sha256:ab62016f9bec7286af65604081564cadeeb364a48faca2346c3f5a5a1f5ef777" } ], "licenses": [ - "MIT" + { + "value": "MIT", + "spdxExpression": "MIT", + "type": "declared", + "url": [], + "locations": [] + } ], "language": "python", "cpes": [ @@ -24,7 +30,6 @@ "metadata": { "name": "package-1", "version": "1.0.1", - "license": "", "author": "", "authorEmail": "", "platform": "", @@ -40,7 +45,7 @@ "locations": [ { "path": "/somefile-2.txt", - "layerID": "sha256:cc833bf31a480c064d65ca67ee37f77f0d0c8ab98eedde7b286ad1ef6f5bdcac" + "layerID": "sha256:f1803845b6747d94d6e4ecce2331457e5f1c4fb97de5216f392a76f4582f63b2" } ], "licenses": [], @@ -64,11 +69,11 @@ ], "artifactRelationships": [], "source": { - "id": "0af8fa79f5497297e4e32f3e03de14ac20ad695159df0ac8373e6543614b9a50", + "id": "c8ac88bbaf3d1c036f6a1d601c3d52bafbf05571c97d68322e7cb3a7ecaa304f", "type": "image", "target": { "userInput": "user-image-input", - "imageID": "sha256:0cb4395791986bda17562bd6f76811bb6f163f686e198397197ef8241bed58df", + "imageID": "sha256:a3c61dc134d2f31b415c50324e75842d7f91622f39a89468e51938330b3fd3af", "manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368", "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "tags": [ @@ -78,17 +83,17 @@ "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", - "digest": "sha256:7e139310bd6ce0956d65a70d26a6d31b240a4f47094a831638f05d381b6c424a", + "digest": "sha256:ab62016f9bec7286af65604081564cadeeb364a48faca2346c3f5a5a1f5ef777", "size": 22 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", - "digest": "sha256:cc833bf31a480c064d65ca67ee37f77f0d0c8ab98eedde7b286ad1ef6f5bdcac", + "digest": "sha256:f1803845b6747d94d6e4ecce2331457e5f1c4fb97de5216f392a76f4582f63b2", "size": 16 } ], - "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NzEsImRpZ2VzdCI6InNoYTI1NjowY2I0Mzk1NzkxOTg2YmRhMTc1NjJiZDZmNzY4MTFiYjZmMTYzZjY4NmUxOTgzOTcxOTdlZjgyNDFiZWQ1OGRmIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1Njo3ZTEzOTMxMGJkNmNlMDk1NmQ2NWE3MGQyNmE2ZDMxYjI0MGE0ZjQ3MDk0YTgzMTYzOGYwNWQzODFiNmM0MjRhIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OmNjODMzYmYzMWE0ODBjMDY0ZDY1Y2E2N2VlMzdmNzdmMGQwYzhhYjk4ZWVkZGU3YjI4NmFkMWVmNmY1YmRjYWMifV19", - "config": "eyJhcmNoaXRlY3R1cmUiOiJhcm02NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjMtMDQtMThUMTQ6MDk6NDIuMzAxMDI2MzhaIiwiaGlzdG9yeSI6W3siY3JlYXRlZCI6IjIwMjMtMDQtMThUMTQ6MDk6NDIuMjg3OTQyNzEzWiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMy0wNC0xOFQxNDowOTo0Mi4zMDEwMjYzOFoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMi50eHQgL3NvbWVmaWxlLTIudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9XSwib3MiOiJsaW51eCIsInJvb3RmcyI6eyJ0eXBlIjoibGF5ZXJzIiwiZGlmZl9pZHMiOlsic2hhMjU2OjdlMTM5MzEwYmQ2Y2UwOTU2ZDY1YTcwZDI2YTZkMzFiMjQwYTRmNDcwOTRhODMxNjM4ZjA1ZDM4MWI2YzQyNGEiLCJzaGEyNTY6Y2M4MzNiZjMxYTQ4MGMwNjRkNjVjYTY3ZWUzN2Y3N2YwZDBjOGFiOThlZWRkZTdiMjg2YWQxZWY2ZjViZGNhYyJdfX0=", + "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NzMsImRpZ2VzdCI6InNoYTI1NjphM2M2MWRjMTM0ZDJmMzFiNDE1YzUwMzI0ZTc1ODQyZDdmOTE2MjJmMzlhODk0NjhlNTE5MzgzMzBiM2ZkM2FmIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjphYjYyMDE2ZjliZWM3Mjg2YWY2NTYwNDA4MTU2NGNhZGVlYjM2NGE0OGZhY2EyMzQ2YzNmNWE1YTFmNWVmNzc3In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OmYxODAzODQ1YjY3NDdkOTRkNmU0ZWNjZTIzMzE0NTdlNWYxYzRmYjk3ZGU1MjE2ZjM5MmE3NmY0NTgyZjYzYjIifV19", + "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjMtMDQtMjFUMTk6MTA6MzcuNjUxODMxMjM0WiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIzLTA0LTIxVDE5OjEwOjM3LjYwNzYxMzU1NVoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMS50eHQgL3NvbWVmaWxlLTEudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjMtMDQtMjFUMTk6MTA6MzcuNjUxODMxMjM0WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6YWI2MjAxNmY5YmVjNzI4NmFmNjU2MDQwODE1NjRjYWRlZWIzNjRhNDhmYWNhMjM0NmMzZjVhNWExZjVlZjc3NyIsInNoYTI1NjpmMTgwMzg0NWI2NzQ3ZDk0ZDZlNGVjY2UyMzMxNDU3ZTVmMWM0ZmI5N2RlNTIxNmYzOTJhNzZmNDU4MmY2M2IyIl19fQ==", "repoDigests": [], "architecture": "", "os": "" @@ -110,5 +115,9 @@ "configuration": { "config-key": "config-value" } + }, + "schema": { + "version": "8.0.0", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-8.0.0.json" } } diff --git a/syft/formats/syftjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/syft/formats/syftjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index 9455ea930505e90c2e18f1f311a3ec9f5b3b49a4..f4aa1e7bb74e9d8c2cf36a69cd7a6d910a9a7087 100644 GIT binary patch literal 15360 zcmeHOTW{Mo6wdR0g{Qs7Ht&=S>>+Ccv_P>8ZPoz`f`H;B(PB%6Bo_$`|Mwlqj$^w^ zv_!U(q7)36*F*9p$TkutO) zGMZRoBVzjp%@0BhNzM<@{Mr1kQV%d=1aU4!RSrUg9&~uK==LQhMX7S=Qd>!zgKH~q zPxWzI{eL<6^!dZ-?@x6XVT1%@n|i;G19Egj|CkGW&}prcr#f|?_oM5-w-=P}(f0pu z41w16|4)&n4P$Dsf6U3|`i~hV-u@e4-AeW}NK7yY+D?PuFc_=crX|SYM54%0$w{n2 zA$c5QCv6l*1`9{9V-`8eonl!GO+a+Yy<@!;(r!4pXVHvrP9}* zOOvF*z^P$j4})b|Hi5xxCfXdTE)vhmyJh%vY}0i)b?h|N(1}I)lk{saNGHSLVeoDQ zW19y*jMUYlqhGS&WMqr`oWHXCCP}Z3lSOY&(lWmrXGsO}yzXiG+awt-MSWDU9WJWR zYGmF%9_PUyM`!PU?N5rlKg_fm_KP@42P^Sq^dX~8nnxiRT)YI(KFo~gBC2wIos?FW zle|)y8X3VrOa_<-!z7*D2D7agd{D0oFwgZ>em_ZItqO5x~tfH0O`|0@9QFgVwJr+SOR+q?LAhfR6W z9s!SlN5CWSh7f3bSlf~RmlEmyzx$Xbt@Xbf|1TBX@BjCqf9(Cg`+)h)w$a-D+xvf! zAb9ip55chh@8!$H5B~jU;s0$r4Jt?M(r1e!iMj~G?+l?q{qvsMr#!W<=s;|%_#OHG zF!SGk_VUhH`v352`v1f?{yzr%55NBJehQS=NztjCfSqeqEZu;+Z`Zfr5%36j1Uv$- zi-4k9V52c*hB%62hPmd55@sb288OmDSP;mHR2VT4T8<+cQi@{g3{|ez{``jt+dTgv z#+iTr?_*|6gGWdW4{Vy6x3VRk#u}%kqF}61SSr%W@)X&~{DGZIndQL2IiEIFLsDSM zIj@hQ^A;{+s2Lm`AJ@B|ZoU2O^w+n;onJa}MX_G{j?ClF>~2I(3k`lv+s^j zvk#-)VjClsCe9Y+|5?Xh+H4d5gLAsV1xGdi!}C91iuUvB!P@z5!J7=WvKIjB|76G6 znYx;kQrQ`x@|-v75Nb*#yq21SE~R=bAQM!E%t`E2$!Eoco+AMpry L1Uv%Y4FdlHH$Ie? literal 15360 zcmeHOZBN@s5YFfO6*=EmlI;7gBi#qCAhl9;R4vz&u0m+NFW@7wBipG!i2r_LCxs+E z2*DwxVk0E7-ksUW%+7e`@fewe(U{8^$4VqtW61?ctngAOjFUuoET|VsI9NbqiDT)! zA`Bp%dXb-U-R>znPQ9NX%PvIyLLDfG%d7hS4UR;4_*K4SVk})F^)0%9)!D#}N(f+|f z*QX=b!~N>A>JG|$>=wqqb(31Si?nJKG9J6E0;+Ep`!+WhuBZw6Ur}_kw@#2n@&8{A zvLeT9yrCSR=ez_tM`<>_1j&m$uRL71j;gCkZR3%?azzOvP5WMDNYDOnt3Ks%(S8q9F#}gQiT&?7-1T?&N&&QC`~-Ynki%; z0stqYg>;TeFQrEo8Ksk0IcKet2~k2@?7Z+iu|}Kd}cdEC`m8xT1!;hcd9sT)`9oF3c!TfaD{sf}D4EO(sbh@oM+;5%vY)h2D=Lcw@ zwg0!z|KWy%SNtCd0r7t)C-arZ|LZ8NlkK+0pw5qPGk2&FQ4eAGJ4R^O{56?(o>e-{ zTyc6=TfEK7D%0aSDFqe>`4|@5jsGfLT_?cIepP96{;9H1Mhv|km2Tl$K837&^`a|) zbbj%;f@FXG{b=H{Wj@X5C^I0$d`(mF)CI|2^kzJ-P4=R9V|_Lk^m9I%jvb`Uvqei) z4M=;KRmIgLPwQ%7wqmKw*^A!m#Es_UQ1mQh>$}c)+Zuk*)Dr!=hQ9Z+=KjyV%l`-# zhz0+@SD{;+Sgv@t$asO(8v7$C>6`e^A?zcBI|yURNU;Bc3ZLz31u#kjfppJx*@eJd z0me~2+jfC*&_K{Y&_K|@3(l@red}5IqIU*%-;$4-!8QLa`$NM|LyTV%<*^6 z|GDJB|95d}=J;RRV*sGnL>IHi<82`&;B|*3uImD*E7kxiHc;gXsQ6QWtXpa;*&L;< zdrvlnY47Qj`GM$d`v}q2@t+C*<)Qzpy=@=E;THdGI*lGfvH6wzc@Vq zeX6=QqJ!r7H}g*a2yNqC5U8s@x=DVl&t_fal`C9cntbAVKE148GU%msBTcVN$CHs8 zsygd493!O?Yl&oDY8AUUcF;B)BM%M8hQ&@2?+x@EOC*hjmr}qiVwklJ2ks@`^_SAo!|C77k@TM0W{9lOR|2sJX m*HO8~L+kfOT07~mwKGLGH!stJc4DTmNzg#hK+wR}HSiykm4wCs diff --git a/syft/formats/syftjson/to_format_model.go b/syft/formats/syftjson/to_format_model.go index c242b4eef..67feccc17 100644 --- a/syft/formats/syftjson/to_format_model.go +++ b/syft/formats/syftjson/to_format_model.go @@ -184,6 +184,24 @@ func toPackageModels(catalog *pkg.Collection) []model.Package { return artifacts } +func toLicenseModel(pkgLicenses []pkg.License) (modelLicenses []model.License) { + for _, l := range pkgLicenses { + // guarantee collection + locations := make([]source.Location, 0) + if v := l.Location.ToSlice(); v != nil { + locations = v + } + modelLicenses = append(modelLicenses, model.License{ + Value: l.Value, + SPDXExpression: l.SPDXExpression, + Type: l.Type, + URL: l.URL.ToSlice(), + Location: locations, + }) + } + return +} + // toPackageModel crates a new Package from the given pkg.Package. func toPackageModel(p pkg.Package) model.Package { var cpes = make([]string, len(p.CPEs)) @@ -191,9 +209,11 @@ func toPackageModel(p pkg.Package) model.Package { cpes[i] = cpe.String(c) } - var licenses = make([]string, 0) - if p.Licenses != nil { - licenses = p.Licenses + // we want to make sure all catalogers are + // initializing the array; this is a good choke point for this check + var licenses = make([]model.License, 0) + if !p.Licenses.Empty() { + licenses = toLicenseModel(p.Licenses.ToSlice()) } return model.Package{ diff --git a/syft/formats/syftjson/to_syft_model.go b/syft/formats/syftjson/to_syft_model.go index 93bc561d9..bfbc53866 100644 --- a/syft/formats/syftjson/to_syft_model.go +++ b/syft/formats/syftjson/to_syft_model.go @@ -9,6 +9,7 @@ import ( "github.com/google/go-cmp/cmp" stereoscopeFile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" @@ -101,6 +102,19 @@ func toSyftFiles(files []model.File) sbom.Artifacts { return ret } +func toSyftLicenses(m []model.License) (p []pkg.License) { + for _, l := range m { + p = append(p, pkg.License{ + Value: l.Value, + SPDXExpression: l.SPDXExpression, + Type: l.Type, + URL: internal.NewStringSet(l.URL...), + Location: source.NewLocationSet(l.Location...), + }) + } + return +} + func toSyftFileType(ty string) stereoscopeFile.Type { switch ty { case "SymbolicLink": @@ -304,7 +318,7 @@ func toSyftPackage(p model.Package, idAliases map[string]string) pkg.Package { Version: p.Version, FoundBy: p.FoundBy, Locations: source.NewLocationSet(p.Locations...), - Licenses: p.Licenses, + Licenses: pkg.NewLicenseSet(toSyftLicenses(p.Licenses)...), Language: p.Language, Type: p.Type, CPEs: cpes, diff --git a/syft/license/license.go b/syft/license/license.go new file mode 100644 index 000000000..e9dd93c62 --- /dev/null +++ b/syft/license/license.go @@ -0,0 +1,35 @@ +// package license provides common methods for working with SPDX license data +package license + +import ( + "fmt" + + "github.com/github/go-spdx/v2/spdxexp" + + "github.com/anchore/syft/internal/spdxlicense" +) + +type Type string + +const ( + Declared Type = "declared" + Concluded Type = "concluded" +) + +func ParseExpression(expression string) (string, error) { + licenseID, exists := spdxlicense.ID(expression) + if exists { + return licenseID, nil + } + + // If it doesn't exist initially in the SPDX list it might be a more complex expression + // ignored variable is any invalid expressions + // TODO: contribute to spdxexp to expose deprecated license IDs + // https://github.com/anchore/syft/issues/1814 + valid, _ := spdxexp.ValidateLicenses([]string{expression}) + if !valid { + return "", fmt.Errorf("failed to validate spdx expression: %s", expression) + } + + return expression, nil +} diff --git a/syft/license/license_test.go b/syft/license/license_test.go new file mode 100644 index 000000000..6387f38da --- /dev/null +++ b/syft/license/license_test.go @@ -0,0 +1,42 @@ +package license + +import "testing" + +func TestParseExpression(t *testing.T) { + tests := []struct { + name string + expression string + want string + wantErr bool + }{ + { + name: "valid single ID expression returns SPDX ID", + expression: "mit", + want: "MIT", + wantErr: false, + }, + { + name: "Valid SPDX expression returns SPDX expression", + expression: "MIT OR Apache-2.0", + want: "MIT OR Apache-2.0", + }, + { + name: "Invalid SPDX expression returns error", + expression: "MIT OR Apache-2.0 OR invalid", + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseExpression(tt.expression) + if (err != nil) != tt.wantErr { + t.Errorf("ParseExpression() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ParseExpression() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/syft/pkg/alpm_metadata.go b/syft/pkg/alpm_metadata.go index 14c4f6eb2..ff65078f9 100644 --- a/syft/pkg/alpm_metadata.go +++ b/syft/pkg/alpm_metadata.go @@ -20,13 +20,12 @@ type AlpmMetadata struct { Description string `mapstructure:"desc" json:"description" cyclonedx:"description"` Architecture string `mapstructure:"arch" json:"architecture" cyclonedx:"architecture"` Size int `mapstructure:"size" json:"size" cyclonedx:"size"` - Packager string `mapstructure:"packager" json:"packager" cyclonedx:"packager"` - License string `mapstructure:"license" json:"license" cyclonedx:"license"` - URL string `mapstructure:"url" json:"url" cyclonedx:"url"` - Validation string `mapstructure:"validation" json:"validation" cyclonedx:"validation"` - Reason int `mapstructure:"reason" json:"reason" cyclonedx:"reason"` - Files []AlpmFileRecord `mapstructure:"files" json:"files" cyclonedx:"files"` - Backup []AlpmFileRecord `mapstructure:"backup" json:"backup" cyclonedx:"backup"` + Packager string `mapstructure:"packager" json:"packager"` + URL string `mapstructure:"url" json:"url"` + Validation string `mapstructure:"validation" json:"validation"` + Reason int `mapstructure:"reason" json:"reason"` + Files []AlpmFileRecord `mapstructure:"files" json:"files"` + Backup []AlpmFileRecord `mapstructure:"backup" json:"backup"` } type AlpmFileRecord struct { diff --git a/syft/pkg/apk_metadata.go b/syft/pkg/apk_metadata.go index 21abd50df..4a3b55ab7 100644 --- a/syft/pkg/apk_metadata.go +++ b/syft/pkg/apk_metadata.go @@ -27,7 +27,6 @@ type ApkMetadata struct { OriginPackage string `mapstructure:"o" json:"originPackage" cyclonedx:"originPackage"` Maintainer string `mapstructure:"m" json:"maintainer"` Version string `mapstructure:"V" json:"version"` - License string `mapstructure:"L" json:"license"` Architecture string `mapstructure:"A" json:"architecture"` URL string `mapstructure:"U" json:"url"` Description string `mapstructure:"T" json:"description"` diff --git a/syft/pkg/apk_metadata_test.go b/syft/pkg/apk_metadata_test.go index a5e5f4121..82c1d58ca 100644 --- a/syft/pkg/apk_metadata_test.go +++ b/syft/pkg/apk_metadata_test.go @@ -47,7 +47,6 @@ func TestApkMetadata_UnmarshalJSON(t *testing.T) { OriginPackage: "pax-utils", Maintainer: "Natanael Copa ", Version: "1.3.4-r0", - License: "GPL-2.0-only", Architecture: "x86_64", URL: "https://wiki.gentoo.org/wiki/Hardened/PaX_Utilities", Description: "Scan ELF binaries for stuff", @@ -86,7 +85,6 @@ func TestApkMetadata_UnmarshalJSON(t *testing.T) { OriginPackage: "pax-utils", Maintainer: "Natanael Copa ", Version: "1.3.4-r0", - License: "GPL-2.0-only", Architecture: "x86_64", URL: "https://wiki.gentoo.org/wiki/Hardened/PaX_Utilities", Description: "Scan ELF binaries for stuff", diff --git a/syft/pkg/catalog_test.go b/syft/pkg/catalog_test.go index 897616ca5..5271ac262 100644 --- a/syft/pkg/catalog_test.go +++ b/syft/pkg/catalog_test.go @@ -17,6 +17,49 @@ type expectedIndexes struct { byPath map[string]*strset.Set } +func TestCatalogMergePackageLicenses(t *testing.T) { + tests := []struct { + name string + pkgs []Package + expectedPkgs []Package + }{ + { + name: "merges licenses of packages with equal ID", + pkgs: []Package{ + { + id: "equal", + Licenses: NewLicenseSet( + NewLicensesFromValues("foo", "baq", "quz")..., + ), + }, + { + id: "equal", + Licenses: NewLicenseSet( + NewLicensesFromValues("bar", "baz", "foo", "qux")..., + ), + }, + }, + expectedPkgs: []Package{ + { + id: "equal", + Licenses: NewLicenseSet( + NewLicensesFromValues("foo", "baq", "quz", "qux", "bar", "baz")..., + ), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + collection := NewCollection(test.pkgs...) + for i, p := range collection.Sorted() { + assert.Equal(t, test.expectedPkgs[i].Licenses, p.Licenses) + } + }) + } +} + func TestCatalogDeleteRemovesPackages(t *testing.T) { tests := []struct { name string diff --git a/syft/pkg/cataloger/alpm/cataloger_test.go b/syft/pkg/cataloger/alpm/cataloger_test.go index 65447d66d..1dedded2e 100644 --- a/syft/pkg/cataloger/alpm/cataloger_test.go +++ b/syft/pkg/cataloger/alpm/cataloger_test.go @@ -13,15 +13,18 @@ import ( ) func TestAlpmCataloger(t *testing.T) { - + dbLocation := source.NewLocation("var/lib/pacman/local/gmp-6.2.1-2/desc") expectedPkgs := []pkg.Package{ { - Name: "gmp", - Version: "6.2.1-2", - Type: pkg.AlpmPkg, - FoundBy: "alpmdb-cataloger", - Licenses: []string{"LGPL3", "GPL"}, - Locations: source.NewLocationSet(source.NewLocation("var/lib/pacman/local/gmp-6.2.1-2/desc")), + Name: "gmp", + Version: "6.2.1-2", + Type: pkg.AlpmPkg, + FoundBy: "alpmdb-cataloger", + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("LGPL3", dbLocation), + pkg.NewLicenseFromLocations("GPL", dbLocation), + ), + Locations: source.NewLocationSet(dbLocation), CPEs: nil, PURL: "", MetadataType: "AlpmMetadata", @@ -33,7 +36,6 @@ func TestAlpmCataloger(t *testing.T) { Architecture: "x86_64", Size: 1044438, Packager: "Antonio Rojas ", - License: "LGPL3\nGPL", URL: "https://gmplib.org/", Validation: "pgp", Reason: 1, diff --git a/syft/pkg/cataloger/alpm/package.go b/syft/pkg/cataloger/alpm/package.go index 1a5bdf131..2c85db47c 100644 --- a/syft/pkg/cataloger/alpm/package.go +++ b/syft/pkg/cataloger/alpm/package.go @@ -1,29 +1,33 @@ package alpm import ( + "strings" + "github.com/anchore/packageurl-go" - "github.com/anchore/syft/internal" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) -func newPackage(m pkg.AlpmMetadata, release *linux.Release, locations ...source.Location) pkg.Package { +func newPackage(m *parsedData, release *linux.Release, dbLocation source.Location) pkg.Package { + licenseCandidates := strings.Split(m.Licenses, "\n") + p := pkg.Package{ Name: m.Package, Version: m.Version, - Locations: source.NewLocationSet(locations...), + Locations: source.NewLocationSet(dbLocation), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(dbLocation.WithoutAnnotations(), licenseCandidates...)...), Type: pkg.AlpmPkg, - Licenses: internal.SplitAny(m.License, " \n"), PURL: packageURL(m, release), MetadataType: pkg.AlpmMetadataType, - Metadata: m, + Metadata: m.AlpmMetadata, } p.SetID() + return p } -func packageURL(m pkg.AlpmMetadata, distro *linux.Release) string { +func packageURL(m *parsedData, distro *linux.Release) string { if distro == nil || distro.ID != "arch" { // note: there is no namespace variation (like with debian ID_LIKE for ubuntu ID, for example) return "" diff --git a/syft/pkg/cataloger/alpm/package_test.go b/syft/pkg/cataloger/alpm/package_test.go index bb88a749c..e41e88791 100644 --- a/syft/pkg/cataloger/alpm/package_test.go +++ b/syft/pkg/cataloger/alpm/package_test.go @@ -13,16 +13,19 @@ import ( func Test_PackageURL(t *testing.T) { tests := []struct { name string - metadata pkg.AlpmMetadata + metadata *parsedData distro linux.Release expected string }{ { name: "bad distro id", - metadata: pkg.AlpmMetadata{ - Package: "p", - Version: "v", - Architecture: "a", + metadata: &parsedData{ + Licenses: "", + AlpmMetadata: pkg.AlpmMetadata{ + Package: "p", + Version: "v", + Architecture: "a", + }, }, distro: linux.Release{ ID: "something-else", @@ -32,10 +35,13 @@ func Test_PackageURL(t *testing.T) { }, { name: "gocase", - metadata: pkg.AlpmMetadata{ - Package: "p", - Version: "v", - Architecture: "a", + metadata: &parsedData{ + Licenses: "", + AlpmMetadata: pkg.AlpmMetadata{ + Package: "p", + Version: "v", + Architecture: "a", + }, }, distro: linux.Release{ ID: "arch", @@ -45,9 +51,12 @@ func Test_PackageURL(t *testing.T) { }, { name: "missing architecture", - metadata: pkg.AlpmMetadata{ - Package: "p", - Version: "v", + metadata: &parsedData{ + Licenses: "", + AlpmMetadata: pkg.AlpmMetadata{ + Package: "p", + Version: "v", + }, }, distro: linux.Release{ ID: "arch", @@ -55,10 +64,13 @@ func Test_PackageURL(t *testing.T) { expected: "pkg:alpm/arch/p@v?distro=arch", }, { - metadata: pkg.AlpmMetadata{ - Package: "python", - Version: "3.10.0", - Architecture: "any", + metadata: &parsedData{ + Licenses: "", + AlpmMetadata: pkg.AlpmMetadata{ + Package: "python", + Version: "3.10.0", + Architecture: "any", + }, }, distro: linux.Release{ ID: "arch", @@ -67,10 +79,13 @@ func Test_PackageURL(t *testing.T) { expected: "pkg:alpm/arch/python@3.10.0?arch=any&distro=arch-rolling", }, { - metadata: pkg.AlpmMetadata{ - Package: "g plus plus", - Version: "v84", - Architecture: "x86_64", + metadata: &parsedData{ + Licenses: "", + AlpmMetadata: pkg.AlpmMetadata{ + Package: "g plus plus", + Version: "v84", + Architecture: "x86_64", + }, }, distro: linux.Release{ ID: "arch", @@ -80,11 +95,14 @@ func Test_PackageURL(t *testing.T) { }, { name: "add source information as qualifier", - metadata: pkg.AlpmMetadata{ - Package: "p", - Version: "v", - Architecture: "a", - BasePackage: "origin", + metadata: &parsedData{ + Licenses: "", + AlpmMetadata: pkg.AlpmMetadata{ + Package: "p", + Version: "v", + Architecture: "a", + BasePackage: "origin", + }, }, distro: linux.Release{ ID: "arch", diff --git a/syft/pkg/cataloger/alpm/parse_alpm_db.go b/syft/pkg/cataloger/alpm/parse_alpm_db.go index 7c57ace45..987a52c07 100644 --- a/syft/pkg/cataloger/alpm/parse_alpm_db.go +++ b/syft/pkg/cataloger/alpm/parse_alpm_db.go @@ -31,8 +31,13 @@ var ( } ) +type parsedData struct { + Licenses string `mapstructure:"license"` + pkg.AlpmMetadata `mapstructure:",squash"` +} + func parseAlpmDB(resolver source.FileResolver, env *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { - metadata, err := parseAlpmDBEntry(reader) + data, err := parseAlpmDBEntry(reader) if err != nil { return nil, nil, err } @@ -48,9 +53,10 @@ func parseAlpmDB(resolver source.FileResolver, env *generic.Environment, reader return nil, nil, err } - // The replace the files found the the pacman database with the files from the mtree These contain more metadata and + // replace the files found the pacman database with the files from the mtree These contain more metadata and // thus more useful. - metadata.Files = pkgFiles + // TODO: probably want to use MTREE and PKGINFO here + data.Files = pkgFiles // We only really do this to get any backup database entries from the files database files := filepath.Join(base, "files") @@ -62,23 +68,23 @@ func parseAlpmDB(resolver source.FileResolver, env *generic.Environment, reader if err != nil { return nil, nil, err } else if filesMetadata != nil { - metadata.Backup = filesMetadata.Backup + data.Backup = filesMetadata.Backup } - if metadata.Package == "" { + if data.Package == "" { return nil, nil, nil } return []pkg.Package{ newPackage( - *metadata, + data, env.LinuxRelease, reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), ), }, nil, nil } -func parseAlpmDBEntry(reader io.Reader) (*pkg.AlpmMetadata, error) { +func parseAlpmDBEntry(reader io.Reader) (*parsedData, error) { scanner := newScanner(reader) metadata, err := parseDatabase(scanner) if err != nil { @@ -128,8 +134,7 @@ func getFileReader(path string, resolver source.FileResolver) (io.Reader, error) return dbContentReader, nil } -func parseDatabase(b *bufio.Scanner) (*pkg.AlpmMetadata, error) { - var entry pkg.AlpmMetadata +func parseDatabase(b *bufio.Scanner) (*parsedData, error) { var err error pkgFields := make(map[string]interface{}) for b.Scan() { @@ -181,16 +186,23 @@ func parseDatabase(b *bufio.Scanner) (*pkg.AlpmMetadata, error) { pkgFields[key] = value } } + + return parsePkgFiles(pkgFields) +} + +func parsePkgFiles(pkgFields map[string]interface{}) (*parsedData, error) { + var entry parsedData if err := mapstructure.Decode(pkgFields, &entry); err != nil { return nil, fmt.Errorf("unable to parse ALPM metadata: %w", err) } - if entry.Package == "" && len(entry.Files) == 0 && len(entry.Backup) == 0 { - return nil, nil - } if entry.Backup == nil { entry.Backup = make([]pkg.AlpmFileRecord, 0) } + + if entry.Package == "" && len(entry.Files) == 0 && len(entry.Backup) == 0 { + return nil, nil + } return &entry, nil } diff --git a/syft/pkg/cataloger/apkdb/package.go b/syft/pkg/cataloger/apkdb/package.go index bad434d55..392fe2771 100644 --- a/syft/pkg/cataloger/apkdb/package.go +++ b/syft/pkg/cataloger/apkdb/package.go @@ -9,16 +9,18 @@ import ( "github.com/anchore/syft/syft/source" ) -func newPackage(d pkg.ApkMetadata, release *linux.Release, locations ...source.Location) pkg.Package { +func newPackage(d parsedData, release *linux.Release, dbLocation source.Location) pkg.Package { + licenseStrings := strings.Split(d.License, " ") + p := pkg.Package{ Name: d.Package, Version: d.Version, - Locations: source.NewLocationSet(locations...), - Licenses: strings.Split(d.License, " "), - PURL: packageURL(d, release), + Locations: source.NewLocationSet(dbLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(dbLocation, licenseStrings...)...), + PURL: packageURL(d.ApkMetadata, release), Type: pkg.ApkPkg, MetadataType: pkg.ApkMetadataType, - Metadata: d, + Metadata: d.ApkMetadata, } p.SetID() diff --git a/syft/pkg/cataloger/apkdb/package_test.go b/syft/pkg/cataloger/apkdb/package_test.go index 8a25ebb98..9ebe4e4dd 100644 --- a/syft/pkg/cataloger/apkdb/package_test.go +++ b/syft/pkg/cataloger/apkdb/package_test.go @@ -15,16 +15,19 @@ import ( func Test_PackageURL(t *testing.T) { tests := []struct { name string - metadata pkg.ApkMetadata + metadata parsedData distro linux.Release expected string }{ { name: "non-alpine distro", - metadata: pkg.ApkMetadata{ - Package: "p", - Version: "v", - Architecture: "a", + metadata: parsedData{ + License: "", + ApkMetadata: pkg.ApkMetadata{ + Package: "p", + Version: "v", + Architecture: "a", + }, }, distro: linux.Release{ ID: "something else", @@ -34,10 +37,13 @@ func Test_PackageURL(t *testing.T) { }, { name: "gocase", - metadata: pkg.ApkMetadata{ - Package: "p", - Version: "v", - Architecture: "a", + metadata: parsedData{ + License: "", + ApkMetadata: pkg.ApkMetadata{ + Package: "p", + Version: "v", + Architecture: "a", + }, }, distro: linux.Release{ ID: "alpine", @@ -47,9 +53,12 @@ func Test_PackageURL(t *testing.T) { }, { name: "missing architecture", - metadata: pkg.ApkMetadata{ - Package: "p", - Version: "v", + metadata: parsedData{ + License: "", + ApkMetadata: pkg.ApkMetadata{ + Package: "p", + Version: "v", + }, }, distro: linux.Release{ ID: "alpine", @@ -59,10 +68,13 @@ func Test_PackageURL(t *testing.T) { }, // verify #351 { - metadata: pkg.ApkMetadata{ - Package: "g++", - Version: "v84", - Architecture: "am86", + metadata: parsedData{ + License: "", + ApkMetadata: pkg.ApkMetadata{ + Package: "g++", + Version: "v84", + Architecture: "am86", + }, }, distro: linux.Release{ ID: "alpine", @@ -71,10 +83,13 @@ func Test_PackageURL(t *testing.T) { expected: "pkg:apk/alpine/g++@v84?arch=am86&distro=alpine-3.4.6", }, { - metadata: pkg.ApkMetadata{ - Package: "g plus plus", - Version: "v84", - Architecture: "am86", + metadata: parsedData{ + License: "", + ApkMetadata: pkg.ApkMetadata{ + Package: "g plus plus", + Version: "v84", + Architecture: "am86", + }, }, distro: linux.Release{ ID: "alpine", @@ -84,11 +99,14 @@ func Test_PackageURL(t *testing.T) { }, { name: "add source information as qualifier", - metadata: pkg.ApkMetadata{ - Package: "p", - Version: "v", - Architecture: "a", - OriginPackage: "origin", + metadata: parsedData{ + License: "", + ApkMetadata: pkg.ApkMetadata{ + Package: "p", + Version: "v", + Architecture: "a", + OriginPackage: "origin", + }, }, distro: linux.Release{ ID: "alpine", @@ -98,10 +116,13 @@ func Test_PackageURL(t *testing.T) { }, { name: "wolfi distro", - metadata: pkg.ApkMetadata{ - Package: "p", - Version: "v", - Architecture: "a", + metadata: parsedData{ + License: "", + ApkMetadata: pkg.ApkMetadata{ + Package: "p", + Version: "v", + Architecture: "a", + }, }, distro: linux.Release{ ID: "wolfi", @@ -113,7 +134,7 @@ func Test_PackageURL(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - actual := packageURL(test.metadata, &test.distro) + actual := packageURL(test.metadata.ApkMetadata, &test.distro) if actual != test.expected { dmp := diffmatchpatch.New() diffs := dmp.DiffMain(test.expected, actual, true) diff --git a/syft/pkg/cataloger/apkdb/parse_apk_db.go b/syft/pkg/cataloger/apkdb/parse_apk_db.go index 3409524f4..748ed7d58 100644 --- a/syft/pkg/cataloger/apkdb/parse_apk_db.go +++ b/syft/pkg/cataloger/apkdb/parse_apk_db.go @@ -26,6 +26,11 @@ var ( repoRegex = regexp.MustCompile(`(?m)^https://.*\.alpinelinux\.org/alpine/v([^/]+)/([a-zA-Z0-9_]+)$`) ) +type parsedData struct { + License string `mapstructure:"L" json:"license"` + pkg.ApkMetadata +} + // parseApkDB parses packages from a given APK installed DB file. For more // information on specific fields, see https://wiki.alpinelinux.org/wiki/Apk_spec. // @@ -33,15 +38,15 @@ var ( func parseApkDB(resolver source.FileResolver, env *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { scanner := bufio.NewScanner(reader) - var apks []pkg.ApkMetadata - var currentEntry pkg.ApkMetadata + var apks []parsedData + var currentEntry parsedData entryParsingInProgress := false fileParsingCtx := newApkFileParsingContext() // creating a dedicated append-like function here instead of using `append(...)` // below since there is nontrivial logic to be performed for each finalized apk // entry. - appendApk := func(p pkg.ApkMetadata) { + appendApk := func(p parsedData) { if files := fileParsingCtx.files; len(files) >= 1 { // attached accumulated files to current package p.Files = files @@ -68,7 +73,7 @@ func parseApkDB(resolver source.FileResolver, env *generic.Environment, reader s entryParsingInProgress = false // zero-out currentEntry for use by any future entry - currentEntry = pkg.ApkMetadata{} + currentEntry = parsedData{} continue } @@ -123,7 +128,7 @@ func parseApkDB(resolver source.FileResolver, env *generic.Environment, reader s pkgs := make([]pkg.Package, 0, len(apks)) for _, apk := range apks { - pkgs = append(pkgs, newPackage(apk, r, reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))) + pkgs = append(pkgs, newPackage(apk, r, reader.Location)) } return pkgs, discoverPackageDependencies(pkgs), nil @@ -201,7 +206,7 @@ type apkField struct { } //nolint:funlen -func (f apkField) apply(p *pkg.ApkMetadata, ctx *apkFileParsingContext) { +func (f apkField) apply(p *parsedData, ctx *apkFileParsingContext) { switch f.name { // APKINDEX field parsing @@ -347,7 +352,7 @@ func parseListValue(value string) []string { return nil } -func nilFieldsToEmptySlice(p *pkg.ApkMetadata) { +func nilFieldsToEmptySlice(p *parsedData) { if p.Dependencies == nil { p.Dependencies = []string{} } diff --git a/syft/pkg/cataloger/apkdb/parse_apk_db_test.go b/syft/pkg/cataloger/apkdb/parse_apk_db_test.go index 60f40c98a..ba26eb4f0 100644 --- a/syft/pkg/cataloger/apkdb/parse_apk_db_test.go +++ b/syft/pkg/cataloger/apkdb/parse_apk_db_test.go @@ -80,83 +80,93 @@ func TestExtraFileAttributes(t *testing.T) { func TestSinglePackageDetails(t *testing.T) { tests := []struct { fixture string - expected pkg.ApkMetadata + expected pkg.Package }{ { fixture: "test-fixtures/single", - expected: pkg.ApkMetadata{ - Package: "musl-utils", - OriginPackage: "musl", - Version: "1.1.24-r2", - Description: "the musl c library (libc) implementation", - Maintainer: "Timo Teräs ", - License: "MIT BSD GPL2+", - Architecture: "x86_64", - URL: "https://musl.libc.org/", - Size: 37944, - InstalledSize: 151552, - Dependencies: []string{"scanelf", "so:libc.musl-x86_64.so.1"}, - Provides: []string{"cmd:getconf", "cmd:getent", "cmd:iconv", "cmd:ldconfig", "cmd:ldd"}, - Checksum: "Q1bTtF5526tETKfL+lnigzIDvm+2o=", - GitCommit: "4024cc3b29ad4c65544ad068b8f59172b5494306", - Files: []pkg.ApkFileRecord{ - { - Path: "/sbin", - }, - { - Path: "/sbin/ldconfig", - OwnerUID: "0", - OwnerGID: "0", - Permissions: "755", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1Kja2+POZKxEkUOZqwSjC6kmaED4=", + expected: pkg.Package{ + Name: "musl-utils", + Version: "1.1.24-r2", + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("MIT"), + pkg.NewLicense("BSD"), + pkg.NewLicense("GPL2+"), + ), + Type: pkg.ApkPkg, + MetadataType: pkg.ApkMetadataType, + Metadata: pkg.ApkMetadata{ + Package: "musl-utils", + OriginPackage: "musl", + Version: "1.1.24-r2", + Description: "the musl c library (libc) implementation", + Maintainer: "Timo Teräs ", + Architecture: "x86_64", + URL: "https://musl.libc.org/", + Size: 37944, + InstalledSize: 151552, + Dependencies: []string{"scanelf", "so:libc.musl-x86_64.so.1"}, + Provides: []string{"cmd:getconf", "cmd:getent", "cmd:iconv", "cmd:ldconfig", "cmd:ldd"}, + Checksum: "Q1bTtF5526tETKfL+lnigzIDvm+2o=", + GitCommit: "4024cc3b29ad4c65544ad068b8f59172b5494306", + Files: []pkg.ApkFileRecord{ + { + Path: "/sbin", }, - }, - { - Path: "/usr", - }, - { - Path: "/usr/bin", - }, - { - Path: "/usr/bin/iconv", - OwnerUID: "0", - OwnerGID: "0", - Permissions: "755", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1CVmFbdY+Hv6/jAHl1gec2Kbx1EY=", + { + Path: "/sbin/ldconfig", + OwnerUID: "0", + OwnerGID: "0", + Permissions: "755", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1Kja2+POZKxEkUOZqwSjC6kmaED4=", + }, }, - }, - { - Path: "/usr/bin/ldd", - OwnerUID: "0", - OwnerGID: "0", - Permissions: "755", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1yFAhGggmL7ERgbIA7KQxyTzf3ks=", + { + Path: "/usr", }, - }, - { - Path: "/usr/bin/getconf", - OwnerUID: "0", - OwnerGID: "0", - Permissions: "755", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1dAdYK8M/INibRQF5B3Rw7cmNDDA=", + { + Path: "/usr/bin", }, - }, - { - Path: "/usr/bin/getent", - OwnerUID: "0", - OwnerGID: "0", - Permissions: "755", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1eR2Dz/WylabgbWMTkd2+hGmEya4=", + { + Path: "/usr/bin/iconv", + OwnerUID: "0", + OwnerGID: "0", + Permissions: "755", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1CVmFbdY+Hv6/jAHl1gec2Kbx1EY=", + }, + }, + { + Path: "/usr/bin/ldd", + OwnerUID: "0", + OwnerGID: "0", + Permissions: "755", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1yFAhGggmL7ERgbIA7KQxyTzf3ks=", + }, + }, + { + Path: "/usr/bin/getconf", + OwnerUID: "0", + OwnerGID: "0", + Permissions: "755", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1dAdYK8M/INibRQF5B3Rw7cmNDDA=", + }, + }, + { + Path: "/usr/bin/getent", + OwnerUID: "0", + OwnerGID: "0", + Permissions: "755", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1eR2Dz/WylabgbWMTkd2+hGmEya4=", + }, }, }, }, @@ -164,487 +174,504 @@ func TestSinglePackageDetails(t *testing.T) { }, { fixture: "test-fixtures/empty-deps-and-provides", - expected: pkg.ApkMetadata{ - Package: "alpine-baselayout-data", - OriginPackage: "alpine-baselayout", - Version: "3.4.0-r0", - Description: "Alpine base dir structure and init scripts", - Maintainer: "Natanael Copa ", - License: "GPL-2.0-only", - Architecture: "x86_64", - URL: "https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout", - Size: 11664, - InstalledSize: 77824, - Dependencies: []string{}, - Provides: []string{}, - Checksum: "Q15ffjKT28lB7iSXjzpI/eDdYRCwM=", - GitCommit: "bd965a7ebf7fd8f07d7a0cc0d7375bf3e4eb9b24", - Files: []pkg.ApkFileRecord{ - {Path: "/etc"}, - {Path: "/etc/fstab"}, - {Path: "/etc/group"}, - {Path: "/etc/hostname"}, - {Path: "/etc/hosts"}, - {Path: "/etc/inittab"}, - {Path: "/etc/modules"}, - {Path: "/etc/mtab", OwnerUID: "0", OwnerGID: "0", Permissions: "0777"}, - {Path: "/etc/nsswitch.conf"}, - {Path: "/etc/passwd"}, - {Path: "/etc/profile"}, - {Path: "/etc/protocols"}, - {Path: "/etc/services"}, - {Path: "/etc/shadow", OwnerUID: "0", OwnerGID: "148", Permissions: "0640"}, - {Path: "/etc/shells"}, - {Path: "/etc/sysctl.conf"}, + expected: pkg.Package{ + Name: "alpine-baselayout-data", + Version: "3.4.0-r0", + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("GPL-2.0-only"), + ), + Type: pkg.ApkPkg, + MetadataType: pkg.ApkMetadataType, + Metadata: pkg.ApkMetadata{ + Package: "alpine-baselayout-data", + OriginPackage: "alpine-baselayout", + Version: "3.4.0-r0", + Description: "Alpine base dir structure and init scripts", + Maintainer: "Natanael Copa ", + Architecture: "x86_64", + URL: "https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout", + Size: 11664, + InstalledSize: 77824, + Dependencies: []string{}, + Provides: []string{}, + Checksum: "Q15ffjKT28lB7iSXjzpI/eDdYRCwM=", + GitCommit: "bd965a7ebf7fd8f07d7a0cc0d7375bf3e4eb9b24", + Files: []pkg.ApkFileRecord{ + {Path: "/etc"}, + {Path: "/etc/fstab"}, + {Path: "/etc/group"}, + {Path: "/etc/hostname"}, + {Path: "/etc/hosts"}, + {Path: "/etc/inittab"}, + {Path: "/etc/modules"}, + {Path: "/etc/mtab", OwnerUID: "0", OwnerGID: "0", Permissions: "0777"}, + {Path: "/etc/nsswitch.conf"}, + {Path: "/etc/passwd"}, + {Path: "/etc/profile"}, + {Path: "/etc/protocols"}, + {Path: "/etc/services"}, + {Path: "/etc/shadow", OwnerUID: "0", OwnerGID: "148", Permissions: "0640"}, + {Path: "/etc/shells"}, + {Path: "/etc/sysctl.conf"}, + }, }, }, }, { fixture: "test-fixtures/base", - expected: pkg.ApkMetadata{ - Package: "alpine-baselayout", - OriginPackage: "alpine-baselayout", - Version: "3.2.0-r6", - Description: "Alpine base dir structure and init scripts", - Maintainer: "Natanael Copa ", - License: "GPL-2.0-only", - Architecture: "x86_64", - URL: "https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout", - Size: 19917, - InstalledSize: 409600, - Dependencies: []string{"/bin/sh", "so:libc.musl-x86_64.so.1"}, - Provides: []string{"cmd:mkmntdirs"}, - Checksum: "Q1myMNfd7u5v5UTgNHeq1e31qTjZU=", - GitCommit: "e1c51734fa96fa4bac92e9f14a474324c67916fc", - Files: []pkg.ApkFileRecord{ - { - Path: "/dev", - }, - { - Path: "/dev/pts", - }, - { - Path: "/dev/shm", - }, - { - Path: "/etc", - }, - { - Path: "/etc/fstab", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q11Q7hNe8QpDS531guqCdrXBzoA/o=", + expected: pkg.Package{ + Name: "alpine-baselayout", + Version: "3.2.0-r6", + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("GPL-2.0-only"), + ), + Type: pkg.ApkPkg, + PURL: "", + MetadataType: pkg.ApkMetadataType, + Metadata: pkg.ApkMetadata{ + Package: "alpine-baselayout", + OriginPackage: "alpine-baselayout", + Version: "3.2.0-r6", + Description: "Alpine base dir structure and init scripts", + Maintainer: "Natanael Copa ", + Architecture: "x86_64", + URL: "https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout", + Size: 19917, + InstalledSize: 409600, + Dependencies: []string{"/bin/sh", "so:libc.musl-x86_64.so.1"}, + Provides: []string{"cmd:mkmntdirs"}, + Checksum: "Q1myMNfd7u5v5UTgNHeq1e31qTjZU=", + GitCommit: "e1c51734fa96fa4bac92e9f14a474324c67916fc", + Files: []pkg.ApkFileRecord{ + { + Path: "/dev", }, - }, - { - Path: "/etc/group", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1oJ16xWudgKOrXIEquEDzlF2Lsm4=", + { + Path: "/dev/pts", }, - }, - { - Path: "/etc/hostname", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q16nVwYVXP/tChvUPdukVD2ifXOmc=", + { + Path: "/dev/shm", }, - }, - { - Path: "/etc/hosts", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1BD6zJKZTRWyqGnPi4tSfd3krsMU=", + { + Path: "/etc", }, - }, - { - Path: "/etc/inittab", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1TsthbhW7QzWRe1E/NKwTOuD4pHc=", + { + Path: "/etc/fstab", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q11Q7hNe8QpDS531guqCdrXBzoA/o=", + }, }, - }, - { - Path: "/etc/modules", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1toogjUipHGcMgECgPJX64SwUT1M=", + { + Path: "/etc/group", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1oJ16xWudgKOrXIEquEDzlF2Lsm4=", + }, }, - }, - { - Path: "/etc/motd", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1XmduVVNURHQ27TvYp1Lr5TMtFcA=", + { + Path: "/etc/hostname", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q16nVwYVXP/tChvUPdukVD2ifXOmc=", + }, }, - }, - { - Path: "/etc/mtab", - OwnerUID: "0", - OwnerGID: "0", - Permissions: "777", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1kiljhXXH1LlQroHsEJIkPZg2eiw=", + { + Path: "/etc/hosts", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1BD6zJKZTRWyqGnPi4tSfd3krsMU=", + }, }, - }, - { - Path: "/etc/passwd", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1TchuuLUfur0izvfZQZxgN/LJhB8=", + { + Path: "/etc/inittab", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1TsthbhW7QzWRe1E/NKwTOuD4pHc=", + }, }, - }, - { - Path: "/etc/profile", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1KpFb8kl5LvwXWlY3e58FNsjrI34=", + { + Path: "/etc/modules", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1toogjUipHGcMgECgPJX64SwUT1M=", + }, }, - }, - { - Path: "/etc/protocols", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q13FqXUnvuOpMDrH/6rehxuYAEE34=", + { + Path: "/etc/motd", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1XmduVVNURHQ27TvYp1Lr5TMtFcA=", + }, }, - }, - { - Path: "/etc/services", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1C6HJNgQvLWqt5VY+n7MZJ1rsDuY=", + { + Path: "/etc/mtab", + OwnerUID: "0", + OwnerGID: "0", + Permissions: "777", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1kiljhXXH1LlQroHsEJIkPZg2eiw=", + }, }, - }, - { - Path: "/etc/shadow", - OwnerUID: "0", - OwnerGID: "42", - Permissions: "640", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1ltrPIAW2zHeDiajsex2Bdmq3uqA=", + { + Path: "/etc/passwd", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1TchuuLUfur0izvfZQZxgN/LJhB8=", + }, }, - }, - { - Path: "/etc/shells", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1ojm2YdpCJ6B/apGDaZ/Sdb2xJkA=", + { + Path: "/etc/profile", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1KpFb8kl5LvwXWlY3e58FNsjrI34=", + }, }, - }, - { - Path: "/etc/sysctl.conf", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q14upz3tfnNxZkIEsUhWn7Xoiw96g=", + { + Path: "/etc/protocols", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q13FqXUnvuOpMDrH/6rehxuYAEE34=", + }, }, - }, - { - Path: "/etc/apk", - }, - { - Path: "/etc/conf.d", - }, - { - Path: "/etc/crontabs", - }, - { - Path: "/etc/crontabs/root", - OwnerUID: "0", - OwnerGID: "0", - Permissions: "600", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1vfk1apUWI4yLJGhhNRd0kJixfvY=", + { + Path: "/etc/services", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1C6HJNgQvLWqt5VY+n7MZJ1rsDuY=", + }, }, - }, - { - Path: "/etc/init.d", - }, - { - Path: "/etc/modprobe.d", - }, - { - Path: "/etc/modprobe.d/aliases.conf", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1WUbh6TBYNVK7e4Y+uUvLs/7viqk=", + { + Path: "/etc/shadow", + OwnerUID: "0", + OwnerGID: "42", + Permissions: "640", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1ltrPIAW2zHeDiajsex2Bdmq3uqA=", + }, }, - }, - { - Path: "/etc/modprobe.d/blacklist.conf", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1xxYGU6S6TLQvb7ervPrWWwAWqMg=", + { + Path: "/etc/shells", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1ojm2YdpCJ6B/apGDaZ/Sdb2xJkA=", + }, }, - }, - { - Path: "/etc/modprobe.d/i386.conf", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1pnay/njn6ol9cCssL7KiZZ8etlc=", + { + Path: "/etc/sysctl.conf", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q14upz3tfnNxZkIEsUhWn7Xoiw96g=", + }, }, - }, - { - Path: "/etc/modprobe.d/kms.conf", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1ynbLn3GYDpvajba/ldp1niayeog=", + { + Path: "/etc/apk", }, - }, - { - Path: "/etc/modules-load.d", - }, - { - Path: "/etc/network", - }, - { - Path: "/etc/network/if-down.d", - }, - { - Path: "/etc/network/if-post-down.d", - }, - { - Path: "/etc/network/if-pre-up.d", - }, - { - Path: "/etc/network/if-up.d", - }, - { - Path: "/etc/opt", - }, - { - Path: "/etc/periodic", - }, - { - Path: "/etc/periodic/15min", - }, - { - Path: "/etc/periodic/daily", - }, - { - Path: "/etc/periodic/hourly", - }, - { - Path: "/etc/periodic/monthly", - }, - { - Path: "/etc/periodic/weekly", - }, - { - Path: "/etc/profile.d", - }, - { - Path: "/etc/profile.d/color_prompt", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q10wL23GuSCVfumMRgakabUI6EsSk=", + { + Path: "/etc/conf.d", }, - }, - { - Path: "/etc/profile.d/locale", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1R4bIEpnKxxOSrlnZy9AoawqZ5DU=", + { + Path: "/etc/crontabs", }, - }, - { - Path: "/etc/sysctl.d", - }, - { - Path: "/home", - }, - { - Path: "/lib", - }, - { - Path: "/lib/firmware", - }, - { - Path: "/lib/mdev", - }, - { - Path: "/lib/modules-load.d", - }, - { - Path: "/lib/sysctl.d", - }, - { - Path: "/lib/sysctl.d/00-alpine.conf", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1HpElzW1xEgmKfERtTy7oommnq6c=", + { + Path: "/etc/crontabs/root", + OwnerUID: "0", + OwnerGID: "0", + Permissions: "600", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1vfk1apUWI4yLJGhhNRd0kJixfvY=", + }, }, - }, - { - Path: "/media", - }, - { - Path: "/media/cdrom", - }, - { - Path: "/media/floppy", - }, - { - Path: "/media/usb", - }, - { - Path: "/mnt", - }, - { - Path: "/opt", - }, - { - Path: "/proc", - }, - { - Path: "/root", - OwnerUID: "0", - OwnerGID: "0", - Permissions: "700", - }, - { - Path: "/run", - }, - { - Path: "/sbin", - }, - { - Path: "/sbin/mkmntdirs", - OwnerUID: "0", - OwnerGID: "0", - Permissions: "755", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1YeuSmC7iDbEWrusPzA/zUQF6YSg=", + { + Path: "/etc/init.d", }, - }, - { - Path: "/srv", - }, - { - Path: "/sys", - }, - { - Path: "/tmp", - OwnerUID: "0", - OwnerGID: "0", - Permissions: "1777", - }, - { - Path: "/usr", - }, - { - Path: "/usr/lib", - }, - { - Path: "/usr/lib/modules-load.d", - }, - { - Path: "/usr/local", - }, - { - Path: "/usr/local/bin", - }, - { - Path: "/usr/local/lib", - }, - { - Path: "/usr/local/share", - }, - { - Path: "/usr/sbin", - }, - { - Path: "/usr/share", - }, - { - Path: "/usr/share/man", - }, - { - Path: "/usr/share/misc", - }, - { - Path: "/var", - }, - { - Path: "/var/run", - OwnerUID: "0", - OwnerGID: "0", - Permissions: "777", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q11/SNZz/8cK2dSKK+cJpVrZIuF4Q=", + { + Path: "/etc/modprobe.d", }, - }, - { - Path: "/var/cache", - }, - { - Path: "/var/cache/misc", - }, - { - Path: "/var/empty", - OwnerUID: "0", - OwnerGID: "0", - Permissions: "555", - }, - { - Path: "/var/lib", - }, - { - Path: "/var/lib/misc", - }, - { - Path: "/var/local", - }, - { - Path: "/var/lock", - }, - { - Path: "/var/lock/subsys", - }, - { - Path: "/var/log", - }, - { - Path: "/var/mail", - }, - { - Path: "/var/opt", - }, - { - Path: "/var/spool", - }, - { - Path: "/var/spool/mail", - OwnerUID: "0", - OwnerGID: "0", - Permissions: "777", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1dzbdazYZA2nTzSIG3YyNw7d4Juc=", + { + Path: "/etc/modprobe.d/aliases.conf", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1WUbh6TBYNVK7e4Y+uUvLs/7viqk=", + }, }, - }, - { - Path: "/var/spool/cron", - }, - { - Path: "/var/spool/cron/crontabs", - OwnerUID: "0", - OwnerGID: "0", - Permissions: "777", - Digest: &file.Digest{ - Algorithm: "'Q1'+base64(sha1)", - Value: "Q1OFZt+ZMp7j0Gny0rqSKuWJyqYmA=", + { + Path: "/etc/modprobe.d/blacklist.conf", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1xxYGU6S6TLQvb7ervPrWWwAWqMg=", + }, + }, + { + Path: "/etc/modprobe.d/i386.conf", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1pnay/njn6ol9cCssL7KiZZ8etlc=", + }, + }, + { + Path: "/etc/modprobe.d/kms.conf", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1ynbLn3GYDpvajba/ldp1niayeog=", + }, + }, + { + Path: "/etc/modules-load.d", + }, + { + Path: "/etc/network", + }, + { + Path: "/etc/network/if-down.d", + }, + { + Path: "/etc/network/if-post-down.d", + }, + { + Path: "/etc/network/if-pre-up.d", + }, + { + Path: "/etc/network/if-up.d", + }, + { + Path: "/etc/opt", + }, + { + Path: "/etc/periodic", + }, + { + Path: "/etc/periodic/15min", + }, + { + Path: "/etc/periodic/daily", + }, + { + Path: "/etc/periodic/hourly", + }, + { + Path: "/etc/periodic/monthly", + }, + { + Path: "/etc/periodic/weekly", + }, + { + Path: "/etc/profile.d", + }, + { + Path: "/etc/profile.d/color_prompt", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q10wL23GuSCVfumMRgakabUI6EsSk=", + }, + }, + { + Path: "/etc/profile.d/locale", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1R4bIEpnKxxOSrlnZy9AoawqZ5DU=", + }, + }, + { + Path: "/etc/sysctl.d", + }, + { + Path: "/home", + }, + { + Path: "/lib", + }, + { + Path: "/lib/firmware", + }, + { + Path: "/lib/mdev", + }, + { + Path: "/lib/modules-load.d", + }, + { + Path: "/lib/sysctl.d", + }, + { + Path: "/lib/sysctl.d/00-alpine.conf", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1HpElzW1xEgmKfERtTy7oommnq6c=", + }, + }, + { + Path: "/media", + }, + { + Path: "/media/cdrom", + }, + { + Path: "/media/floppy", + }, + { + Path: "/media/usb", + }, + { + Path: "/mnt", + }, + { + Path: "/opt", + }, + { + Path: "/proc", + }, + { + Path: "/root", + OwnerUID: "0", + OwnerGID: "0", + Permissions: "700", + }, + { + Path: "/run", + }, + { + Path: "/sbin", + }, + { + Path: "/sbin/mkmntdirs", + OwnerUID: "0", + OwnerGID: "0", + Permissions: "755", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1YeuSmC7iDbEWrusPzA/zUQF6YSg=", + }, + }, + { + Path: "/srv", + }, + { + Path: "/sys", + }, + { + Path: "/tmp", + OwnerUID: "0", + OwnerGID: "0", + Permissions: "1777", + }, + { + Path: "/usr", + }, + { + Path: "/usr/lib", + }, + { + Path: "/usr/lib/modules-load.d", + }, + { + Path: "/usr/local", + }, + { + Path: "/usr/local/bin", + }, + { + Path: "/usr/local/lib", + }, + { + Path: "/usr/local/share", + }, + { + Path: "/usr/sbin", + }, + { + Path: "/usr/share", + }, + { + Path: "/usr/share/man", + }, + { + Path: "/usr/share/misc", + }, + { + Path: "/var", + }, + { + Path: "/var/run", + OwnerUID: "0", + OwnerGID: "0", + Permissions: "777", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q11/SNZz/8cK2dSKK+cJpVrZIuF4Q=", + }, + }, + { + Path: "/var/cache", + }, + { + Path: "/var/cache/misc", + }, + { + Path: "/var/empty", + OwnerUID: "0", + OwnerGID: "0", + Permissions: "555", + }, + { + Path: "/var/lib", + }, + { + Path: "/var/lib/misc", + }, + { + Path: "/var/local", + }, + { + Path: "/var/lock", + }, + { + Path: "/var/lock/subsys", + }, + { + Path: "/var/log", + }, + { + Path: "/var/mail", + }, + { + Path: "/var/opt", + }, + { + Path: "/var/spool", + }, + { + Path: "/var/spool/mail", + OwnerUID: "0", + OwnerGID: "0", + Permissions: "777", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1dzbdazYZA2nTzSIG3YyNw7d4Juc=", + }, + }, + { + Path: "/var/spool/cron", + }, + { + Path: "/var/spool/cron/crontabs", + OwnerUID: "0", + OwnerGID: "0", + Permissions: "777", + Digest: &file.Digest{ + Algorithm: "'Q1'+base64(sha1)", + Value: "Q1OFZt+ZMp7j0Gny0rqSKuWJyqYmA=", + }, + }, + { + Path: "/var/tmp", + OwnerUID: "0", + OwnerGID: "0", + Permissions: "1777", }, - }, - { - Path: "/var/tmp", - OwnerUID: "0", - OwnerGID: "0", - Permissions: "1777", }, }, }, @@ -653,28 +680,29 @@ func TestSinglePackageDetails(t *testing.T) { for _, test := range tests { t.Run(test.fixture, func(t *testing.T) { - lrc := newLocationReadCloser(t, test.fixture) - - pkgs, _, err := parseApkDB(nil, new(generic.Environment), lrc) - require.NoError(t, err) - require.Len(t, pkgs, 1) - metadata := pkgs[0].Metadata.(pkg.ApkMetadata) - - if diff := cmp.Diff(test.expected, metadata); diff != "" { - t.Errorf("Entry mismatch (-want +got):\n%s", diff) + fixtureLocation := source.NewLocation(test.fixture) + test.expected.Locations = source.NewLocationSet(fixtureLocation) + licenses := test.expected.Licenses.ToSlice() + for i := range licenses { + licenses[i].Location.Add(fixtureLocation) } + test.expected.Licenses = pkg.NewLicenseSet(licenses...) + pkgtest.TestFileParser(t, test.fixture, parseApkDB, []pkg.Package{test.expected}, nil) }) } } func TestMultiplePackages(t *testing.T) { fixture := "test-fixtures/multiple" - fixtureLocationSet := source.NewLocationSet(source.NewLocation(fixture)) + location := source.NewLocation(fixture) + fixtureLocationSet := source.NewLocationSet(location) expectedPkgs := []pkg.Package{ { - Name: "libc-utils", - Version: "0.7.2-r0", - Licenses: []string{"BSD"}, + Name: "libc-utils", + Version: "0.7.2-r0", + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("BSD", location), + ), Type: pkg.ApkPkg, PURL: "pkg:apk/alpine/libc-utils@0.7.2-r0?arch=x86_64&upstream=libc-dev&distro=alpine-3.12", Locations: fixtureLocationSet, @@ -684,7 +712,6 @@ func TestMultiplePackages(t *testing.T) { OriginPackage: "libc-dev", Maintainer: "Natanael Copa ", Version: "0.7.2-r0", - License: "BSD", Architecture: "x86_64", URL: "http://alpinelinux.org", Description: "Meta package to pull in correct libc", @@ -698,12 +725,16 @@ func TestMultiplePackages(t *testing.T) { }, }, { - Name: "musl-utils", - Version: "1.1.24-r2", - Licenses: []string{"MIT", "BSD", "GPL2+"}, - Type: pkg.ApkPkg, - PURL: "pkg:apk/alpine/musl-utils@1.1.24-r2?arch=x86_64&upstream=musl&distro=alpine-3.12", - Locations: fixtureLocationSet, + Name: "musl-utils", + Version: "1.1.24-r2", + Type: pkg.ApkPkg, + PURL: "pkg:apk/alpine/musl-utils@1.1.24-r2?arch=x86_64&upstream=musl&distro=alpine-3.12", + Locations: fixtureLocationSet, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", location), + pkg.NewLicenseFromLocations("BSD", location), + pkg.NewLicenseFromLocations("GPL2+", location), + ), MetadataType: pkg.ApkMetadataType, Metadata: pkg.ApkMetadata{ Package: "musl-utils", @@ -711,7 +742,6 @@ func TestMultiplePackages(t *testing.T) { Version: "1.1.24-r2", Description: "the musl c library (libc) implementation", Maintainer: "Timo Teräs ", - License: "MIT BSD GPL2+", Architecture: "x86_64", URL: "https://musl.libc.org/", Size: 37944, @@ -994,7 +1024,7 @@ func Test_discoverPackageDependencies(t *testing.T) { t.Run(test.name, func(t *testing.T) { pkgs, wantRelationships := test.genFn() gotRelationships := discoverPackageDependencies(pkgs) - d := cmp.Diff(wantRelationships, gotRelationships, cmpopts.IgnoreUnexported(pkg.Package{}, source.LocationSet{})) + d := cmp.Diff(wantRelationships, gotRelationships, cmpopts.IgnoreUnexported(pkg.Package{}, source.LocationSet{}, pkg.LicenseSet{})) if d != "" { t.Fail() t.Log(d) diff --git a/syft/pkg/cataloger/deb/cataloger_test.go b/syft/pkg/cataloger/deb/cataloger_test.go index 8fac459c6..ab3415d54 100644 --- a/syft/pkg/cataloger/deb/cataloger_test.go +++ b/syft/pkg/cataloger/deb/cataloger_test.go @@ -10,12 +10,17 @@ import ( ) func TestDpkgCataloger(t *testing.T) { + licenseLocation := source.NewVirtualLocation("/usr/share/doc/libpam-runtime/copyright", "/usr/share/doc/libpam-runtime/copyright") expected := []pkg.Package{ { - Name: "libpam-runtime", - Version: "1.1.8-3.6", - FoundBy: "dpkgdb-cataloger", - Licenses: []string{"GPL-1", "GPL-2", "LGPL-2.1"}, + Name: "libpam-runtime", + Version: "1.1.8-3.6", + FoundBy: "dpkgdb-cataloger", + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("GPL-1", licenseLocation), + pkg.NewLicenseFromLocations("GPL-2", licenseLocation), + pkg.NewLicenseFromLocations("LGPL-2.1", licenseLocation), + ), Locations: source.NewLocationSet( source.NewVirtualLocation("/var/lib/dpkg/status", "/var/lib/dpkg/status"), source.NewVirtualLocation("/var/lib/dpkg/info/libpam-runtime.md5sums", "/var/lib/dpkg/info/libpam-runtime.md5sums"), diff --git a/syft/pkg/cataloger/deb/package.go b/syft/pkg/cataloger/deb/package.go index 2113c8472..1685051c9 100644 --- a/syft/pkg/cataloger/deb/package.go +++ b/syft/pkg/cataloger/deb/package.go @@ -22,9 +22,12 @@ const ( ) func newDpkgPackage(d pkg.DpkgMetadata, dbLocation source.Location, resolver source.FileResolver, release *linux.Release) pkg.Package { + // TODO: separate pr to license refactor, but explore extracting dpkg-specific license parsing into a separate function + licenses := make([]pkg.License, 0) p := pkg.Package{ Name: d.Package, Version: d.Version, + Licenses: pkg.NewLicenseSet(licenses...), Locations: source.NewLocationSet(dbLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), PURL: packageURL(d, release), Type: pkg.DebPkg, @@ -93,8 +96,10 @@ func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pk if copyrightReader != nil && copyrightLocation != nil { defer internal.CloseAndLogError(copyrightReader, copyrightLocation.VirtualPath) // attach the licenses - p.Licenses = parseLicensesFromCopyright(copyrightReader) - + licenseStrs := parseLicensesFromCopyright(copyrightReader) + for _, licenseStr := range licenseStrs { + p.Licenses.Add(pkg.NewLicenseFromLocations(licenseStr, copyrightLocation.WithoutAnnotations())) + } // keep a record of the file where this was discovered p.Locations.Add(*copyrightLocation) } diff --git a/syft/pkg/cataloger/deb/parse_dpkg_db_test.go b/syft/pkg/cataloger/deb/parse_dpkg_db_test.go index 0bae7c896..fc4e51633 100644 --- a/syft/pkg/cataloger/deb/parse_dpkg_db_test.go +++ b/syft/pkg/cataloger/deb/parse_dpkg_db_test.go @@ -307,6 +307,7 @@ Installed-Size: 10kib Name: "apt", Type: "deb", PURL: "pkg:deb/debian/apt?distro=debian-10", + Licenses: pkg.NewLicenseSet(), Locations: source.NewLocationSet(source.NewLocation("place")), MetadataType: "DpkgMetadata", Metadata: pkg.DpkgMetadata{ diff --git a/syft/pkg/cataloger/golang/licenses.go b/syft/pkg/cataloger/golang/licenses.go index bfffb9f7f..e85ad7ec7 100644 --- a/syft/pkg/cataloger/golang/licenses.go +++ b/syft/pkg/cataloger/golang/licenses.go @@ -22,6 +22,7 @@ import ( "github.com/anchore/syft/internal/licenses" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/event" + "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) @@ -74,7 +75,7 @@ func modCacheResolver(modCacheDir string) source.WritableFileResolver { return r } -func (c *goLicenses) getLicenses(resolver source.FileResolver, moduleName, moduleVersion string) (licenses []string, err error) { +func (c *goLicenses) getLicenses(resolver source.FileResolver, moduleName, moduleVersion string) (licenses []pkg.License, err error) { licenses, err = findLicenses(resolver, fmt.Sprintf(`**/go/pkg/mod/%s@%s/*`, processCaps(moduleName), moduleVersion), ) @@ -93,7 +94,7 @@ func (c *goLicenses) getLicenses(resolver source.FileResolver, moduleName, modul return requireCollection(licenses), err } -func (c *goLicenses) getLicensesFromLocal(moduleName, moduleVersion string) ([]string, error) { +func (c *goLicenses) getLicensesFromLocal(moduleName, moduleVersion string) ([]pkg.License, error) { if !c.opts.searchLocalModCacheLicenses { return nil, nil } @@ -103,7 +104,7 @@ func (c *goLicenses) getLicensesFromLocal(moduleName, moduleVersion string) ([]s return findLicenses(c.localModCacheResolver, moduleSearchGlob(moduleName, moduleVersion)) } -func (c *goLicenses) getLicensesFromRemote(moduleName, moduleVersion string) ([]string, error) { +func (c *goLicenses) getLicensesFromRemote(moduleName, moduleVersion string) ([]pkg.License, error) { if !c.opts.searchRemoteLicenses { return nil, nil } @@ -148,14 +149,15 @@ func moduleSearchGlob(moduleName, moduleVersion string) string { return fmt.Sprintf("%s/*", moduleDir(moduleName, moduleVersion)) } -func requireCollection(licenses []string) []string { +func requireCollection(licenses []pkg.License) []pkg.License { if licenses == nil { - return []string{} + return make([]pkg.License, 0) } return licenses } -func findLicenses(resolver source.FileResolver, globMatch string) (out []string, err error) { +func findLicenses(resolver source.FileResolver, globMatch string) (out []pkg.License, err error) { + out = make([]pkg.License, 0) if resolver == nil { return } @@ -172,7 +174,7 @@ func findLicenses(resolver source.FileResolver, globMatch string) (out []string, if err != nil { return nil, err } - parsed, err := licenses.Parse(contents) + parsed, err := licenses.Parse(contents, l) if err != nil { return nil, err } diff --git a/syft/pkg/cataloger/golang/licenses_test.go b/syft/pkg/cataloger/golang/licenses_test.go index fa361004a..64b66b3f6 100644 --- a/syft/pkg/cataloger/golang/licenses_test.go +++ b/syft/pkg/cataloger/golang/licenses_test.go @@ -13,24 +13,42 @@ import ( "github.com/stretchr/testify/require" + "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft/license" + "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) func Test_LocalLicenseSearch(t *testing.T) { + loc1 := source.NewLocation("github.com/someorg/somename@v0.3.2/LICENSE") + loc2 := source.NewLocation("github.com/!cap!o!r!g/!cap!project@v4.111.5/LICENSE.txt") + tests := []struct { name string version string - expected string + expected pkg.License }{ { - name: "github.com/someorg/somename", - version: "v0.3.2", - expected: "Apache-2.0", + name: "github.com/someorg/somename", + version: "v0.3.2", + expected: pkg.License{ + Value: "Apache-2.0", + SPDXExpression: "Apache-2.0", + Type: license.Concluded, + Location: source.NewLocationSet(loc1), + URL: internal.NewStringSet(), + }, }, { - name: "github.com/CapORG/CapProject", - version: "v4.111.5", - expected: "MIT", + name: "github.com/CapORG/CapProject", + version: "v4.111.5", + expected: pkg.License{ + Value: "MIT", + SPDXExpression: "MIT", + Type: license.Concluded, + Location: source.NewLocationSet(loc2), + URL: internal.NewStringSet(), + }, }, } @@ -39,10 +57,12 @@ func Test_LocalLicenseSearch(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - l := newGoLicenses(GoCatalogerOpts{ - searchLocalModCacheLicenses: true, - localModCacheDir: path.Join(wd, "test-fixtures", "licenses", "pkg", "mod"), - }) + l := newGoLicenses( + GoCatalogerOpts{ + searchLocalModCacheLicenses: true, + localModCacheDir: path.Join(wd, "test-fixtures", "licenses", "pkg", "mod"), + }, + ) licenses, err := l.getLicenses(source.EmptyResolver{}, test.name, test.version) require.NoError(t, err) @@ -54,6 +74,9 @@ func Test_LocalLicenseSearch(t *testing.T) { } func Test_RemoteProxyLicenseSearch(t *testing.T) { + loc1 := source.NewLocation("github.com/someorg/somename@v0.3.2/LICENSE") + loc2 := source.NewLocation("github.com/!cap!o!r!g/!cap!project@v4.111.5/LICENSE.txt") + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { buf := &bytes.Buffer{} uri := strings.TrimPrefix(strings.TrimSuffix(r.RequestURI, ".zip"), "/") @@ -94,17 +117,29 @@ func Test_RemoteProxyLicenseSearch(t *testing.T) { tests := []struct { name string version string - expected string + expected pkg.License }{ { - name: "github.com/someorg/somename", - version: "v0.3.2", - expected: "Apache-2.0", + name: "github.com/someorg/somename", + version: "v0.3.2", + expected: pkg.License{ + Value: "Apache-2.0", + SPDXExpression: "Apache-2.0", + Type: license.Concluded, + Location: source.NewLocationSet(loc1), + URL: internal.NewStringSet(), + }, }, { - name: "github.com/CapORG/CapProject", - version: "v4.111.5", - expected: "MIT", + name: "github.com/CapORG/CapProject", + version: "v4.111.5", + expected: pkg.License{ + Value: "MIT", + SPDXExpression: "MIT", + Type: license.Concluded, + Location: source.NewLocationSet(loc2), + URL: internal.NewStringSet(), + }, }, } diff --git a/syft/pkg/cataloger/golang/package.go b/syft/pkg/cataloger/golang/package.go index de12e7880..2e4c28089 100644 --- a/syft/pkg/cataloger/golang/package.go +++ b/syft/pkg/cataloger/golang/package.go @@ -24,7 +24,7 @@ func (c *goBinaryCataloger) newGoBinaryPackage(resolver source.FileResolver, dep p := pkg.Package{ Name: dep.Path, Version: dep.Version, - Licenses: licenses, + Licenses: pkg.NewLicenseSet(licenses...), PURL: packageURL(dep.Path, dep.Version), Language: pkg.Go, Type: pkg.GoModulePkg, diff --git a/syft/pkg/cataloger/golang/parse_go_binary_test.go b/syft/pkg/cataloger/golang/parse_go_binary_test.go index 4fe87c2ec..d180d158f 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary_test.go +++ b/syft/pkg/cataloger/golang/parse_go_binary_test.go @@ -497,9 +497,6 @@ func TestBuildGoPkgInfo(t *testing.T) { t.Run(test.name, func(t *testing.T) { for i := range test.expected { p := &test.expected[i] - if p.Licenses == nil { - p.Licenses = []string{} - } p.SetID() } location := source.NewLocationFromCoordinates( diff --git a/syft/pkg/cataloger/golang/parse_go_mod.go b/syft/pkg/cataloger/golang/parse_go_mod.go index 203ba3189..3fdc45b9a 100644 --- a/syft/pkg/cataloger/golang/parse_go_mod.go +++ b/syft/pkg/cataloger/golang/parse_go_mod.go @@ -50,7 +50,7 @@ func (c *goModCataloger) parseGoModFile(resolver source.FileResolver, _ *generic packages[m.Mod.Path] = pkg.Package{ Name: m.Mod.Path, Version: m.Mod.Version, - Licenses: licenses, + Licenses: pkg.NewLicenseSet(licenses...), Locations: source.NewLocationSet(reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), PURL: packageURL(m.Mod.Path, m.Mod.Version), Language: pkg.Go, @@ -72,7 +72,7 @@ func (c *goModCataloger) parseGoModFile(resolver source.FileResolver, _ *generic packages[m.New.Path] = pkg.Package{ Name: m.New.Path, Version: m.New.Version, - Licenses: licenses, + Licenses: pkg.NewLicenseSet(licenses...), Locations: source.NewLocationSet(reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), PURL: packageURL(m.New.Path, m.New.Version), Language: pkg.Go, diff --git a/syft/pkg/cataloger/golang/parse_go_mod_test.go b/syft/pkg/cataloger/golang/parse_go_mod_test.go index 531ee5ef7..83b75beb1 100644 --- a/syft/pkg/cataloger/golang/parse_go_mod_test.go +++ b/syft/pkg/cataloger/golang/parse_go_mod_test.go @@ -88,12 +88,6 @@ func TestParseGoMod(t *testing.T) { for _, test := range tests { t.Run(test.fixture, func(t *testing.T) { - for i := range test.expected { - p := &test.expected[i] - if p.Licenses == nil { - p.Licenses = []string{} - } - } c := goModCataloger{} pkgtest.NewCatalogTester(). FromFile(t, test.fixture). @@ -154,12 +148,6 @@ func Test_GoSumHashes(t *testing.T) { for _, test := range tests { t.Run(test.fixture, func(t *testing.T) { - for i := range test.expected { - p := &test.expected[i] - if p.Licenses == nil { - p.Licenses = []string{} - } - } pkgtest.NewCatalogTester(). FromDirectory(t, test.fixture). Expects(test.expected, nil). diff --git a/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go b/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go index 43e024673..5d230b011 100644 --- a/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go +++ b/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go @@ -21,6 +21,7 @@ import ( ) type locationComparer func(x, y source.Location) bool +type licenseComparer func(x, y pkg.License) bool type CatalogTester struct { expectedPkgs []pkg.Package @@ -36,12 +37,14 @@ type CatalogTester struct { wantErr require.ErrorAssertionFunc compareOptions []cmp.Option locationComparer locationComparer + licenseComparer licenseComparer } func NewCatalogTester() *CatalogTester { return &CatalogTester{ wantErr: require.NoError, locationComparer: DefaultLocationComparer, + licenseComparer: DefaultLicenseComparer, ignoreUnfulfilledPathResponses: map[string][]string{ "FilesByPath": { // most catalogers search for a linux release, which will not be fulfilled in testing @@ -59,6 +62,25 @@ func DefaultLocationComparer(x, y source.Location) bool { return cmp.Equal(x.Coordinates, y.Coordinates) && cmp.Equal(x.VirtualPath, y.VirtualPath) } +func DefaultLicenseComparer(x, y pkg.License) bool { + return cmp.Equal(x, y, cmp.Comparer(DefaultLocationComparer), cmp.Comparer( + func(x, y source.LocationSet) bool { + xs := x.ToSlice() + ys := y.ToSlice() + if len(xs) != len(ys) { + return false + } + for i, xe := range xs { + ye := ys[i] + if !DefaultLocationComparer(xe, ye) { + return false + } + } + return true + }, + )) +} + func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester { t.Helper() @@ -139,6 +161,26 @@ func (p *CatalogTester) IgnoreLocationLayer() *CatalogTester { p.locationComparer = func(x, y source.Location) bool { return cmp.Equal(x.Coordinates.RealPath, y.Coordinates.RealPath) && cmp.Equal(x.VirtualPath, y.VirtualPath) } + + // we need to update the license comparer to use the ignored location layer + p.licenseComparer = func(x, y pkg.License) bool { + return cmp.Equal(x, y, cmp.Comparer(p.locationComparer), cmp.Comparer( + func(x, y source.LocationSet) bool { + xs := x.ToSlice() + ys := y.ToSlice() + if len(xs) != len(ys) { + return false + } + for i, xe := range xs { + ye := ys[i] + if !p.locationComparer(xe, ye) { + return false + } + } + + return true + })) + } return p } @@ -209,6 +251,7 @@ func (p *CatalogTester) TestCataloger(t *testing.T, cataloger pkg.Cataloger) { } } +// nolint:funlen func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) { t.Helper() @@ -233,6 +276,30 @@ func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationshi return true }, ), + cmp.Comparer( + func(x, y pkg.LicenseSet) bool { + xs := x.ToSlice() + ys := y.ToSlice() + + if len(xs) != len(ys) { + return false + } + for i, xe := range xs { + ye := ys[i] + if !p.licenseComparer(xe, ye) { + return false + } + } + + return true + }, + ), + cmp.Comparer( + p.locationComparer, + ), + cmp.Comparer( + p.licenseComparer, + ), ) { @@ -295,6 +362,30 @@ func AssertPackagesEqual(t *testing.T, a, b pkg.Package) { return true }, ), + cmp.Comparer( + func(x, y pkg.LicenseSet) bool { + xs := x.ToSlice() + ys := y.ToSlice() + + if len(xs) != len(ys) { + return false + } + for i, xe := range xs { + ye := ys[i] + if !DefaultLicenseComparer(xe, ye) { + return false + } + } + + return true + }, + ), + cmp.Comparer( + DefaultLocationComparer, + ), + cmp.Comparer( + DefaultLicenseComparer, + ), } if diff := cmp.Diff(a, b, opts...); diff != "" { diff --git a/syft/pkg/cataloger/java/archive_parser.go b/syft/pkg/cataloger/java/archive_parser.go index 0128709d5..d6c0ad926 100644 --- a/syft/pkg/cataloger/java/archive_parser.go +++ b/syft/pkg/cataloger/java/archive_parser.go @@ -185,11 +185,13 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) { log.Warnf("failed to create digest for file=%q: %+v", j.archivePath, err) } + // we use j.location because we want to associate the license declaration with where we discovered the contents in the manifest + licenses := pkg.NewLicensesFromLocation(j.location, selectLicenses(manifest)...) return &pkg.Package{ Name: selectName(manifest, j.fileInfo), Version: selectVersion(manifest, j.fileInfo), - Licenses: selectLicense(manifest), Language: pkg.Java, + Licenses: pkg.NewLicenseSet(licenses...), Locations: source.NewLocationSet( j.location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), ), diff --git a/syft/pkg/cataloger/java/archive_parser_test.go b/syft/pkg/cataloger/java/archive_parser_test.go index 101c4e947..5385dec73 100644 --- a/syft/pkg/cataloger/java/archive_parser_test.go +++ b/syft/pkg/cataloger/java/archive_parser_test.go @@ -96,10 +96,12 @@ func TestParseJar(t *testing.T) { }, expected: map[string]pkg.Package{ "example-jenkins-plugin": { - Name: "example-jenkins-plugin", - Version: "1.0-SNAPSHOT", - PURL: "pkg:maven/io.jenkins.plugins/example-jenkins-plugin@1.0-SNAPSHOT", - Licenses: []string{"MIT License"}, + Name: "example-jenkins-plugin", + Version: "1.0-SNAPSHOT", + PURL: "pkg:maven/io.jenkins.plugins/example-jenkins-plugin@1.0-SNAPSHOT", + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT License", source.NewLocation("test-fixtures/java-builds/packages/example-jenkins-plugin.hpi")), + ), Language: pkg.Java, Type: pkg.JenkinsPluginPkg, MetadataType: pkg.JavaMetadataType, @@ -150,7 +152,6 @@ func TestParseJar(t *testing.T) { Name: "example-java-app-gradle", Version: "0.1.0", PURL: "pkg:maven/example-java-app-gradle/example-java-app-gradle@0.1.0", - Licenses: []string{}, Language: pkg.Java, Type: pkg.JavaPkg, MetadataType: pkg.JavaMetadataType, @@ -205,7 +206,6 @@ func TestParseJar(t *testing.T) { Name: "example-java-app-maven", Version: "0.1.0", PURL: "pkg:maven/org.anchore/example-java-app-maven@0.1.0", - Licenses: []string{}, Language: pkg.Java, Type: pkg.JavaPkg, MetadataType: pkg.JavaMetadataType, diff --git a/syft/pkg/cataloger/java/parse_java_manifest.go b/syft/pkg/cataloger/java/parse_java_manifest.go index 134aa2297..f615da406 100644 --- a/syft/pkg/cataloger/java/parse_java_manifest.go +++ b/syft/pkg/cataloger/java/parse_java_manifest.go @@ -157,7 +157,7 @@ func selectVersion(manifest *pkg.JavaManifest, filenameObj archiveFilename) stri return "" } -func selectLicense(manifest *pkg.JavaManifest) []string { +func selectLicenses(manifest *pkg.JavaManifest) []string { result := []string{} if manifest == nil { return result diff --git a/syft/pkg/cataloger/javascript/cataloger_test.go b/syft/pkg/cataloger/javascript/cataloger_test.go index 6e0ba16b6..5b9c18f0e 100644 --- a/syft/pkg/cataloger/javascript/cataloger_test.go +++ b/syft/pkg/cataloger/javascript/cataloger_test.go @@ -12,14 +12,16 @@ func Test_JavascriptCataloger(t *testing.T) { locationSet := source.NewLocationSet(source.NewLocation("package-lock.json")) expectedPkgs := []pkg.Package{ { - Name: "@actions/core", - Version: "1.6.0", - FoundBy: "javascript-lock-cataloger", - PURL: "pkg:npm/%40actions/core@1.6.0", - Locations: locationSet, - Language: pkg.JavaScript, - Type: pkg.NpmPkg, - Licenses: []string{"MIT"}, + Name: "@actions/core", + Version: "1.6.0", + FoundBy: "javascript-lock-cataloger", + PURL: "pkg:npm/%40actions/core@1.6.0", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", source.NewLocation("package-lock.json")), + ), MetadataType: pkg.NpmPackageLockJSONMetadataType, Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz", Integrity: "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw=="}, }, @@ -35,14 +37,16 @@ func Test_JavascriptCataloger(t *testing.T) { Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", Integrity: "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="}, }, { - Name: "cowsay", - Version: "1.4.0", - FoundBy: "javascript-lock-cataloger", - PURL: "pkg:npm/cowsay@1.4.0", - Locations: locationSet, - Language: pkg.JavaScript, - Type: pkg.NpmPkg, - Licenses: []string{"MIT"}, + Name: "cowsay", + Version: "1.4.0", + FoundBy: "javascript-lock-cataloger", + PURL: "pkg:npm/cowsay@1.4.0", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", source.NewLocation("package-lock.json")), + ), MetadataType: pkg.NpmPackageLockJSONMetadataType, Metadata: pkg.NpmPackageLockJSONMetadata{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 314ce64e5..468854a39 100644 --- a/syft/pkg/cataloger/javascript/package.go +++ b/syft/pkg/cataloger/javascript/package.go @@ -12,30 +12,30 @@ import ( "github.com/anchore/syft/syft/source" ) -func newPackageJSONPackage(u packageJSON, locations ...source.Location) pkg.Package { - licenses, err := u.licensesFromJSON() +func newPackageJSONPackage(u packageJSON, indexLocation source.Location) pkg.Package { + licenseCandidates, err := u.licensesFromJSON() if err != nil { log.Warnf("unable to extract licenses from javascript package.json: %+v", err) } + license := pkg.NewLicensesFromLocation(indexLocation, licenseCandidates...) p := pkg.Package{ Name: u.Name, Version: u.Version, - Licenses: licenses, PURL: packageURL(u.Name, u.Version), - Locations: source.NewLocationSet(locations...), + Locations: source.NewLocationSet(indexLocation), Language: pkg.JavaScript, + Licenses: pkg.NewLicenseSet(license...), Type: pkg.NpmPkg, MetadataType: pkg.NpmPackageJSONMetadataType, Metadata: pkg.NpmPackageJSONMetadata{ Name: u.Name, Version: u.Version, + Description: u.Description, Author: u.Author.AuthorString(), Homepage: u.Homepage, URL: u.Repository.URL, - Licenses: licenses, Private: u.Private, - Description: u.Description, }, } @@ -77,12 +77,6 @@ func newPackageLockV1Package(resolver source.FileResolver, location source.Locat } func newPackageLockV2Package(resolver source.FileResolver, location source.Location, name string, u lockPackage) pkg.Package { - var licenses []string - - if u.License != nil { - licenses = u.License - } - return finalizeLockPkg( resolver, location, @@ -90,10 +84,10 @@ func newPackageLockV2Package(resolver source.FileResolver, location source.Locat Name: name, Version: u.Version, Locations: source.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(location, u.License...)...), PURL: packageURL(name, u.Version), Language: pkg.JavaScript, Type: pkg.NpmPkg, - Licenses: licenses, MetadataType: pkg.NpmPackageLockJSONMetadataType, Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: u.Resolved, Integrity: u.Integrity}, }, @@ -131,7 +125,8 @@ func newYarnLockPackage(resolver source.FileResolver, location source.Location, } func finalizeLockPkg(resolver source.FileResolver, location source.Location, p pkg.Package) pkg.Package { - p.Licenses = append(p.Licenses, addLicenses(p.Name, resolver, location)...) + licenseCandidate := addLicenses(p.Name, resolver, location) + p.Licenses.Add(pkg.NewLicensesFromLocation(location, licenseCandidate...)...) p.SetID() return p } @@ -140,13 +135,13 @@ func addLicenses(name string, resolver source.FileResolver, location source.Loca if resolver == nil { return allLicenses } + dir := path.Dir(location.RealPath) pkgPath := []string{dir, "node_modules"} pkgPath = append(pkgPath, strings.Split(name, "/")...) pkgPath = append(pkgPath, "package.json") pkgFile := path.Join(pkgPath...) locations, err := resolver.FilesByPath(pkgFile) - if err != nil { log.Debugf("an error occurred attempting to read: %s - %+v", pkgFile, err) return allLicenses diff --git a/syft/pkg/cataloger/javascript/parse_package_json.go b/syft/pkg/cataloger/javascript/parse_package_json.go index 2d943e710..59c8a5c50 100644 --- a/syft/pkg/cataloger/javascript/parse_package_json.go +++ b/syft/pkg/cataloger/javascript/parse_package_json.go @@ -140,7 +140,7 @@ func (r *repository) UnmarshalJSON(b []byte) error { return nil } -type license struct { +type npmPackageLicense struct { Type string `json:"type"` URL string `json:"url"` } @@ -154,7 +154,7 @@ func licenseFromJSON(b []byte) (string, error) { } // then try as object (this format is deprecated) - var licenseObject license + var licenseObject npmPackageLicense err = json.Unmarshal(b, &licenseObject) if err == nil { return licenseObject.Type, nil @@ -178,7 +178,7 @@ func (p packageJSON) licensesFromJSON() ([]string, error) { // The "licenses" field is deprecated. It should be inspected as a last resort. if multiLicense != nil && err == nil { - mapLicenses := func(licenses []license) []string { + mapLicenses := func(licenses []npmPackageLicense) []string { mappedLicenses := make([]string, len(licenses)) for i, l := range licenses { mappedLicenses[i] = l.Type @@ -192,8 +192,8 @@ func (p packageJSON) licensesFromJSON() ([]string, error) { return nil, err } -func licensesFromJSON(b []byte) ([]license, error) { - var licenseObject []license +func licensesFromJSON(b []byte) ([]npmPackageLicense, error) { + var licenseObject []npmPackageLicense err := json.Unmarshal(b, &licenseObject) if err == nil { return licenseObject, nil diff --git a/syft/pkg/cataloger/javascript/parse_package_json_test.go b/syft/pkg/cataloger/javascript/parse_package_json_test.go index 19d51238f..c0e0b17b0 100644 --- a/syft/pkg/cataloger/javascript/parse_package_json_test.go +++ b/syft/pkg/cataloger/javascript/parse_package_json_test.go @@ -18,12 +18,14 @@ func TestParsePackageJSON(t *testing.T) { { Fixture: "test-fixtures/pkg-json/package.json", ExpectedPkg: pkg.Package{ - Name: "npm", - Version: "6.14.6", - PURL: "pkg:npm/npm@6.14.6", - Type: pkg.NpmPkg, - Licenses: []string{"Artistic-2.0"}, - Language: pkg.JavaScript, + Name: "npm", + Version: "6.14.6", + PURL: "pkg:npm/npm@6.14.6", + Type: pkg.NpmPkg, + Language: pkg.JavaScript, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("Artistic-2.0", source.NewLocation("test-fixtures/pkg-json/package.json")), + ), MetadataType: pkg.NpmPackageJSONMetadataType, Metadata: pkg.NpmPackageJSONMetadata{ Name: "npm", @@ -31,7 +33,6 @@ func TestParsePackageJSON(t *testing.T) { Author: "Isaac Z. Schlueter (http://blog.izs.me)", Homepage: "https://docs.npmjs.com/", URL: "https://github.com/npm/cli", - Licenses: []string{"Artistic-2.0"}, Description: "a package manager for JavaScript", }, }, @@ -39,12 +40,14 @@ func TestParsePackageJSON(t *testing.T) { { Fixture: "test-fixtures/pkg-json/package-license-object.json", ExpectedPkg: pkg.Package{ - Name: "npm", - Version: "6.14.6", - PURL: "pkg:npm/npm@6.14.6", - Type: pkg.NpmPkg, - Licenses: []string{"ISC"}, - Language: pkg.JavaScript, + Name: "npm", + Version: "6.14.6", + PURL: "pkg:npm/npm@6.14.6", + Type: pkg.NpmPkg, + Language: pkg.JavaScript, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("ISC", source.NewLocation("test-fixtures/pkg-json/package-license-object.json")), + ), MetadataType: pkg.NpmPackageJSONMetadataType, Metadata: pkg.NpmPackageJSONMetadata{ Name: "npm", @@ -52,7 +55,6 @@ func TestParsePackageJSON(t *testing.T) { Author: "Isaac Z. Schlueter (http://blog.izs.me)", Homepage: "https://docs.npmjs.com/", URL: "https://github.com/npm/cli", - Licenses: []string{"ISC"}, Description: "a package manager for JavaScript", }, }, @@ -60,11 +62,14 @@ func TestParsePackageJSON(t *testing.T) { { Fixture: "test-fixtures/pkg-json/package-license-objects.json", ExpectedPkg: pkg.Package{ - Name: "npm", - Version: "6.14.6", - PURL: "pkg:npm/npm@6.14.6", - Type: pkg.NpmPkg, - Licenses: []string{"MIT", "Apache-2.0"}, + Name: "npm", + Version: "6.14.6", + PURL: "pkg:npm/npm@6.14.6", + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", source.NewLocation("test-fixtures/pkg-json/package-license-objects.json")), + pkg.NewLicenseFromLocations("Apache-2.0", source.NewLocation("test-fixtures/pkg-json/package-license-objects.json")), + ), Language: pkg.JavaScript, MetadataType: pkg.NpmPackageJSONMetadataType, Metadata: pkg.NpmPackageJSONMetadata{ @@ -73,7 +78,6 @@ func TestParsePackageJSON(t *testing.T) { Author: "Isaac Z. Schlueter (http://blog.izs.me)", Homepage: "https://docs.npmjs.com/", URL: "https://github.com/npm/cli", - Licenses: []string{"MIT", "Apache-2.0"}, Description: "a package manager for JavaScript", }, }, @@ -85,7 +89,6 @@ func TestParsePackageJSON(t *testing.T) { Version: "6.14.6", PURL: "pkg:npm/npm@6.14.6", Type: pkg.NpmPkg, - Licenses: nil, Language: pkg.JavaScript, MetadataType: pkg.NpmPackageJSONMetadataType, Metadata: pkg.NpmPackageJSONMetadata{ @@ -94,7 +97,6 @@ func TestParsePackageJSON(t *testing.T) { Author: "Isaac Z. Schlueter (http://blog.izs.me)", Homepage: "https://docs.npmjs.com/", URL: "https://github.com/npm/cli", - Licenses: nil, Description: "a package manager for JavaScript", }, }, @@ -106,7 +108,6 @@ func TestParsePackageJSON(t *testing.T) { Version: "6.14.6", PURL: "pkg:npm/npm@6.14.6", Type: pkg.NpmPkg, - Licenses: []string{}, Language: pkg.JavaScript, MetadataType: pkg.NpmPackageJSONMetadataType, Metadata: pkg.NpmPackageJSONMetadata{ @@ -115,7 +116,6 @@ func TestParsePackageJSON(t *testing.T) { Author: "Isaac Z. Schlueter (http://blog.izs.me)", Homepage: "https://docs.npmjs.com/", URL: "https://github.com/npm/cli", - Licenses: []string{}, Description: "a package manager for JavaScript", }, }, @@ -123,11 +123,13 @@ func TestParsePackageJSON(t *testing.T) { { Fixture: "test-fixtures/pkg-json/package-nested-author.json", ExpectedPkg: pkg.Package{ - Name: "npm", - Version: "6.14.6", - PURL: "pkg:npm/npm@6.14.6", - Type: pkg.NpmPkg, - Licenses: []string{"Artistic-2.0"}, + Name: "npm", + Version: "6.14.6", + PURL: "pkg:npm/npm@6.14.6", + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("Artistic-2.0", source.NewLocation("test-fixtures/pkg-json/package-nested-author.json")), + ), Language: pkg.JavaScript, MetadataType: pkg.NpmPackageJSONMetadataType, Metadata: pkg.NpmPackageJSONMetadata{ @@ -136,7 +138,6 @@ func TestParsePackageJSON(t *testing.T) { Author: "Isaac Z. Schlueter (http://blog.izs.me)", Homepage: "https://docs.npmjs.com/", URL: "https://github.com/npm/cli", - Licenses: []string{"Artistic-2.0"}, Description: "a package manager for JavaScript", }, }, @@ -144,11 +145,13 @@ func TestParsePackageJSON(t *testing.T) { { Fixture: "test-fixtures/pkg-json/package-repo-string.json", ExpectedPkg: pkg.Package{ - Name: "function-bind", - Version: "1.1.1", - PURL: "pkg:npm/function-bind@1.1.1", - Type: pkg.NpmPkg, - Licenses: []string{"MIT"}, + Name: "function-bind", + Version: "1.1.1", + PURL: "pkg:npm/function-bind@1.1.1", + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", source.NewLocation("test-fixtures/pkg-json/package-repo-string.json")), + ), Language: pkg.JavaScript, MetadataType: pkg.NpmPackageJSONMetadataType, Metadata: pkg.NpmPackageJSONMetadata{ @@ -157,7 +160,6 @@ func TestParsePackageJSON(t *testing.T) { Author: "Raynos ", Homepage: "https://github.com/Raynos/function-bind", URL: "git://github.com/Raynos/function-bind.git", - Licenses: []string{"MIT"}, Description: "Implementation of Function.prototype.bind", }, }, @@ -165,11 +167,13 @@ func TestParsePackageJSON(t *testing.T) { { Fixture: "test-fixtures/pkg-json/package-private.json", ExpectedPkg: pkg.Package{ - Name: "npm", - Version: "6.14.6", - PURL: "pkg:npm/npm@6.14.6", - Type: pkg.NpmPkg, - Licenses: []string{"Artistic-2.0"}, + Name: "npm", + Version: "6.14.6", + PURL: "pkg:npm/npm@6.14.6", + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("Artistic-2.0", source.NewLocation("test-fixtures/pkg-json/package-private.json")), + ), Language: pkg.JavaScript, MetadataType: pkg.NpmPackageJSONMetadataType, Metadata: pkg.NpmPackageJSONMetadata{ @@ -178,7 +182,6 @@ func TestParsePackageJSON(t *testing.T) { Author: "Isaac Z. Schlueter (http://blog.izs.me)", Homepage: "https://docs.npmjs.com/", URL: "https://github.com/npm/cli", - Licenses: []string{"Artistic-2.0"}, Private: true, Description: "a package manager for JavaScript", }, diff --git a/syft/pkg/cataloger/javascript/parse_package_lock.go b/syft/pkg/cataloger/javascript/parse_package_lock.go index d0a75f1ac..7ca2669b3 100644 --- a/syft/pkg/cataloger/javascript/parse_package_lock.go +++ b/syft/pkg/cataloger/javascript/parse_package_lock.go @@ -25,39 +25,6 @@ type packageLock struct { Packages map[string]lockPackage } -// packageLockLicense -type packageLockLicense []string - -func (licenses *packageLockLicense) UnmarshalJSON(data []byte) (err error) { - // The license field could be either a string or an array. - - // 1. An array - var arr []string - if err := json.Unmarshal(data, &arr); err == nil { - *licenses = arr - return nil - } - - // 2. A string - var str string - if err = json.Unmarshal(data, &str); err == nil { - *licenses = make([]string, 1) - (*licenses)[0] = str - return nil - } - - // debug the content we did not expect - if len(data) > 0 { - log.WithFields("license", string(data)).Debug("Unable to parse the following `license` value in package-lock.json") - } - - // 3. Unexpected - // In case we are unable to parse the license field, - // i.e if we have not covered the full specification, - // we do not want to throw an error, instead assign nil. - return nil -} - // lockDependency represents a single package dependency listed in the package.lock json file type lockDependency struct { Version string `json:"version"` @@ -73,6 +40,9 @@ type lockPackage struct { License packageLockLicense `json:"license"` } +// packageLockLicense +type packageLockLicense []string + // parsePackageLock parses a package-lock.json and returns the discovered JavaScript packages. func parsePackageLock(resolver source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { // in the case we find package-lock.json files in the node_modules directories, skip those @@ -125,6 +95,36 @@ func parsePackageLock(resolver source.FileResolver, _ *generic.Environment, read return pkgs, nil, nil } +func (licenses *packageLockLicense) UnmarshalJSON(data []byte) (err error) { + // The license field could be either a string or an array. + + // 1. An array + var arr []string + if err := json.Unmarshal(data, &arr); err == nil { + *licenses = arr + return nil + } + + // 2. A string + var str string + if err = json.Unmarshal(data, &str); err == nil { + *licenses = make([]string, 1) + (*licenses)[0] = str + return nil + } + + // debug the content we did not expect + if len(data) > 0 { + log.WithFields("license", string(data)).Debug("Unable to parse the following `license` value in package-lock.json") + } + + // 3. Unexpected + // In case we are unable to parse the license field, + // i.e if we have not covered the full specification, + // we do not want to throw an error, instead assign nil. + return nil +} + func getNameFromPath(path string) string { parts := strings.Split(path, "node_modules/") return parts[len(parts)-1] diff --git a/syft/pkg/cataloger/javascript/parse_package_lock_test.go b/syft/pkg/cataloger/javascript/parse_package_lock_test.go index 75601d437..dec36fb5f 100644 --- a/syft/pkg/cataloger/javascript/parse_package_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_package_lock_test.go @@ -134,42 +134,50 @@ func TestParsePackageLockV2(t *testing.T) { Metadata: pkg.NpmPackageLockJSONMetadata{}, }, { - Name: "@types/prop-types", - Version: "15.7.5", - PURL: "pkg:npm/%40types/prop-types@15.7.5", - Language: pkg.JavaScript, - Type: pkg.NpmPkg, - Licenses: []string{"MIT"}, + Name: "@types/prop-types", + Version: "15.7.5", + PURL: "pkg:npm/%40types/prop-types@15.7.5", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)), + ), MetadataType: "NpmPackageLockJsonMetadata", Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", Integrity: "sha1-XxnSuFqY6VWANvajysyIGUIPBc8="}, }, { - Name: "@types/react", - Version: "18.0.17", - PURL: "pkg:npm/%40types/react@18.0.17", - Language: pkg.JavaScript, - Type: pkg.NpmPkg, - Licenses: []string{"MIT"}, + Name: "@types/react", + Version: "18.0.17", + PURL: "pkg:npm/%40types/react@18.0.17", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)), + ), MetadataType: "NpmPackageLockJsonMetadata", Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@types/react/-/react-18.0.17.tgz", Integrity: "sha1-RYPZwyLWfv5LOak10iPtzHBQzPQ="}, }, { - Name: "@types/scheduler", - Version: "0.16.2", - PURL: "pkg:npm/%40types/scheduler@0.16.2", - Language: pkg.JavaScript, - Type: pkg.NpmPkg, - Licenses: []string{"MIT"}, + Name: "@types/scheduler", + Version: "0.16.2", + PURL: "pkg:npm/%40types/scheduler@0.16.2", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)), + ), MetadataType: "NpmPackageLockJsonMetadata", Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", Integrity: "sha1-GmL4lSVyPd4kuhsBsJK/XfitTTk="}, }, { - Name: "csstype", - Version: "3.1.0", - PURL: "pkg:npm/csstype@3.1.0", - Language: pkg.JavaScript, - Type: pkg.NpmPkg, - Licenses: []string{"MIT"}, + Name: "csstype", + Version: "3.1.0", + PURL: "pkg:npm/csstype@3.1.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)), + ), MetadataType: "NpmPackageLockJsonMetadata", Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", Integrity: "sha1-TdysNxjXh8+d8NG30VAzklyPKfI="}, }, @@ -268,33 +276,35 @@ func TestParsePackageLockAlias(t *testing.T) { }, } - v2Pkg := pkg.Package{ - Name: "alias-check", - Version: "1.0.0", - PURL: "pkg:npm/alias-check@1.0.0", - Language: pkg.JavaScript, - Type: pkg.NpmPkg, - Licenses: []string{"ISC"}, - MetadataType: "NpmPackageLockJsonMetadata", - Metadata: pkg.NpmPackageLockJSONMetadata{}, - } - packageLockV1 := "test-fixtures/pkg-lock/alias-package-lock-1.json" packageLockV2 := "test-fixtures/pkg-lock/alias-package-lock-2.json" packageLocks := []string{packageLockV1, packageLockV2} - for _, packageLock := range packageLocks { + v2Pkg := pkg.Package{ + Name: "alias-check", + Version: "1.0.0", + PURL: "pkg:npm/alias-check@1.0.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("ISC", source.NewLocation(packageLockV2)), + ), + MetadataType: "NpmPackageLockJsonMetadata", + Metadata: pkg.NpmPackageLockJSONMetadata{}, + } + + for _, pl := range packageLocks { expected := make([]pkg.Package, len(commonPkgs)) copy(expected, commonPkgs) - if packageLock == packageLockV2 { + if pl == packageLockV2 { expected = append(expected, v2Pkg) } for i := range expected { - expected[i].Locations.Add(source.NewLocation(packageLock)) + expected[i].Locations.Add(source.NewLocation(pl)) } - pkgtest.TestFileParser(t, packageLock, parsePackageLock, expected, expectedRelationships) + pkgtest.TestFileParser(t, pl, parsePackageLock, expected, expectedRelationships) } } @@ -303,31 +313,39 @@ func TestParsePackageLockLicenseWithArray(t *testing.T) { var expectedRelationships []artifact.Relationship expectedPkgs := []pkg.Package{ { - Name: "tmp", - Version: "1.0.0", - Licenses: []string{"ISC"}, - Language: pkg.JavaScript, - Type: pkg.NpmPkg, + Name: "tmp", + Version: "1.0.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("ISC", source.NewLocation(fixture)), + ), PURL: "pkg:npm/tmp@1.0.0", MetadataType: "NpmPackageLockJsonMetadata", Metadata: pkg.NpmPackageLockJSONMetadata{}, }, { - Name: "pause-stream", - Version: "0.0.11", - Licenses: []string{"MIT", "Apache2"}, - Language: pkg.JavaScript, - Type: pkg.NpmPkg, + Name: "pause-stream", + Version: "0.0.11", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)), + pkg.NewLicenseFromLocations("Apache2", source.NewLocation(fixture)), + ), PURL: "pkg:npm/pause-stream@0.0.11", MetadataType: "NpmPackageLockJsonMetadata", Metadata: pkg.NpmPackageLockJSONMetadata{}, }, { - Name: "through", - Version: "2.3.8", - Licenses: []string{"MIT"}, - Language: pkg.JavaScript, - Type: pkg.NpmPkg, + Name: "through", + Version: "2.3.8", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)), + ), PURL: "pkg:npm/through@2.3.8", MetadataType: "NpmPackageLockJsonMetadata", Metadata: pkg.NpmPackageLockJSONMetadata{}, diff --git a/syft/pkg/cataloger/kernel/cataloger_test.go b/syft/pkg/cataloger/kernel/cataloger_test.go index ed5e6b083..b223acf1a 100644 --- a/syft/pkg/cataloger/kernel/cataloger_test.go +++ b/syft/pkg/cataloger/kernel/cataloger_test.go @@ -47,9 +47,14 @@ func Test_KernelCataloger(t *testing.T) { "/lib/modules/6.0.7-301.fc37.x86_64/kernel/drivers/tty/ttynull.ko", ), ), - Licenses: []string{ - "GPL v2", - }, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("GPL v2", + source.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", + ), + ), + ), Type: pkg.LinuxKernelModulePkg, PURL: "pkg:generic/ttynull", MetadataType: pkg.LinuxKernelModuleMetadataType, diff --git a/syft/pkg/cataloger/kernel/package.go b/syft/pkg/cataloger/kernel/package.go index 69fab21b3..3ea606688 100644 --- a/syft/pkg/cataloger/kernel/package.go +++ b/syft/pkg/cataloger/kernel/package.go @@ -10,11 +10,11 @@ import ( const linuxKernelPackageName = "linux-kernel" -func newLinuxKernelPackage(metadata pkg.LinuxKernelMetadata, locations ...source.Location) pkg.Package { +func newLinuxKernelPackage(metadata pkg.LinuxKernelMetadata, archiveLocation source.Location) pkg.Package { p := pkg.Package{ Name: linuxKernelPackageName, Version: metadata.Version, - Locations: source.NewLocationSet(locations...), + Locations: source.NewLocationSet(archiveLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), PURL: packageURL(linuxKernelPackageName, metadata.Version), Type: pkg.LinuxKernelPkg, MetadataType: pkg.LinuxKernelMetadataType, @@ -26,19 +26,12 @@ func newLinuxKernelPackage(metadata pkg.LinuxKernelMetadata, locations ...source return p } -func newLinuxKernelModulePackage(metadata pkg.LinuxKernelModuleMetadata, locations ...source.Location) pkg.Package { - var licenses []string - if metadata.License != "" { - licenses = []string{metadata.License} - } else { - licenses = []string{} - } - +func newLinuxKernelModulePackage(metadata pkg.LinuxKernelModuleMetadata, kmLocation source.Location) pkg.Package { p := pkg.Package{ Name: metadata.Name, Version: metadata.Version, - Locations: source.NewLocationSet(locations...), - Licenses: licenses, + Locations: source.NewLocationSet(kmLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(kmLocation, metadata.License)...), PURL: packageURL(metadata.Name, metadata.Version), Type: pkg.LinuxKernelModulePkg, MetadataType: pkg.LinuxKernelModuleMetadataType, diff --git a/syft/pkg/cataloger/kernel/parse_linux_kernel_file.go b/syft/pkg/cataloger/kernel/parse_linux_kernel_file.go index 303e88192..0be32c5b6 100644 --- a/syft/pkg/cataloger/kernel/parse_linux_kernel_file.go +++ b/syft/pkg/cataloger/kernel/parse_linux_kernel_file.go @@ -37,7 +37,7 @@ func parseLinuxKernelFile(_ source.FileResolver, _ *generic.Environment, reader return []pkg.Package{ newLinuxKernelPackage( metadata, - reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + reader.Location, ), }, nil, nil } 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 fb0d818a5..3adeb5632 100644 --- a/syft/pkg/cataloger/kernel/parse_linux_kernel_module_file.go +++ b/syft/pkg/cataloger/kernel/parse_linux_kernel_module_file.go @@ -32,7 +32,7 @@ func parseLinuxKernelModuleFile(_ source.FileResolver, _ *generic.Environment, r return []pkg.Package{ newLinuxKernelModulePackage( *metadata, - reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + reader.Location, ), }, nil, nil } diff --git a/syft/pkg/cataloger/php/package.go b/syft/pkg/cataloger/php/package.go index ee3be7019..507fd26da 100644 --- a/syft/pkg/cataloger/php/package.go +++ b/syft/pkg/cataloger/php/package.go @@ -8,23 +8,24 @@ import ( "github.com/anchore/syft/syft/source" ) -func newComposerLockPackage(m pkg.PhpComposerJSONMetadata, location ...source.Location) pkg.Package { +func newComposerLockPackage(m parsedData, indexLocation source.Location) pkg.Package { p := pkg.Package{ Name: m.Name, Version: m.Version, - Locations: source.NewLocationSet(location...), + Locations: source.NewLocationSet(indexLocation), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(indexLocation, m.License...)...), PURL: packageURL(m), Language: pkg.PHP, Type: pkg.PhpComposerPkg, MetadataType: pkg.PhpComposerJSONMetadataType, - Metadata: m, + Metadata: m.PhpComposerJSONMetadata, } p.SetID() return p } -func packageURL(m pkg.PhpComposerJSONMetadata) string { +func packageURL(m parsedData) string { var name, vendor string fields := strings.Split(m.Name, "/") switch len(fields) { diff --git a/syft/pkg/cataloger/php/package_test.go b/syft/pkg/cataloger/php/package_test.go index 9a32df1ff..310f4100d 100644 --- a/syft/pkg/cataloger/php/package_test.go +++ b/syft/pkg/cataloger/php/package_test.go @@ -11,30 +11,39 @@ import ( func Test_packageURL(t *testing.T) { tests := []struct { name string - metadata pkg.PhpComposerJSONMetadata + metadata parsedData expected string }{ { name: "with extractable vendor", - metadata: pkg.PhpComposerJSONMetadata{ - Name: "ven/name", - Version: "1.0.1", + metadata: parsedData{ + []string{}, + pkg.PhpComposerJSONMetadata{ + Version: "1.0.1", + Name: "ven/name", + }, }, expected: "pkg:composer/ven/name@1.0.1", }, { name: "name with slashes (invalid)", - metadata: pkg.PhpComposerJSONMetadata{ - Name: "ven/name/component", - Version: "1.0.1", + metadata: parsedData{ + []string{}, + pkg.PhpComposerJSONMetadata{ + Name: "ven/name/component", + Version: "1.0.1", + }, }, expected: "pkg:composer/ven/name-component@1.0.1", }, { name: "unknown vendor", - metadata: pkg.PhpComposerJSONMetadata{ - Name: "name", - Version: "1.0.1", + metadata: parsedData{ + []string{}, + pkg.PhpComposerJSONMetadata{ + Name: "name", + Version: "1.0.1", + }, }, expected: "pkg:composer/name@1.0.1", }, diff --git a/syft/pkg/cataloger/php/parse_composer_lock.go b/syft/pkg/cataloger/php/parse_composer_lock.go index 516146350..248b7519e 100644 --- a/syft/pkg/cataloger/php/parse_composer_lock.go +++ b/syft/pkg/cataloger/php/parse_composer_lock.go @@ -14,9 +14,14 @@ import ( var _ generic.Parser = parseComposerLock +type parsedData struct { + License []string `json:"license"` + pkg.PhpComposerJSONMetadata +} + type composerLock struct { - Packages []pkg.PhpComposerJSONMetadata `json:"packages"` - PackageDev []pkg.PhpComposerJSONMetadata `json:"packages-dev"` + Packages []parsedData `json:"packages"` + PackageDev []parsedData `json:"packages-dev"` } // parseComposerLock is a parser function for Composer.lock contents, returning "Default" php packages discovered. @@ -40,6 +45,11 @@ func parseComposerLock(_ source.FileResolver, _ *generic.Environment, reader sou ), ) } + + // TODO: did we omit this on purpose? + // for _, m := range lock.PackageDev { + // pkgs = append(pkgs, newComposerLockPackage(m, reader.Location)) + //} } return pkgs, nil, nil diff --git a/syft/pkg/cataloger/php/parse_composer_lock_test.go b/syft/pkg/cataloger/php/parse_composer_lock_test.go index f47049707..ad7814a97 100644 --- a/syft/pkg/cataloger/php/parse_composer_lock_test.go +++ b/syft/pkg/cataloger/php/parse_composer_lock_test.go @@ -15,10 +15,13 @@ func TestParseComposerFileLock(t *testing.T) { locations := source.NewLocationSet(source.NewLocation(fixture)) expectedPkgs := []pkg.Package{ { - Name: "adoy/fastcgi-client", - Version: "1.0.2", - PURL: "pkg:composer/adoy/fastcgi-client@1.0.2", - Locations: locations, + Name: "adoy/fastcgi-client", + Version: "1.0.2", + PURL: "pkg:composer/adoy/fastcgi-client@1.0.2", + Locations: locations, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)), + ), Language: pkg.PHP, Type: pkg.PhpComposerPkg, MetadataType: pkg.PhpComposerJSONMetadataType, @@ -37,9 +40,6 @@ func TestParseComposerFileLock(t *testing.T) { }, Type: "library", NotificationURL: "https://packagist.org/downloads/", - License: []string{ - "MIT", - }, Authors: []pkg.PhpComposerAuthors{ { Name: "Pierrick Charron", @@ -55,11 +55,14 @@ func TestParseComposerFileLock(t *testing.T) { }, }, { - Name: "alcaeus/mongo-php-adapter", - Version: "1.1.11", - Locations: locations, - PURL: "pkg:composer/alcaeus/mongo-php-adapter@1.1.11", - Language: pkg.PHP, + Name: "alcaeus/mongo-php-adapter", + Version: "1.1.11", + Locations: locations, + PURL: "pkg:composer/alcaeus/mongo-php-adapter@1.1.11", + Language: pkg.PHP, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)), + ), Type: pkg.PhpComposerPkg, MetadataType: pkg.PhpComposerJSONMetadataType, Metadata: pkg.PhpComposerJSONMetadata{ @@ -91,9 +94,6 @@ func TestParseComposerFileLock(t *testing.T) { }, Type: "library", NotificationURL: "https://packagist.org/downloads/", - License: []string{ - "MIT", - }, Authors: []pkg.PhpComposerAuthors{ { Name: "alcaeus", diff --git a/syft/pkg/cataloger/php/parse_installed_json.go b/syft/pkg/cataloger/php/parse_installed_json.go index e5309c5f3..8c1213200 100644 --- a/syft/pkg/cataloger/php/parse_installed_json.go +++ b/syft/pkg/cataloger/php/parse_installed_json.go @@ -16,19 +16,19 @@ var _ generic.Parser = parseComposerLock // Note: composer version 2 introduced a new structure for the installed.json file, so we support both type installedJSONComposerV2 struct { - Packages []pkg.PhpComposerJSONMetadata `json:"packages"` + Packages []parsedData `json:"packages"` } func (w *installedJSONComposerV2) UnmarshalJSON(data []byte) error { type compv2 struct { - Packages []pkg.PhpComposerJSONMetadata `json:"packages"` + Packages []parsedData `json:"packages"` } compv2er := new(compv2) err := json.Unmarshal(data, &compv2er) if err != nil { // If we had an err or, we may be dealing with a composer v.1 installed.json // which should be all arrays - var packages []pkg.PhpComposerJSONMetadata + var packages []parsedData err := json.Unmarshal(data, &packages) if err != nil { return err diff --git a/syft/pkg/cataloger/php/parse_installed_json_test.go b/syft/pkg/cataloger/php/parse_installed_json_test.go index b2788f506..4b914640a 100644 --- a/syft/pkg/cataloger/php/parse_installed_json_test.go +++ b/syft/pkg/cataloger/php/parse_installed_json_test.go @@ -24,6 +24,9 @@ func TestParseInstalledJsonComposerV1(t *testing.T) { Language: pkg.PHP, Type: pkg.PhpComposerPkg, MetadataType: pkg.PhpComposerJSONMetadataType, + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("MIT"), + ), Metadata: pkg.PhpComposerJSONMetadata{ Name: "asm89/stack-cors", Version: "1.3.0", @@ -49,9 +52,6 @@ func TestParseInstalledJsonComposerV1(t *testing.T) { Time: "2019-12-24T22:41:47+00:00", Type: "library", NotificationURL: "https://packagist.org/downloads/", - License: []string{ - "MIT", - }, Authors: []pkg.PhpComposerAuthors{ { Name: "Alexander", @@ -68,11 +68,14 @@ func TestParseInstalledJsonComposerV1(t *testing.T) { }, }, { - Name: "behat/mink", - Version: "v1.8.1", - PURL: "pkg:composer/behat/mink@v1.8.1", - Language: pkg.PHP, - Type: pkg.PhpComposerPkg, + Name: "behat/mink", + Version: "v1.8.1", + PURL: "pkg:composer/behat/mink@v1.8.1", + Language: pkg.PHP, + Type: pkg.PhpComposerPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("MIT"), + ), MetadataType: pkg.PhpComposerJSONMetadataType, Metadata: pkg.PhpComposerJSONMetadata{ Name: "behat/mink", @@ -106,9 +109,6 @@ func TestParseInstalledJsonComposerV1(t *testing.T) { Time: "2020-03-11T15:45:53+00:00", Type: "library", NotificationURL: "https://packagist.org/downloads/", - License: []string{ - "MIT", - }, Authors: []pkg.PhpComposerAuthors{ { Name: "Konstantin Kudryashov", @@ -133,9 +133,14 @@ func TestParseInstalledJsonComposerV1(t *testing.T) { locations := source.NewLocationSet(source.NewLocation(fixture)) for i := range expectedPkgs { expectedPkgs[i].Locations = locations + locationLicenses := pkg.NewLicenseSet() + for _, license := range expectedPkgs[i].Licenses.ToSlice() { + license.Location = locations + locationLicenses.Add(license) + } + expectedPkgs[i].Licenses = locationLicenses } pkgtest.TestFileParser(t, fixture, parseInstalledJSON, expectedPkgs, expectedRelationships) }) } - } diff --git a/syft/pkg/cataloger/portage/cataloger_test.go b/syft/pkg/cataloger/portage/cataloger_test.go index 3f4494b29..b2ff5f26d 100644 --- a/syft/pkg/cataloger/portage/cataloger_test.go +++ b/syft/pkg/cataloger/portage/cataloger_test.go @@ -11,7 +11,7 @@ import ( ) func TestPortageCataloger(t *testing.T) { - + expectedLicenseLocation := source.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/LICENSE") expectedPkgs := []pkg.Package{ { Name: "app-containers/skopeo", @@ -20,10 +20,10 @@ func TestPortageCataloger(t *testing.T) { PURL: "pkg:ebuild/app-containers/skopeo@1.5.1", Locations: source.NewLocationSet( source.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/CONTENTS"), - source.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/LICENSE"), source.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/SIZE"), + expectedLicenseLocation, ), - Licenses: []string{"Apache-2.0", "BSD", "BSD-2", "CC-BY-SA-4.0", "ISC", "MIT"}, + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(expectedLicenseLocation, "Apache-2.0", "BSD", "BSD-2", "CC-BY-SA-4.0", "ISC", "MIT")...), Type: pkg.PortagePkg, MetadataType: pkg.PortageMetadataType, Metadata: pkg.PortageMetadata{ diff --git a/syft/pkg/cataloger/portage/parse_portage_contents.go b/syft/pkg/cataloger/portage/parse_portage_contents.go index 799cb8b2a..ac93c6ea0 100644 --- a/syft/pkg/cataloger/portage/parse_portage_contents.go +++ b/syft/pkg/cataloger/portage/parse_portage_contents.go @@ -6,7 +6,6 @@ import ( "path" "path/filepath" "regexp" - "sort" "strconv" "strings" @@ -116,9 +115,9 @@ func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pk findings.Add(token) } } - licenses := findings.ToSlice() - sort.Strings(licenses) - p.Licenses = licenses + + licenseCandidates := findings.ToSlice() + p.Licenses = pkg.NewLicenseSet(pkg.NewLicensesFromLocation(*location, licenseCandidates...)...) p.Locations.Add(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation)) } diff --git a/syft/pkg/cataloger/python/cataloger_test.go b/syft/pkg/cataloger/python/cataloger_test.go index 8515daf4b..10522f215 100644 --- a/syft/pkg/cataloger/python/cataloger_test.go +++ b/syft/pkg/cataloger/python/cataloger_test.go @@ -40,18 +40,19 @@ func Test_PackageCataloger(t *testing.T) { "test-fixtures/egg-info/top_level.txt", }, expectedPackage: pkg.Package{ - Name: "requests", - Version: "2.22.0", - PURL: "pkg:pypi/requests@2.22.0", - Type: pkg.PythonPkg, - Language: pkg.Python, - Licenses: []string{"Apache 2.0"}, + Name: "requests", + Version: "2.22.0", + PURL: "pkg:pypi/requests@2.22.0", + Type: pkg.PythonPkg, + Language: pkg.Python, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("Apache 2.0", source.NewLocation("test-fixtures/egg-info/PKG-INFO")), + ), FoundBy: "python-package-cataloger", MetadataType: pkg.PythonPackageMetadataType, Metadata: pkg.PythonPackageMetadata{ Name: "requests", Version: "2.22.0", - License: "Apache 2.0", Platform: "UNKNOWN", Author: "Kenneth Reitz", AuthorEmail: "me@kennethreitz.org", @@ -77,18 +78,19 @@ func Test_PackageCataloger(t *testing.T) { "test-fixtures/dist-info/direct_url.json", }, expectedPackage: pkg.Package{ - Name: "Pygments", - Version: "2.6.1", - PURL: "pkg:pypi/Pygments@2.6.1?vcs_url=git+https://github.com/python-test/test.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - Type: pkg.PythonPkg, - Language: pkg.Python, - Licenses: []string{"BSD License"}, + Name: "Pygments", + Version: "2.6.1", + PURL: "pkg:pypi/Pygments@2.6.1?vcs_url=git+https://github.com/python-test/test.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + Type: pkg.PythonPkg, + Language: pkg.Python, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("BSD License", source.NewLocation("test-fixtures/dist-info/METADATA")), + ), FoundBy: "python-package-cataloger", MetadataType: pkg.PythonPackageMetadataType, Metadata: pkg.PythonPackageMetadata{ Name: "Pygments", Version: "2.6.1", - License: "BSD License", Platform: "any", Author: "Georg Brandl", AuthorEmail: "georg@python.org", @@ -114,18 +116,19 @@ func Test_PackageCataloger(t *testing.T) { "test-fixtures/malformed-record/dist-info/RECORD", }, expectedPackage: pkg.Package{ - Name: "Pygments", - Version: "2.6.1", - PURL: "pkg:pypi/Pygments@2.6.1", - Type: pkg.PythonPkg, - Language: pkg.Python, - Licenses: []string{"BSD License"}, + Name: "Pygments", + Version: "2.6.1", + PURL: "pkg:pypi/Pygments@2.6.1", + Type: pkg.PythonPkg, + Language: pkg.Python, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("BSD License", source.NewLocation("test-fixtures/malformed-record/dist-info/METADATA")), + ), FoundBy: "python-package-cataloger", MetadataType: pkg.PythonPackageMetadataType, Metadata: pkg.PythonPackageMetadata{ Name: "Pygments", Version: "2.6.1", - License: "BSD License", Platform: "any", Author: "Georg Brandl", AuthorEmail: "georg@python.org", @@ -145,18 +148,19 @@ func Test_PackageCataloger(t *testing.T) { name: "partial dist-info directory", fixtures: []string{"test-fixtures/partial.dist-info/METADATA"}, expectedPackage: pkg.Package{ - Name: "Pygments", - Version: "2.6.1", - PURL: "pkg:pypi/Pygments@2.6.1", - Type: pkg.PythonPkg, - Language: pkg.Python, - Licenses: []string{"BSD License"}, + Name: "Pygments", + Version: "2.6.1", + PURL: "pkg:pypi/Pygments@2.6.1", + Type: pkg.PythonPkg, + Language: pkg.Python, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("BSD License", source.NewLocation("test-fixtures/partial.dist-info/METADATA")), + ), FoundBy: "python-package-cataloger", MetadataType: pkg.PythonPackageMetadataType, Metadata: pkg.PythonPackageMetadata{ Name: "Pygments", Version: "2.6.1", - License: "BSD License", Platform: "any", Author: "Georg Brandl", AuthorEmail: "georg@python.org", @@ -168,18 +172,19 @@ func Test_PackageCataloger(t *testing.T) { name: "egg-info regular file", fixtures: []string{"test-fixtures/test.egg-info"}, expectedPackage: pkg.Package{ - Name: "requests", - Version: "2.22.0", - PURL: "pkg:pypi/requests@2.22.0", - Type: pkg.PythonPkg, - Language: pkg.Python, - Licenses: []string{"Apache 2.0"}, + Name: "requests", + Version: "2.22.0", + PURL: "pkg:pypi/requests@2.22.0", + Type: pkg.PythonPkg, + Language: pkg.Python, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("Apache 2.0", source.NewLocation("test-fixtures/test.egg-info")), + ), FoundBy: "python-package-cataloger", MetadataType: pkg.PythonPackageMetadataType, Metadata: pkg.PythonPackageMetadata{ Name: "requests", Version: "2.22.0", - License: "Apache 2.0", Platform: "UNKNOWN", Author: "Kenneth Reitz", AuthorEmail: "me@kennethreitz.org", diff --git a/syft/pkg/cataloger/python/package.go b/syft/pkg/cataloger/python/package.go index d247597dc..68f7f1dcc 100644 --- a/syft/pkg/cataloger/python/package.go +++ b/syft/pkg/cataloger/python/package.go @@ -57,25 +57,21 @@ func newPackageForRequirementsWithMetadata(name, version string, metadata pkg.Py return p } -func newPackageForPackage(m pkg.PythonPackageMetadata, sources ...source.Location) pkg.Package { - var licenses []string - if m.License != "" { - licenses = []string{m.License} - } - +func newPackageForPackage(m parsedData, sources ...source.Location) pkg.Package { p := pkg.Package{ Name: m.Name, Version: m.Version, - PURL: packageURL(m.Name, m.Version, &m), + PURL: packageURL(m.Name, m.Version, &m.PythonPackageMetadata), Locations: source.NewLocationSet(sources...), - Licenses: licenses, + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(m.LicenseLocation, m.Licenses)...), Language: pkg.Python, Type: pkg.PythonPkg, MetadataType: pkg.PythonPackageMetadataType, - Metadata: m, + Metadata: m.PythonPackageMetadata, } p.SetID() + return p } diff --git a/syft/pkg/cataloger/python/parse_wheel_egg.go b/syft/pkg/cataloger/python/parse_wheel_egg.go index 7299e150c..911e7801c 100644 --- a/syft/pkg/cataloger/python/parse_wheel_egg.go +++ b/syft/pkg/cataloger/python/parse_wheel_egg.go @@ -17,21 +17,21 @@ import ( // parseWheelOrEgg takes the primary metadata file reference and returns the python package it represents. func parseWheelOrEgg(resolver source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { - metadata, sources, err := assembleEggOrWheelMetadata(resolver, reader.Location) + pd, sources, err := assembleEggOrWheelMetadata(resolver, reader.Location) if err != nil { return nil, nil, err } - if metadata == nil { + if pd == nil { return nil, nil, nil } // This can happen for Python 2.7 where it is reported from an egg-info, but Python is // the actual runtime, it isn't a "package". The special-casing here allows to skip it - if metadata.Name == "Python" { + if pd.Name == "Python" { return nil, nil, nil } - pkgs := []pkg.Package{newPackageForPackage(*metadata, sources...)} + pkgs := []pkg.Package{newPackageForPackage(*pd, sources...)} return pkgs, nil, nil } @@ -160,7 +160,7 @@ func fetchDirectURLData(resolver source.FileResolver, metadataLocation source.Lo } // assembleEggOrWheelMetadata discovers and accumulates python package metadata from multiple file sources and returns a single metadata object as well as a list of files where the metadata was derived from. -func assembleEggOrWheelMetadata(resolver source.FileResolver, metadataLocation source.Location) (*pkg.PythonPackageMetadata, []source.Location, error) { +func assembleEggOrWheelMetadata(resolver source.FileResolver, metadataLocation source.Location) (*parsedData, []source.Location, error) { var sources = []source.Location{ metadataLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), } @@ -171,12 +171,12 @@ func assembleEggOrWheelMetadata(resolver source.FileResolver, metadataLocation s } defer internal.CloseAndLogError(metadataContents, metadataLocation.VirtualPath) - metadata, err := parseWheelOrEggMetadata(metadataLocation.RealPath, metadataContents) + pd, err := parseWheelOrEggMetadata(metadataLocation.RealPath, metadataContents) if err != nil { return nil, nil, err } - if metadata.Name == "" { + if pd.Name == "" { return nil, nil, nil } @@ -186,14 +186,14 @@ func assembleEggOrWheelMetadata(resolver source.FileResolver, metadataLocation s return nil, nil, err } if len(r) == 0 { - r, s, err = fetchInstalledFiles(resolver, metadataLocation, metadata.SitePackagesRootPath) + r, s, err = fetchInstalledFiles(resolver, metadataLocation, pd.SitePackagesRootPath) if err != nil { return nil, nil, err } } sources = append(sources, s...) - metadata.Files = r + pd.Files = r // attach any top-level package names found for the given wheel/egg installation p, s, err := fetchTopLevelPackages(resolver, metadataLocation) @@ -201,15 +201,15 @@ func assembleEggOrWheelMetadata(resolver source.FileResolver, metadataLocation s return nil, nil, err } sources = append(sources, s...) - metadata.TopLevelPackages = p + pd.TopLevelPackages = p // attach any direct-url package data found for the given wheel/egg installation d, s, err := fetchDirectURLData(resolver, metadataLocation) if err != nil { return nil, nil, err } - sources = append(sources, s...) - metadata.DirectURLOrigin = d - return &metadata, sources, nil + sources = append(sources, s...) + pd.DirectURLOrigin = d + return &pd, sources, nil } diff --git a/syft/pkg/cataloger/python/parse_wheel_egg_metadata.go b/syft/pkg/cataloger/python/parse_wheel_egg_metadata.go index ab97a06a9..55ac924f0 100644 --- a/syft/pkg/cataloger/python/parse_wheel_egg_metadata.go +++ b/syft/pkg/cataloger/python/parse_wheel_egg_metadata.go @@ -12,11 +12,18 @@ import ( "github.com/anchore/syft/internal/file" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" ) +type parsedData struct { + Licenses string `mapstructure:"License"` + LicenseLocation source.Location + pkg.PythonPackageMetadata `mapstructure:",squash"` +} + // parseWheelOrEggMetadata takes a Python Egg or Wheel (which share the same format and values for our purposes), // returning all Python packages listed. -func parseWheelOrEggMetadata(path string, reader io.Reader) (pkg.PythonPackageMetadata, error) { +func parseWheelOrEggMetadata(path string, reader io.Reader) (parsedData, error) { fields := make(map[string]string) var key string @@ -43,7 +50,7 @@ func parseWheelOrEggMetadata(path string, reader io.Reader) (pkg.PythonPackageMe // a field-body continuation updatedValue, err := handleFieldBodyContinuation(key, line, fields) if err != nil { - return pkg.PythonPackageMetadata{}, err + return parsedData{}, err } fields[key] = updatedValue @@ -62,19 +69,22 @@ func parseWheelOrEggMetadata(path string, reader io.Reader) (pkg.PythonPackageMe } if err := scanner.Err(); err != nil { - return pkg.PythonPackageMetadata{}, fmt.Errorf("failed to parse python wheel/egg: %w", err) + return parsedData{}, fmt.Errorf("failed to parse python wheel/egg: %w", err) } - var metadata pkg.PythonPackageMetadata - if err := mapstructure.Decode(fields, &metadata); err != nil { - return pkg.PythonPackageMetadata{}, fmt.Errorf("unable to parse APK metadata: %w", err) + var pd parsedData + if err := mapstructure.Decode(fields, &pd); err != nil { + return pd, fmt.Errorf("unable to parse APK metadata: %w", err) } // add additional metadata not stored in the egg/wheel metadata file - metadata.SitePackagesRootPath = determineSitePackagesRootPath(path) + pd.SitePackagesRootPath = determineSitePackagesRootPath(path) + if pd.Licenses != "" { + pd.LicenseLocation = source.NewLocation(path) + } - return metadata, nil + return pd, nil } // isEggRegularFile determines if the specified path is the regular file variant diff --git a/syft/pkg/cataloger/python/parse_wheel_egg_metadata_test.go b/syft/pkg/cataloger/python/parse_wheel_egg_metadata_test.go index 7159962b8..cb776b669 100644 --- a/syft/pkg/cataloger/python/parse_wheel_egg_metadata_test.go +++ b/syft/pkg/cataloger/python/parse_wheel_egg_metadata_test.go @@ -7,35 +7,42 @@ import ( "github.com/go-test/deep" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" ) func TestParseWheelEggMetadata(t *testing.T) { tests := []struct { Fixture string - ExpectedMetadata pkg.PythonPackageMetadata + ExpectedMetadata parsedData }{ { Fixture: "test-fixtures/egg-info/PKG-INFO", - ExpectedMetadata: pkg.PythonPackageMetadata{ - Name: "requests", - Version: "2.22.0", - License: "Apache 2.0", - Platform: "UNKNOWN", - Author: "Kenneth Reitz", - AuthorEmail: "me@kennethreitz.org", - SitePackagesRootPath: "test-fixtures", + ExpectedMetadata: parsedData{ + "Apache 2.0", + source.NewLocation("test-fixtures/egg-info/PKG-INFO"), + pkg.PythonPackageMetadata{ + Name: "requests", + Version: "2.22.0", + Platform: "UNKNOWN", + Author: "Kenneth Reitz", + AuthorEmail: "me@kennethreitz.org", + SitePackagesRootPath: "test-fixtures", + }, }, }, { Fixture: "test-fixtures/dist-info/METADATA", - ExpectedMetadata: pkg.PythonPackageMetadata{ - Name: "Pygments", - Version: "2.6.1", - License: "BSD License", - Platform: "any", - Author: "Georg Brandl", - AuthorEmail: "georg@python.org", - SitePackagesRootPath: "test-fixtures", + ExpectedMetadata: parsedData{ + "BSD License", + source.NewLocation("test-fixtures/dist-info/METADATA"), + pkg.PythonPackageMetadata{ + Name: "Pygments", + Version: "2.6.1", + Platform: "any", + Author: "Georg Brandl", + AuthorEmail: "georg@python.org", + SitePackagesRootPath: "test-fixtures", + }, }, }, } @@ -122,14 +129,18 @@ func TestDetermineSitePackagesRootPath(t *testing.T) { func TestParseWheelEggMetadataInvalid(t *testing.T) { tests := []struct { Fixture string - ExpectedMetadata pkg.PythonPackageMetadata + ExpectedMetadata parsedData }{ { Fixture: "test-fixtures/egg-info/PKG-INFO-INVALID", - ExpectedMetadata: pkg.PythonPackageMetadata{ - Name: "mxnet", - Version: "1.8.0", - SitePackagesRootPath: "test-fixtures", + ExpectedMetadata: parsedData{ + "", + source.Location{}, + pkg.PythonPackageMetadata{ + Name: "mxnet", + Version: "1.8.0", + SitePackagesRootPath: "test-fixtures", + }, }, }, } diff --git a/syft/pkg/cataloger/r/cataloger_test.go b/syft/pkg/cataloger/r/cataloger_test.go index 82e6c2b27..1581e8dc6 100644 --- a/syft/pkg/cataloger/r/cataloger_test.go +++ b/syft/pkg/cataloger/r/cataloger_test.go @@ -16,7 +16,7 @@ func TestRPackageCataloger(t *testing.T) { Version: "4.3.0", FoundBy: "r-package-cataloger", Locations: source.NewLocationSet(source.NewLocation("base/DESCRIPTION")), - Licenses: []string{"Part of R 4.3.0"}, + Licenses: pkg.NewLicenseSet([]pkg.License{pkg.NewLicense("Part of R 4.3.0")}...), Language: pkg.R, Type: pkg.Rpkg, PURL: "pkg:cran/base@4.3.0", @@ -35,7 +35,7 @@ func TestRPackageCataloger(t *testing.T) { Version: "1.5.0.9000", FoundBy: "r-package-cataloger", Locations: source.NewLocationSet(source.NewLocation("stringr/DESCRIPTION")), - Licenses: []string{"MIT + file LICENSE"}, + Licenses: pkg.NewLicenseSet([]pkg.License{pkg.NewLicense("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 e08028317..b916cc9da 100644 --- a/syft/pkg/cataloger/r/package.go +++ b/syft/pkg/cataloger/r/package.go @@ -1,6 +1,8 @@ package r import ( + "strings" + "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" @@ -11,11 +13,14 @@ func newPackage(pd parseData, locations ...source.Location) pkg.Package { for _, loc := range locations { locationSet.Add(loc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) } + + licenses := parseLicenseData(pd.License) + result := pkg.Package{ Name: pd.Package, Version: pd.Version, Locations: locationSet, - Licenses: []string{pd.License}, + Licenses: pkg.NewLicenseSet(licenses...), Language: pkg.R, Type: pkg.Rpkg, PURL: packageURL(pd), @@ -30,3 +35,95 @@ func newPackage(pd parseData, locations ...source.Location) pkg.Package { func packageURL(m parseData) string { return packageurl.NewPackageURL("cran", "", m.Package, m.Version, nil, "").ToString() } + +// https://r-pkgs.org/description.html#the-license-field +// four forms: +// 1. "GPL (>= 2)" +// 2. "GPL-2" +// 3. "MIT + file LICENSE" +// 4. "pointer to the full text of the license; file LICENSE" +// 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 ...source.Location) []pkg.License { + licenses := make([]pkg.License, 0) + + // check if multiple licenses are separated by | + splitField := strings.Split(license, "|") + for _, l := range splitField { + // check case 1 for surrounding parens + l = strings.TrimSpace(l) + if strings.Contains(l, "(") && strings.Contains(l, ")") { + 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...)) + continue + } + } + + // case 3 + if strings.Contains(l, "+") && strings.Contains(l, "LICENSE") { + splitField := strings.Split(l, " ") + if len(splitField) > 0 { + licenses = append(licenses, pkg.NewLicenseFromLocations(splitField[0], locations...)) + continue + } + } + + // TODO: case 4 if we are able to read the location data and find the adjacent file? + if l == "file LICENSE" { + continue + } + + // 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...)) + continue + } + return licenses +} + +// attempt to make best guess at SPDX license ID from version operator in case 2 +/* +‘<’, ‘<=’, ‘>’, ‘>=’, ‘==’, or ‘!=’ +cant be (>= 2.0) OR (>= 2.0, < 3) +since there is no way in SPDX licenses to express < some other version +we attempt to check the constraint to see if this should be + or not +*/ +func parseVersion(version string) string { + version = strings.ReplaceAll(version, "(", "") + version = strings.ReplaceAll(version, ")", "") + + // multiple constraints + if strings.Contains(version, ",") { + multipleConstraints := strings.Split(version, ",") + // SPDX does not support considering multiple constraints + // so we will just take the first one and attempt to form the best SPDX ID we can + for _, v := range multipleConstraints { + constraintVersion := strings.SplitN(v, " ", 2) + if len(constraintVersion) == 2 { + // switch on the operator and return the version with + or without + switch constraintVersion[0] { + case ">", ">=": + return constraintVersion[1] + "+" + default: + return constraintVersion[1] + } + } + } + } + // single constraint + singleContraint := strings.Split(version, " ") + if len(singleContraint) == 2 { + switch singleContraint[0] { + case ">", ">=": + return singleContraint[1] + "+" + default: + return singleContraint[1] + } + } + + // could not parse version constraint so return "" + return "" +} diff --git a/syft/pkg/cataloger/r/package_test.go b/syft/pkg/cataloger/r/package_test.go index cccebf679..8eb06642c 100644 --- a/syft/pkg/cataloger/r/package_test.go +++ b/syft/pkg/cataloger/r/package_test.go @@ -1,14 +1,106 @@ package r -import "testing" +import ( + "testing" -func Test_newPackage(t *testing.T) { + "github.com/anchore/syft/syft/pkg" +) + +func Test_NewPackageLicenses(t *testing.T) { testCases := []struct { name string - }{} + pd parseData + want []pkg.License + }{ + { + "License field with single valid spdx", + parseData{ + Package: "Foo", + Version: "1", + License: "MIT", + }, + []pkg.License{ + pkg.NewLicense("MIT"), + }, + }, + { + "License field with single version separator no +", + parseData{ + Package: "Bar", + Version: "2", + License: "LGPL (== 2.0)", + }, + []pkg.License{ + pkg.NewLicense("LGPL2.0"), + }, + }, + { + "License field with multiple version separator", + parseData{ + Package: "Bar", + Version: "2", + License: "LGPL (>= 2.0, < 3)", + }, + []pkg.License{ + pkg.NewLicense("LGPL2.0+"), + }, + }, + { + "License field with file reference", + parseData{ + Package: "Baz", + Version: "3", + License: "GPL-2 + file LICENSE", + }, + []pkg.License{ + pkg.NewLicense("GPL-2"), + }, + }, + { + "License field which covers no case", + parseData{ + Package: "Baz", + Version: "3", + License: "Mozilla Public License", + }, + []pkg.License{ + pkg.NewLicense("Mozilla Public License"), + }, + }, + { + "License field with multiple cases", + parseData{ + Package: "Baz", + Version: "3", + License: "GPL-2 | file LICENSE | LGPL (>= 2.0)", + }, + []pkg.License{ + pkg.NewLicense("GPL-2"), + pkg.NewLicense("LGPL2.0+"), + }, + }, + } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { + got := parseLicenseData(tt.pd.License) + if len(got) != len(tt.want) { + t.Errorf("unexpected number of licenses: got=%d, want=%d", len(got), len(tt.want)) + } + + for _, wantLicense := range tt.want { + found := false + for _, gotLicense := range got { + if wantLicense.Type == gotLicense.Type && + wantLicense.SPDXExpression == gotLicense.SPDXExpression && + wantLicense.Value == gotLicense.Value { + found = true + } + } + if !found { + t.Errorf("could not find expected license: %+v; got: %+v", wantLicense, got) + } + } }) } } diff --git a/syft/pkg/cataloger/rpm/package.go b/syft/pkg/cataloger/rpm/package.go index 77e5e14be..53c0925b9 100644 --- a/syft/pkg/cataloger/rpm/package.go +++ b/syft/pkg/cataloger/rpm/package.go @@ -13,42 +13,46 @@ import ( "github.com/anchore/syft/syft/source" ) -func newPackage(location source.Location, metadata pkg.RpmMetadata, distro *linux.Release) pkg.Package { +func newPackage(dbOrRpmLocation source.Location, pd parsedData, distro *linux.Release) pkg.Package { p := pkg.Package{ - Name: metadata.Name, - Version: toELVersion(metadata), - PURL: packageURL(metadata, distro), - Locations: source.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), + Name: pd.Name, + Version: toELVersion(pd.RpmMetadata), + Licenses: pkg.NewLicenseSet(pd.Licenses...), + PURL: packageURL(pd.RpmMetadata, distro), + Locations: source.NewLocationSet(dbOrRpmLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), Type: pkg.RpmPkg, MetadataType: pkg.RpmMetadataType, - Metadata: metadata, - } - - if metadata.License != "" { - p.Licenses = append(p.Licenses, metadata.License) + Metadata: pd.RpmMetadata, } p.SetID() return p } -func newMetadataFromEntry(entry rpmdb.PackageInfo, files []pkg.RpmdbFileRecord) pkg.RpmMetadata { - return pkg.RpmMetadata{ - Name: entry.Name, - Version: entry.Version, - Epoch: entry.Epoch, - Arch: entry.Arch, - Release: entry.Release, - SourceRpm: entry.SourceRpm, - Vendor: entry.Vendor, - License: entry.License, - Size: entry.Size, - ModularityLabel: entry.Modularitylabel, - Files: files, +type parsedData struct { + Licenses []pkg.License + pkg.RpmMetadata +} + +func newParsedDataFromEntry(licenseLocation source.Location, entry rpmdb.PackageInfo, files []pkg.RpmdbFileRecord) parsedData { + return parsedData{ + Licenses: pkg.NewLicensesFromLocation(licenseLocation, entry.License), + RpmMetadata: pkg.RpmMetadata{ + Name: entry.Name, + Version: entry.Version, + Epoch: entry.Epoch, + Arch: entry.Arch, + Release: entry.Release, + SourceRpm: entry.SourceRpm, + Vendor: entry.Vendor, + Size: entry.Size, + ModularityLabel: entry.Modularitylabel, + Files: files, + }, } } -func newMetadataFromManifestLine(entry string) (*pkg.RpmMetadata, error) { +func newMetadataFromManifestLine(entry string) (*parsedData, error) { parts := strings.Split(entry, "\t") if len(parts) < 10 { return nil, fmt.Errorf("unexpected number of fields in line: %s", entry) @@ -74,16 +78,17 @@ func newMetadataFromManifestLine(entry string) (*pkg.RpmMetadata, error) { if err == nil { size = converted } - - return &pkg.RpmMetadata{ - Name: parts[0], - Version: version, - Epoch: epoch, - Arch: parts[7], - Release: release, - SourceRpm: parts[9], - Vendor: parts[4], - Size: size, + return &parsedData{ + RpmMetadata: pkg.RpmMetadata{ + Name: parts[0], + Version: version, + Epoch: epoch, + Arch: parts[7], + Release: release, + SourceRpm: parts[9], + Vendor: parts[4], + Size: size, + }, }, nil } diff --git a/syft/pkg/cataloger/rpm/parse_rpm.go b/syft/pkg/cataloger/rpm/parse_rpm.go index 687735faf..6e866c5ca 100644 --- a/syft/pkg/cataloger/rpm/parse_rpm.go +++ b/syft/pkg/cataloger/rpm/parse_rpm.go @@ -3,7 +3,6 @@ package rpm import ( "fmt" "strconv" - "strings" rpmdb "github.com/knqyf263/go-rpmdb/pkg" "github.com/sassoftware/go-rpmutils" @@ -34,20 +33,22 @@ func parseRpm(_ source.FileResolver, _ *generic.Environment, reader source.Locat size, _ := rpm.Header.InstalledSize() files, _ := rpm.Header.GetFiles() - metadata := pkg.RpmMetadata{ - Name: nevra.Name, - Version: nevra.Version, - Epoch: parseEpoch(nevra.Epoch), - Arch: nevra.Arch, - Release: nevra.Release, - SourceRpm: sourceRpm, - Vendor: vendor, - License: strings.Join(licenses, " AND "), // TODO: AND conjunction is not necessarily correct, but we don't have a way to represent multiple licenses yet - Size: int(size), - Files: mapFiles(files, digestAlgorithm), + pd := parsedData{ + Licenses: pkg.NewLicensesFromLocation(reader.Location, licenses...), + RpmMetadata: pkg.RpmMetadata{ + Name: nevra.Name, + Version: nevra.Version, + Epoch: parseEpoch(nevra.Epoch), + Arch: nevra.Arch, + Release: nevra.Release, + SourceRpm: sourceRpm, + Vendor: vendor, + Size: int(size), + Files: mapFiles(files, digestAlgorithm), + }, } - return []pkg.Package{newPackage(reader.Location, metadata, nil)}, nil, nil + return []pkg.Package{newPackage(reader.Location, pd, nil)}, nil, nil } func getDigestAlgorithm(header *rpmutils.RpmHeader) string { diff --git a/syft/pkg/cataloger/rpm/parse_rpm_db.go b/syft/pkg/cataloger/rpm/parse_rpm_db.go index 58e96e9cb..ee4d64b4f 100644 --- a/syft/pkg/cataloger/rpm/parse_rpm_db.go +++ b/syft/pkg/cataloger/rpm/parse_rpm_db.go @@ -60,7 +60,7 @@ func parseRpmDB(resolver source.FileResolver, env *generic.Environment, reader s p := newPackage( reader.Location, - newMetadataFromEntry(*entry, extractRpmdbFileRecords(resolver, *entry)), + newParsedDataFromEntry(reader.Location, *entry, extractRpmdbFileRecords(resolver, *entry)), distro, ) diff --git a/syft/pkg/cataloger/rpm/parse_rpm_db_test.go b/syft/pkg/cataloger/rpm/parse_rpm_db_test.go index 111195fd3..b58a01744 100644 --- a/syft/pkg/cataloger/rpm/parse_rpm_db_test.go +++ b/syft/pkg/cataloger/rpm/parse_rpm_db_test.go @@ -80,6 +80,7 @@ func (r *rpmdbTestFileResolverMock) FilesByMIMEType(...string) ([]source.Locatio } func TestParseRpmDB(t *testing.T) { + packagesLocation := source.NewLocation("test-fixtures/Packages") tests := []struct { fixture string expected []pkg.Package @@ -94,10 +95,12 @@ func TestParseRpmDB(t *testing.T) { Name: "dive", Version: "0.9.2-1", PURL: "pkg:rpm/dive@0.9.2-1?arch=x86_64&upstream=dive-0.9.2-1.src.rpm", - Locations: source.NewLocationSet(source.NewLocation("test-fixtures/Packages")), + Locations: source.NewLocationSet(packagesLocation), Type: pkg.RpmPkg, MetadataType: pkg.RpmMetadataType, - Licenses: []string{"MIT"}, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", packagesLocation), + ), Metadata: pkg.RpmMetadata{ Name: "dive", Epoch: nil, @@ -106,7 +109,6 @@ func TestParseRpmDB(t *testing.T) { Version: "0.9.2", SourceRpm: "dive-0.9.2-1.src.rpm", Size: 12406784, - License: "MIT", Vendor: "", Files: []pkg.RpmdbFileRecord{}, }, @@ -122,10 +124,12 @@ func TestParseRpmDB(t *testing.T) { Name: "dive", Version: "0.9.2-1", PURL: "pkg:rpm/dive@0.9.2-1?arch=x86_64&upstream=dive-0.9.2-1.src.rpm", - Locations: source.NewLocationSet(source.NewLocation("test-fixtures/Packages")), + Locations: source.NewLocationSet(packagesLocation), Type: pkg.RpmPkg, MetadataType: pkg.RpmMetadataType, - Licenses: []string{"MIT"}, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", packagesLocation), + ), Metadata: pkg.RpmMetadata{ Name: "dive", Epoch: nil, @@ -134,7 +138,6 @@ func TestParseRpmDB(t *testing.T) { Version: "0.9.2", SourceRpm: "dive-0.9.2-1.src.rpm", Size: 12406784, - License: "MIT", Vendor: "", Files: []pkg.RpmdbFileRecord{ { @@ -163,7 +166,6 @@ func TestParseRpmDB(t *testing.T) { TestParser(t, parseRpmDB) }) } - } func TestToElVersion(t *testing.T) { diff --git a/syft/pkg/cataloger/rpm/parse_rpm_test.go b/syft/pkg/cataloger/rpm/parse_rpm_test.go index 1d082e693..253d99f59 100644 --- a/syft/pkg/cataloger/rpm/parse_rpm_test.go +++ b/syft/pkg/cataloger/rpm/parse_rpm_test.go @@ -10,6 +10,8 @@ import ( ) func TestParseRpmFiles(t *testing.T) { + abcRpmLocation := source.NewLocation("abc-1.01-9.hg20160905.el7.x86_64.rpm") + zorkRpmLocation := source.NewLocation("zork-1.0.3-1.el7.x86_64.rpm") tests := []struct { fixture string expected []pkg.Package @@ -21,11 +23,13 @@ func TestParseRpmFiles(t *testing.T) { Name: "abc", Version: "0:1.01-9.hg20160905.el7", PURL: "pkg:rpm/abc@1.01-9.hg20160905.el7?arch=x86_64&epoch=0&upstream=abc-1.01-9.hg20160905.el7.src.rpm", - Locations: source.NewLocationSet(source.NewLocation("abc-1.01-9.hg20160905.el7.x86_64.rpm")), + Locations: source.NewLocationSet(abcRpmLocation), FoundBy: "rpm-file-cataloger", Type: pkg.RpmPkg, MetadataType: pkg.RpmMetadataType, - Licenses: []string{"MIT"}, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", abcRpmLocation), + ), Metadata: pkg.RpmMetadata{ Name: "abc", Epoch: intRef(0), @@ -34,7 +38,6 @@ func TestParseRpmFiles(t *testing.T) { Version: "1.01", SourceRpm: "abc-1.01-9.hg20160905.el7.src.rpm", Size: 17396, - License: "MIT", Vendor: "Fedora Project", Files: []pkg.RpmdbFileRecord{ {"/usr/bin/abc", 33261, 7120, file.Digest{"sha256", "8f8495a65c66762b60afa0c3949d81b275ca6fa0601696caba5af762f455d0b9"}, "root", "root", ""}, @@ -49,11 +52,13 @@ func TestParseRpmFiles(t *testing.T) { Name: "zork", Version: "0:1.0.3-1.el7", PURL: "pkg:rpm/zork@1.0.3-1.el7?arch=x86_64&epoch=0&upstream=zork-1.0.3-1.el7.src.rpm", - Locations: source.NewLocationSet(source.NewLocation("zork-1.0.3-1.el7.x86_64.rpm")), + Locations: source.NewLocationSet(zorkRpmLocation), FoundBy: "rpm-file-cataloger", Type: pkg.RpmPkg, MetadataType: pkg.RpmMetadataType, - Licenses: []string{"Public Domain"}, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("Public Domain", zorkRpmLocation), + ), Metadata: pkg.RpmMetadata{ Name: "zork", Epoch: intRef(0), @@ -62,7 +67,6 @@ func TestParseRpmFiles(t *testing.T) { Version: "1.0.3", SourceRpm: "zork-1.0.3-1.el7.src.rpm", Size: 262367, - License: "Public Domain", Vendor: "Fedora Project", Files: []pkg.RpmdbFileRecord{ {"/usr/bin/zork", 33261, 115440, file.Digest{"sha256", "31b2ffc20b676a8fff795a45308f584273b9c47e8f7e196b4f36220b2734b472"}, "root", "root", ""}, diff --git a/syft/pkg/cataloger/ruby/package.go b/syft/pkg/cataloger/ruby/package.go index 408653c25..973d67350 100644 --- a/syft/pkg/cataloger/ruby/package.go +++ b/syft/pkg/cataloger/ruby/package.go @@ -21,17 +21,17 @@ func newGemfileLockPackage(name, version string, locations ...source.Location) p return p } -func newGemspecPackage(m pkg.GemMetadata, locations ...source.Location) pkg.Package { +func newGemspecPackage(m gemData, gemSpecLocation source.Location) pkg.Package { p := pkg.Package{ Name: m.Name, Version: m.Version, - Locations: source.NewLocationSet(locations...), + Locations: source.NewLocationSet(gemSpecLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(gemSpecLocation, m.Licenses...)...), PURL: packageURL(m.Name, m.Version), - Licenses: m.Licenses, Language: pkg.Ruby, Type: pkg.GemPkg, MetadataType: pkg.GemMetadataType, - Metadata: m, + Metadata: m.GemMetadata, } p.SetID() diff --git a/syft/pkg/cataloger/ruby/parse_gemspec.go b/syft/pkg/cataloger/ruby/parse_gemspec.go index 6bb11a809..347caabbd 100644 --- a/syft/pkg/cataloger/ruby/parse_gemspec.go +++ b/syft/pkg/cataloger/ruby/parse_gemspec.go @@ -20,6 +20,11 @@ var _ generic.Parser = parseGemFileLockEntries type postProcessor func(string) []string +type gemData struct { + Licenses []string `mapstructure:"licenses" json:"licenses,omitempty"` + pkg.GemMetadata `mapstructure:",squash" json:",inline"` +} + // match example: Al\u003Ex ---> 003E var unicodePattern = regexp.MustCompile(`\\u(?P[0-9A-F]{4})`) @@ -90,14 +95,17 @@ func parseGemSpecEntries(_ source.FileResolver, _ *generic.Environment, reader s } if fields["name"] != "" && fields["version"] != "" { - var metadata pkg.GemMetadata + var metadata gemData if err := mapstructure.Decode(fields, &metadata); err != nil { return nil, nil, fmt.Errorf("unable to decode gem metadata: %w", err) } pkgs = append( pkgs, - newGemspecPackage(metadata, reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), + newGemspecPackage( + metadata, + reader.Location, + ), ) } diff --git a/syft/pkg/cataloger/ruby/parse_gemspec_test.go b/syft/pkg/cataloger/ruby/parse_gemspec_test.go index 6b378ce35..53cb59ecf 100644 --- a/syft/pkg/cataloger/ruby/parse_gemspec_test.go +++ b/syft/pkg/cataloger/ruby/parse_gemspec_test.go @@ -14,12 +14,14 @@ func TestParseGemspec(t *testing.T) { locations := source.NewLocationSet(source.NewLocation(fixture)) var expectedPkg = pkg.Package{ - Name: "bundler", - Version: "2.1.4", - PURL: "pkg:gem/bundler@2.1.4", - Locations: locations, - Type: pkg.GemPkg, - Licenses: []string{"MIT"}, + Name: "bundler", + Version: "2.1.4", + PURL: "pkg:gem/bundler@2.1.4", + Locations: locations, + Type: pkg.GemPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)), + ), Language: pkg.Ruby, MetadataType: pkg.GemMetadataType, Metadata: pkg.GemMetadata{ @@ -27,7 +29,6 @@ func TestParseGemspec(t *testing.T) { Version: "2.1.4", Files: []string{"exe/bundle", "exe/bundler"}, Authors: []string{"André Arko", "Samuel Giddins", "Colby Swandale", "Hiroshi Shibata", "David Rodríguez", "Grey Baker", "Stephanie Morillo", "Chris Morris", "James Wen", "Tim Moore", "André Medeiros", "Jessica Lynn Suttles", "Terence Lee", "Carl Lerche", "Yehuda Katz"}, - Licenses: []string{"MIT"}, Homepage: "https://bundler.io", }, } diff --git a/syft/pkg/cataloger/rust/parse_cargo_lock_test.go b/syft/pkg/cataloger/rust/parse_cargo_lock_test.go index 25fba560e..d05f62d62 100644 --- a/syft/pkg/cataloger/rust/parse_cargo_lock_test.go +++ b/syft/pkg/cataloger/rust/parse_cargo_lock_test.go @@ -21,7 +21,7 @@ func TestParseCargoLock(t *testing.T) { Language: pkg.Rust, Type: pkg.RustPkg, MetadataType: pkg.RustCargoPackageMetadataType, - Licenses: nil, + Licenses: pkg.NewLicenseSet(), Metadata: pkg.CargoPackageMetadata{ Name: "ansi_term", Version: "0.12.1", @@ -40,7 +40,7 @@ func TestParseCargoLock(t *testing.T) { Language: pkg.Rust, Type: pkg.RustPkg, MetadataType: pkg.RustCargoPackageMetadataType, - Licenses: nil, + Licenses: pkg.NewLicenseSet(), Metadata: pkg.CargoPackageMetadata{ Name: "matches", Version: "0.1.8", @@ -57,7 +57,7 @@ func TestParseCargoLock(t *testing.T) { Language: pkg.Rust, Type: pkg.RustPkg, MetadataType: pkg.RustCargoPackageMetadataType, - Licenses: nil, + Licenses: pkg.NewLicenseSet(), Metadata: pkg.CargoPackageMetadata{ Name: "memchr", Version: "2.3.3", @@ -74,7 +74,7 @@ func TestParseCargoLock(t *testing.T) { Language: pkg.Rust, Type: pkg.RustPkg, MetadataType: pkg.RustCargoPackageMetadataType, - Licenses: nil, + Licenses: pkg.NewLicenseSet(), Metadata: pkg.CargoPackageMetadata{ Name: "natord", Version: "1.0.9", @@ -91,7 +91,7 @@ func TestParseCargoLock(t *testing.T) { Language: pkg.Rust, Type: pkg.RustPkg, MetadataType: pkg.RustCargoPackageMetadataType, - Licenses: nil, + Licenses: pkg.NewLicenseSet(), Metadata: pkg.CargoPackageMetadata{ Name: "nom", Version: "4.2.3", @@ -111,7 +111,7 @@ func TestParseCargoLock(t *testing.T) { Language: pkg.Rust, Type: pkg.RustPkg, MetadataType: pkg.RustCargoPackageMetadataType, - Licenses: nil, + Licenses: pkg.NewLicenseSet(), Metadata: pkg.CargoPackageMetadata{ Name: "unicode-bidi", Version: "0.3.4", @@ -130,7 +130,7 @@ func TestParseCargoLock(t *testing.T) { Language: pkg.Rust, Type: pkg.RustPkg, MetadataType: pkg.RustCargoPackageMetadataType, - Licenses: nil, + Licenses: pkg.NewLicenseSet(), Metadata: pkg.CargoPackageMetadata{ Name: "version_check", Version: "0.1.5", @@ -147,7 +147,7 @@ func TestParseCargoLock(t *testing.T) { Language: pkg.Rust, Type: pkg.RustPkg, MetadataType: pkg.RustCargoPackageMetadataType, - Licenses: nil, + Licenses: pkg.NewLicenseSet(), Metadata: pkg.CargoPackageMetadata{ Name: "winapi", Version: "0.3.9", @@ -167,7 +167,7 @@ func TestParseCargoLock(t *testing.T) { Language: pkg.Rust, Type: pkg.RustPkg, MetadataType: pkg.RustCargoPackageMetadataType, - Licenses: nil, + Licenses: pkg.NewLicenseSet(), Metadata: pkg.CargoPackageMetadata{ Name: "winapi-i686-pc-windows-gnu", Version: "0.4.0", @@ -184,7 +184,7 @@ func TestParseCargoLock(t *testing.T) { Language: pkg.Rust, Type: pkg.RustPkg, MetadataType: pkg.RustCargoPackageMetadataType, - Licenses: nil, + Licenses: pkg.NewLicenseSet(), Metadata: pkg.CargoPackageMetadata{ Name: "winapi-x86_64-pc-windows-gnu", Version: "0.4.0", diff --git a/syft/pkg/cataloger/sbom/cataloger_test.go b/syft/pkg/cataloger/sbom/cataloger_test.go index f7459d5d5..a2226f80e 100644 --- a/syft/pkg/cataloger/sbom/cataloger_test.go +++ b/syft/pkg/cataloger/sbom/cataloger_test.go @@ -32,14 +32,13 @@ func must(c cpe.CPE, e error) cpe.CPE { } func Test_parseSBOM(t *testing.T) { - expectedPkgs := []pkg.Package{ { Name: "alpine-baselayout", Version: "3.2.0-r23", Type: "apk", Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")), - Licenses: []string{"GPL-2.0-only"}, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("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( @@ -56,7 +55,7 @@ func Test_parseSBOM(t *testing.T) { Version: "3.2.0-r23", Type: "apk", Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")), - Licenses: []string{"GPL-2.0-only"}, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("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( @@ -77,7 +76,7 @@ func Test_parseSBOM(t *testing.T) { Version: "2.4-r1", Type: "apk", Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")), - Licenses: []string{"MIT"}, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("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( @@ -94,7 +93,7 @@ func Test_parseSBOM(t *testing.T) { Version: "2.12.9-r3", Type: "apk", Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")), - Licenses: []string{"GPL-2.0-only"}, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("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( @@ -111,7 +110,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.35.0-r17", Type: "apk", Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")), - Licenses: []string{"GPL-2.0-only"}, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("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( @@ -123,9 +122,12 @@ func Test_parseSBOM(t *testing.T) { Version: "20220614-r0", Type: "apk", Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")), - Licenses: []string{"MPL-2.0", "AND", "MIT"}, - FoundBy: "sbom-cataloger", - PURL: "pkg:apk/alpine/ca-certificates-bundle@20220614-r0?arch=x86_64&upstream=ca-certificates&distro=alpine-3.16.3", + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("MPL-2.0"), + pkg.NewLicense("MIT"), + ), + FoundBy: "sbom-cataloger", + PURL: "pkg:apk/alpine/ca-certificates-bundle@20220614-r0?arch=x86_64&upstream=ca-certificates&distro=alpine-3.16.3", CPEs: mustCPEs( "cpe:2.3:a:ca-certificates-bundle:ca-certificates-bundle:20220614-r0:*:*:*:*:*:*:*", "cpe:2.3:a:ca-certificates-bundle:ca_certificates_bundle:20220614-r0:*:*:*:*:*:*:*", @@ -144,9 +146,12 @@ func Test_parseSBOM(t *testing.T) { Version: "0.7.2-r3", Type: "apk", Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")), - Licenses: []string{"BSD-2-Clause", "AND", "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", + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("BSD-2-Clause"), + pkg.NewLicense("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", CPEs: mustCPEs( "cpe:2.3:a:libc-utils:libc-utils:0.7.2-r3:*:*:*:*:*:*:*", "cpe:2.3:a:libc-utils:libc_utils:0.7.2-r3:*:*:*:*:*:*:*", @@ -161,7 +166,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.1.1s-r0", Type: "apk", Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")), - Licenses: []string{"OpenSSL"}, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("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( @@ -173,7 +178,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.1.1s-r0", Type: "apk", Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")), - Licenses: []string{"OpenSSL"}, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("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( @@ -185,7 +190,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.2.3-r1", Type: "apk", Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")), - Licenses: []string{"MIT"}, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("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( @@ -197,9 +202,13 @@ func Test_parseSBOM(t *testing.T) { Version: "1.2.3-r1", Type: "apk", Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")), - Licenses: []string{"MIT", "BSD", "GPL2+"}, - FoundBy: "sbom-cataloger", - PURL: "pkg:apk/alpine/musl-utils@1.2.3-r1?arch=x86_64&upstream=musl&distro=alpine-3.16.3", + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("MIT"), + pkg.NewLicense("BSD"), + pkg.NewLicense("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", CPEs: mustCPEs( "cpe:2.3:a:musl-utils:musl-utils:1.2.3-r1:*:*:*:*:*:*:*", "cpe:2.3:a:musl-utils:musl_utils:1.2.3-r1:*:*:*:*:*:*:*", @@ -214,9 +223,11 @@ func Test_parseSBOM(t *testing.T) { Version: "1.3.4-r0", Type: "apk", Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")), - Licenses: []string{"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", + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("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", CPEs: mustCPEs( "cpe:2.3:a:scanelf:scanelf:1.3.4-r0:*:*:*:*:*:*:*", ), @@ -226,9 +237,11 @@ func Test_parseSBOM(t *testing.T) { Version: "1.35.0-r17", Type: "apk", Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")), - Licenses: []string{"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", + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("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", CPEs: mustCPEs( "cpe:2.3:a:ssl-client:ssl-client:1.35.0-r17:*:*:*:*:*:*:*", "cpe:2.3:a:ssl-client:ssl_client:1.35.0-r17:*:*:*:*:*:*:*", @@ -243,9 +256,11 @@ func Test_parseSBOM(t *testing.T) { Version: "1.2.12-r3", Type: "apk", Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")), - Licenses: []string{"Zlib"}, - FoundBy: "sbom-cataloger", - PURL: "pkg:apk/alpine/zlib@1.2.12-r3?arch=x86_64&upstream=zlib&distro=alpine-3.16.3", + Licenses: pkg.NewLicenseSet( + pkg.NewLicense("Zlib"), + ), + FoundBy: "sbom-cataloger", + PURL: "pkg:apk/alpine/zlib@1.2.12-r3?arch=x86_64&upstream=zlib&distro=alpine-3.16.3", CPEs: mustCPEs( "cpe:2.3:a:zlib:zlib:1.2.12-r3:*:*:*:*:*:*:*", ), @@ -266,7 +281,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.1.1s-r0", Type: "apk", Locations: apkgdbLocation, - Licenses: []string{"OpenSSL"}, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("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( @@ -279,7 +294,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.35.0-r17", Type: "apk", Locations: apkgdbLocation, - Licenses: []string{"GPL-2.0-only"}, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("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( @@ -297,7 +312,7 @@ func Test_parseSBOM(t *testing.T) { Version: "3.2.0-r23", Type: "apk", Locations: apkgdbLocation, - Licenses: []string{"GPL-2.0-only"}, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("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( @@ -315,7 +330,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.35.0-r17", Type: "apk", Locations: apkgdbLocation, - Licenses: []string{"GPL-2.0-only"}, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("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( @@ -328,7 +343,7 @@ func Test_parseSBOM(t *testing.T) { Version: "1.2.3-r1", Type: "apk", Locations: apkgdbLocation, - Licenses: []string{"MIT"}, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("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/sbom/test-fixtures/alpine/syft-json/sbom.syft.json b/syft/pkg/cataloger/sbom/test-fixtures/alpine/syft-json/sbom.syft.json index 41d0600c0..9bd9e991f 100644 --- a/syft/pkg/cataloger/sbom/test-fixtures/alpine/syft-json/sbom.syft.json +++ b/syft/pkg/cataloger/sbom/test-fixtures/alpine/syft-json/sbom.syft.json @@ -1278,7 +1278,6 @@ ], "licenses": [ "MPL-2.0", - "AND", "MIT" ], "language": "", @@ -1357,7 +1356,6 @@ ], "licenses": [ "BSD-2-Clause", - "AND", "BSD-3-Clause" ], "language": "", diff --git a/syft/pkg/gem_metadata.go b/syft/pkg/gem_metadata.go index 2ddeb001c..01b05eec4 100644 --- a/syft/pkg/gem_metadata.go +++ b/syft/pkg/gem_metadata.go @@ -21,6 +21,5 @@ type GemMetadata struct { // ... we can't reliably determine the full path to the file on disk, thus cannot implement FileOwner (...yet...). Files []string `mapstructure:"files" json:"files,omitempty"` Authors []string `mapstructure:"authors" json:"authors,omitempty"` - Licenses []string `mapstructure:"licenses" json:"licenses,omitempty"` Homepage string `mapstructure:"homepage" json:"homepage,omitempty"` } diff --git a/syft/pkg/license.go b/syft/pkg/license.go new file mode 100644 index 000000000..e5cc57910 --- /dev/null +++ b/syft/pkg/license.go @@ -0,0 +1,151 @@ +package pkg + +import ( + "fmt" + "sort" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/license" + "github.com/anchore/syft/syft/source" +) + +var _ sort.Interface = (*Licenses)(nil) + +// License represents an SPDX Expression or license value extracted from a packages metadata +// We want to ignore URL 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 +// of where it was found and how it was sourced. +// 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 +type License struct { + Value string `json:"value"` + SPDXExpression string `json:"spdxExpression"` + Type license.Type `json:"type"` + URL internal.StringSet `hash:"ignore"` + Location source.LocationSet `hash:"ignore"` +} + +type Licenses []License + +func (l Licenses) Len() int { + return len(l) +} + +func (l Licenses) Less(i, j int) bool { + if l[i].Value == l[j].Value { + if l[i].SPDXExpression == l[j].SPDXExpression { + if l[i].Type == l[j].Type { + // While URL and location are not exclusive fields + // 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 + return true + } + return l[i].Type < l[j].Type + } + return l[i].SPDXExpression < l[j].SPDXExpression + } + return l[i].Value < l[j].Value +} + +func (l Licenses) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} + +func NewLicense(value string) License { + spdxExpression, err := license.ParseExpression(value) + if err != nil { + log.Trace("unable to parse license expression: %w", err) + } + + return License{ + Value: value, + SPDXExpression: spdxExpression, + Type: license.Declared, + URL: internal.NewStringSet(), + Location: source.NewLocationSet(), + } +} + +func NewLicenseFromType(value string, t license.Type) License { + spdxExpression, err := license.ParseExpression(value) + if err != nil { + log.Trace("unable to parse license expression: %w", err) + } + + return License{ + Value: value, + SPDXExpression: spdxExpression, + Type: t, + URL: internal.NewStringSet(), + Location: source.NewLocationSet(), + } +} + +func NewLicensesFromValues(values ...string) (licenses []License) { + for _, v := range values { + licenses = append(licenses, NewLicense(v)) + } + return +} + +func NewLicensesFromLocation(location source.Location, values ...string) (licenses []License) { + for _, v := range values { + if v == "" { + continue + } + licenses = append(licenses, NewLicenseFromLocations(v, location)) + } + return +} + +func NewLicenseFromLocations(value string, locations ...source.Location) License { + l := NewLicense(value) + for _, loc := range locations { + l.Location.Add(loc) + } + return l +} + +func NewLicenseFromURLs(value string, urls ...string) License { + l := NewLicense(value) + for _, u := range urls { + if u != "" { + l.URL.Add(u) + } + } + return l +} + +// this is a bit of a hack to not infinitely recurse when hashing a license +func (s License) Merge(l License) (*License, error) { + sHash, err := artifact.IDByHash(s) + if err != nil { + return nil, err + } + lHash, err := artifact.IDByHash(l) + if err != nil { + return nil, err + } + if err != nil { + return nil, err + } + if sHash != lHash { + return nil, fmt.Errorf("cannot merge licenses with different hash") + } + + s.URL.Add(l.URL.ToSlice()...) + if s.Location.Empty() && l.Location.Empty() { + return &s, nil + } + + s.Location.Add(l.Location.ToSlice()...) + return &s, nil +} diff --git a/syft/pkg/license_set.go b/syft/pkg/license_set.go new file mode 100644 index 000000000..d404288a0 --- /dev/null +++ b/syft/pkg/license_set.go @@ -0,0 +1,88 @@ +package pkg + +import ( + "fmt" + "sort" + + "github.com/mitchellh/hashstructure/v2" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" +) + +type LicenseSet struct { + set map[artifact.ID]License +} + +func NewLicenseSet(licenses ...License) (s LicenseSet) { + for _, l := range licenses { + s.Add(l) + } + + return s +} + +func (s *LicenseSet) addToExisting(license License) (id artifact.ID, merged bool, err error) { + id, err = artifact.IDByHash(license) + if err != nil { + return id, false, fmt.Errorf("could not get the hash for a license: %w", err) + } + + v, ok := s.set[id] + if !ok { + // doesn't exist safe to add + return id, false, nil + } + + // we got the same id; we want to merge the URL and Location data + // URL/Location are not considered when taking the Hash + m, err := v.Merge(license) + if err != nil { + return id, false, fmt.Errorf("could not merge license into map: %w", err) + } + s.set[id] = *m + + return id, true, nil +} + +func (s *LicenseSet) Add(licenses ...License) { + if s.set == nil { + s.set = make(map[artifact.ID]License) + } + for _, l := range licenses { + // we only want to add licenses that have a value + // note, this check should be moved to the license constructor in the future + if l.Value != "" { + if id, merged, err := s.addToExisting(l); err == nil && !merged { + // doesn't exist, add it + s.set[id] = l + } else if err != nil { + log.Trace("license set failed to add license %#v: %+v", l, err) + } + } + } +} + +func (s LicenseSet) ToSlice() []License { + if s.set == nil { + return nil + } + var licenses []License + for _, v := range s.set { + licenses = append(licenses, v) + } + sort.Sort(Licenses(licenses)) + return licenses +} + +func (s LicenseSet) Hash() (uint64, error) { + // access paths and filesystem IDs are not considered when hashing a license set, only the real paths + return hashstructure.Hash(s.ToSlice(), hashstructure.FormatV2, &hashstructure.HashOptions{ + ZeroNil: true, + SlicesAsSets: true, + }) +} + +func (s LicenseSet) Empty() bool { + return len(s.set) < 1 +} diff --git a/syft/pkg/license_set_test.go b/syft/pkg/license_set_test.go new file mode 100644 index 000000000..c6039b2c2 --- /dev/null +++ b/syft/pkg/license_set_test.go @@ -0,0 +1,139 @@ +package pkg + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft/license" + "github.com/anchore/syft/syft/source" +) + +func TestLicenseSet_Add(t *testing.T) { + tests := []struct { + name string + licenses []License + want []License + }{ + { + name: "add one simple license", + licenses: []License{ + NewLicense("MIT"), + }, + want: []License{ + NewLicense("MIT"), + }, + }, + { + name: "add multiple simple licenses", + licenses: []License{ + NewLicense("MIT"), + NewLicense("MIT"), + NewLicense("Apache-2.0"), + }, + want: []License{ + NewLicense("Apache-2.0"), + NewLicense("MIT"), + }, + }, + { + name: "attempt to add a license with no name", + licenses: []License{ + NewLicense(""), + }, + want: nil, + }, + { + name: "keep multiple licenses sorted", + licenses: []License{ + NewLicense("MIT"), + NewLicense("Apache-2.0"), + }, + want: []License{ + NewLicense("Apache-2.0"), + NewLicense("MIT"), + }, + }, + { + name: "deduplicate licenses with locations", + licenses: []License{ + NewLicenseFromLocations("MIT", source.NewLocationFromCoordinates(source.Coordinates{RealPath: "/place", FileSystemID: "1"})), + NewLicenseFromLocations("MIT", source.NewLocationFromCoordinates(source.Coordinates{RealPath: "/place", FileSystemID: "1"})), + NewLicenseFromLocations("MIT", source.NewLocationFromCoordinates(source.Coordinates{RealPath: "/place", FileSystemID: "2"})), + }, + want: []License{ + NewLicenseFromLocations( + "MIT", + source.NewLocationFromCoordinates(source.Coordinates{RealPath: "/place", FileSystemID: "1"}), + source.NewLocationFromCoordinates(source.Coordinates{RealPath: "/place", FileSystemID: "2"}), + ), + }, + }, + { + name: "same licenses with different locations", + licenses: []License{ + NewLicense("MIT"), + NewLicenseFromLocations("MIT", source.NewLocationFromCoordinates(source.Coordinates{RealPath: "/place", FileSystemID: "2"})), + NewLicenseFromLocations("MIT", source.NewLocationFromCoordinates(source.Coordinates{RealPath: "/place", FileSystemID: "1"})), + }, + want: []License{ + NewLicenseFromLocations( + "MIT", + source.NewLocationFromCoordinates(source.Coordinates{RealPath: "/place", FileSystemID: "1"}), + source.NewLocationFromCoordinates(source.Coordinates{RealPath: "/place", FileSystemID: "2"}), + ), + }, + }, + { + name: "same license from different sources", + licenses: []License{ + NewLicense("MIT"), + NewLicenseFromLocations("MIT", source.NewLocation("/place")), + NewLicenseFromURLs("MIT", "https://example.com"), + }, + want: []License{ + { + Value: "MIT", + SPDXExpression: "MIT", + Type: license.Declared, + URL: internal.NewStringSet("https://example.com"), + Location: source.NewLocationSet(source.NewLocation("/place")), + }, + }, + }, + { + name: "different licenses from different sources with different types constitute two licenses", + licenses: []License{ + NewLicenseFromType("MIT", license.Concluded), + NewLicenseFromType("MIT", license.Declared), + NewLicenseFromLocations("MIT", source.NewLocation("/place")), + NewLicenseFromURLs("MIT", "https://example.com"), + }, + want: []License{ + { + Value: "MIT", + SPDXExpression: "MIT", + Type: license.Concluded, + URL: internal.NewStringSet(), + Location: source.NewLocationSet(), + }, + { + Value: "MIT", + SPDXExpression: "MIT", + Type: license.Declared, + URL: internal.NewStringSet("https://example.com"), + Location: source.NewLocationSet(source.NewLocation("/place")), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := NewLicenseSet() + s.Add(tt.licenses...) + testMe := s.ToSlice() + assert.Equal(t, tt.want, testMe) + }) + } +} diff --git a/syft/pkg/license_test.go b/syft/pkg/license_test.go new file mode 100644 index 000000000..142d18d0a --- /dev/null +++ b/syft/pkg/license_test.go @@ -0,0 +1,99 @@ +package pkg + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/source" +) + +func Test_Hash(t *testing.T) { + + loc1 := source.NewLocation("place!") + loc1.FileSystemID = "fs1" + loc2 := source.NewLocation("place!") + loc2.FileSystemID = "fs2" // important! there is a different file system ID + + lic1 := NewLicenseFromLocations("MIT", loc1) + lic2 := NewLicenseFromLocations("MIT", loc2) + + lic1.URL.Add("foo") + lic2.URL.Add("bar") // we also want to check the URL are ignored + + hash1, err := artifact.IDByHash(lic1) + require.NoError(t, err) + + hash2, err := artifact.IDByHash(lic2) + require.NoError(t, err) + + assert.Equal(t, hash1, hash2) +} + +func Test_Sort(t *testing.T) { + tests := []struct { + name string + licenses Licenses + expected Licenses + }{ + { + name: "empty", + licenses: []License{}, + expected: []License{}, + }, + { + name: "single", + licenses: []License{ + NewLicenseFromLocations("MIT", source.NewLocation("place!")), + }, + expected: []License{ + NewLicenseFromLocations("MIT", source.NewLocation("place!")), + }, + }, + { + name: "multiple", + licenses: []License{ + NewLicenseFromLocations("MIT", source.NewLocation("place!")), + NewLicenseFromURLs("MIT", "https://github.com/anchore/syft/blob/main/LICENSE"), + NewLicenseFromLocations("Apache", source.NewLocation("area!")), + NewLicenseFromLocations("gpl2+", source.NewLocation("area!")), + }, + expected: Licenses{ + NewLicenseFromLocations("Apache", source.NewLocation("area!")), + NewLicenseFromURLs("MIT", "https://github.com/anchore/syft/blob/main/LICENSE"), + NewLicenseFromLocations("MIT", source.NewLocation("place!")), + NewLicenseFromLocations("gpl2+", source.NewLocation("area!")), + }, + }, + { + name: "multiple with location variants", + licenses: []License{ + NewLicenseFromLocations("MIT", source.NewLocation("place!")), + NewLicenseFromLocations("MIT", source.NewLocation("park!")), + NewLicense("MIT"), + NewLicense("AAL"), + NewLicense("Adobe-2006"), + NewLicenseFromLocations("Apache", source.NewLocation("area!")), + }, + expected: Licenses{ + NewLicense("AAL"), + NewLicense("Adobe-2006"), + NewLicenseFromLocations("Apache", source.NewLocation("area!")), + NewLicense("MIT"), + NewLicenseFromLocations("MIT", source.NewLocation("park!")), + NewLicenseFromLocations("MIT", source.NewLocation("place!")), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sort.Sort(test.licenses) + assert.Equal(t, test.expected, test.licenses) + }) + + } +} diff --git a/syft/pkg/npm_package_json_metadata.go b/syft/pkg/npm_package_json_metadata.go index c1756f1ea..a17f4035f 100644 --- a/syft/pkg/npm_package_json_metadata.go +++ b/syft/pkg/npm_package_json_metadata.go @@ -2,12 +2,11 @@ package pkg // NpmPackageJSONMetadata holds parsing information for a javascript package.json file type NpmPackageJSONMetadata struct { - Name string `mapstructure:"name" json:"name"` - Version string `mapstructure:"version" json:"version"` - Author string `mapstructure:"author" json:"author"` - Licenses []string `mapstructure:"licenses" json:"licenses"` - Homepage string `mapstructure:"homepage" json:"homepage"` - Description string `mapstructure:"description" json:"description"` - URL string `mapstructure:"url" json:"url"` - Private bool `mapstructure:"private" json:"private"` + Name string `mapstructure:"name" json:"name"` + Version string `mapstructure:"version" json:"version"` + Author string `mapstructure:"author" json:"author"` + Homepage string `mapstructure:"homepage" json:"homepage"` + Description string `mapstructure:"description" json:"description"` + URL string `mapstructure:"url" json:"url"` + Private bool `mapstructure:"private" json:"private"` } diff --git a/syft/pkg/package.go b/syft/pkg/package.go index 88351f217..6d028f20c 100644 --- a/syft/pkg/package.go +++ b/syft/pkg/package.go @@ -16,13 +16,14 @@ import ( // Package represents an application or library that has been bundled into a distributable format. // TODO: if we ignore FoundBy for ID generation should we merge the field to show it was found in two places? +// TODO: should cyclonedx tags exist on the struct? Why don't we use the model.Package type? type Package struct { id artifact.ID `hash:"ignore"` Name string // the package name Version string // the version of the package FoundBy string `hash:"ignore" cyclonedx:"foundBy"` // the specific cataloger that discovered this package Locations source.LocationSet // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package) - Licenses []string // licenses discovered with the package metadata + Licenses LicenseSet // licenses discovered with the package metadata Language Language `hash:"ignore" cyclonedx:"language"` // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc) Type Type `cyclonedx:"type"` // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc) CPEs []cpe.CPE `hash:"ignore"` // all possible Common Platform Enumerators (note: this is NOT included in the definition of the ID since all fields on a CPE are derived from other fields) @@ -64,6 +65,7 @@ func (p *Package) merge(other Package) error { } p.Locations.Add(other.Locations.ToSlice()...) + p.Licenses.Add(other.Licenses.ToSlice()...) p.CPEs = cpe.Merge(p.CPEs, other.CPEs) diff --git a/syft/pkg/package_test.go b/syft/pkg/package_test.go index 69addce28..7c461a680 100644 --- a/syft/pkg/package_test.go +++ b/syft/pkg/package_test.go @@ -27,10 +27,10 @@ func TestIDUniqueness(t *testing.T) { Locations: source.NewLocationSet( originalLocation, ), - Licenses: []string{ - "cc0-1.0", - "MIT", - }, + Licenses: NewLicenseSet( + NewLicense("MIT"), + NewLicense("cc0-1.0"), + ), Language: "math", Type: PythonPkg, CPEs: []cpe.CPE{ @@ -41,7 +41,6 @@ func TestIDUniqueness(t *testing.T) { Metadata: PythonPackageMetadata{ Name: "pi", Version: "3.14", - License: "cc0-1.0", Author: "Archimedes", AuthorEmail: "Archimedes@circles.io", Platform: "universe", @@ -70,7 +69,6 @@ func TestIDUniqueness(t *testing.T) { pkg.Metadata = PythonPackageMetadata{ Name: "pi", Version: "3.14", - License: "cc0-1.0", Author: "Archimedes", AuthorEmail: "Archimedes@circles.io", Platform: "universe", @@ -84,10 +82,10 @@ func TestIDUniqueness(t *testing.T) { name: "licenses order is ignored", transform: func(pkg Package) Package { // note: same as the original package, only a different order - pkg.Licenses = []string{ - "MIT", - "cc0-1.0", - } + pkg.Licenses = NewLicenseSet( + NewLicense("cc0-1.0"), + NewLicense("MIT"), + ) return pkg }, expectedIDComparison: assert.Equal, @@ -110,6 +108,14 @@ func TestIDUniqueness(t *testing.T) { }, expectedIDComparison: assert.NotEqual, }, + { + name: "licenses is reflected", + transform: func(pkg Package) Package { + pkg.Licenses = NewLicenseSet(NewLicense("new!")) + return pkg + }, + expectedIDComparison: assert.NotEqual, + }, { name: "same path for different filesystem is NOT reflected", transform: func(pkg Package) Package { @@ -143,14 +149,6 @@ func TestIDUniqueness(t *testing.T) { }, expectedIDComparison: assert.NotEqual, }, - { - name: "licenses is reflected", - transform: func(pkg Package) Package { - pkg.Licenses = []string{"new!"} - return pkg - }, - expectedIDComparison: assert.NotEqual, - }, { name: "type is reflected", transform: func(pkg Package) Package { @@ -264,10 +262,6 @@ func TestPackage_Merge(t *testing.T) { Locations: source.NewLocationSet( originalLocation, ), - Licenses: []string{ - "cc0-1.0", - "MIT", - }, Language: "math", Type: PythonPkg, CPEs: []cpe.CPE{ @@ -278,7 +272,6 @@ func TestPackage_Merge(t *testing.T) { Metadata: PythonPackageMetadata{ Name: "pi", Version: "3.14", - License: "cc0-1.0", Author: "Archimedes", AuthorEmail: "Archimedes@circles.io", Platform: "universe", @@ -292,10 +285,6 @@ func TestPackage_Merge(t *testing.T) { Locations: source.NewLocationSet( similarLocation, // NOTE: difference; we have a different layer but the same path ), - Licenses: []string{ - "cc0-1.0", - "MIT", - }, Language: "math", Type: PythonPkg, CPEs: []cpe.CPE{ @@ -306,7 +295,6 @@ func TestPackage_Merge(t *testing.T) { Metadata: PythonPackageMetadata{ Name: "pi", Version: "3.14", - License: "cc0-1.0", Author: "Archimedes", AuthorEmail: "Archimedes@circles.io", Platform: "universe", @@ -321,10 +309,6 @@ func TestPackage_Merge(t *testing.T) { originalLocation, similarLocation, // NOTE: merge! ), - Licenses: []string{ - "cc0-1.0", - "MIT", - }, Language: "math", Type: PythonPkg, CPEs: []cpe.CPE{ @@ -336,7 +320,6 @@ func TestPackage_Merge(t *testing.T) { Metadata: PythonPackageMetadata{ Name: "pi", Version: "3.14", - License: "cc0-1.0", Author: "Archimedes", AuthorEmail: "Archimedes@circles.io", Platform: "universe", @@ -353,10 +336,6 @@ func TestPackage_Merge(t *testing.T) { Locations: source.NewLocationSet( originalLocation, ), - Licenses: []string{ - "cc0-1.0", - "MIT", - }, Language: "math", Type: PythonPkg, CPEs: []cpe.CPE{ @@ -367,7 +346,6 @@ func TestPackage_Merge(t *testing.T) { Metadata: PythonPackageMetadata{ Name: "pi", Version: "3.14", - License: "cc0-1.0", Author: "Archimedes", AuthorEmail: "Archimedes@circles.io", Platform: "universe", @@ -381,10 +359,6 @@ func TestPackage_Merge(t *testing.T) { Locations: source.NewLocationSet( originalLocation, ), - Licenses: []string{ - "cc0-1.0", - "MIT", - }, Language: "math", Type: PythonPkg, CPEs: []cpe.CPE{ @@ -395,7 +369,6 @@ func TestPackage_Merge(t *testing.T) { Metadata: PythonPackageMetadata{ Name: "pi", Version: "3.14", - License: "cc0-1.0", Author: "Archimedes", AuthorEmail: "Archimedes@circles.io", Platform: "universe", @@ -439,6 +412,24 @@ func TestPackage_Merge(t *testing.T) { return true }, ), + cmp.Comparer( + func(x, y LicenseSet) bool { + xs := x.ToSlice() + ys := y.ToSlice() + + if len(xs) != len(ys) { + return false + } + for i, xe := range xs { + ye := ys[i] + if !licenseComparer(xe, ye) { + return false + } + } + + return true + }, + ), cmp.Comparer(locationComparer), ); diff != "" { t.Errorf("unexpected result from parsing (-expected +actual)\n%s", diff) @@ -447,6 +438,10 @@ func TestPackage_Merge(t *testing.T) { } } +func licenseComparer(x, y License) bool { + return cmp.Equal(x, y, cmp.Comparer(locationComparer)) +} + func locationComparer(x, y source.Location) bool { return cmp.Equal(x.Coordinates, y.Coordinates) && cmp.Equal(x.VirtualPath, y.VirtualPath) } diff --git a/syft/pkg/php_composer_json_metadata.go b/syft/pkg/php_composer_json_metadata.go index dc0a6e804..4bc7528c1 100644 --- a/syft/pkg/php_composer_json_metadata.go +++ b/syft/pkg/php_composer_json_metadata.go @@ -10,10 +10,10 @@ type PhpComposerJSONMetadata struct { Provide map[string]string `json:"provide,omitempty"` RequireDev map[string]string `json:"require-dev,omitempty"` Suggest map[string]string `json:"suggest,omitempty"` + License []string `json:"license,omitempty"` Type string `json:"type,omitempty"` NotificationURL string `json:"notification-url,omitempty"` Bin []string `json:"bin,omitempty"` - License []string `json:"license,omitempty"` Authors []PhpComposerAuthors `json:"authors,omitempty"` Description string `json:"description,omitempty"` Homepage string `json:"homepage,omitempty"` diff --git a/syft/pkg/python_package_metadata.go b/syft/pkg/python_package_metadata.go index 97a8ab806..36d7fe887 100644 --- a/syft/pkg/python_package_metadata.go +++ b/syft/pkg/python_package_metadata.go @@ -31,7 +31,6 @@ type PythonDirectURLOriginInfo struct { type PythonPackageMetadata struct { Name string `json:"name" mapstruct:"Name"` Version string `json:"version" mapstruct:"Version"` - License string `json:"license" mapstruct:"License"` Author string `json:"author" mapstruct:"Author"` AuthorEmail string `json:"authorEmail" mapstruct:"Authoremail"` Platform string `json:"platform" mapstruct:"Platform"` diff --git a/syft/pkg/rpm_metadata.go b/syft/pkg/rpm_metadata.go index 899147d17..41a825d94 100644 --- a/syft/pkg/rpm_metadata.go +++ b/syft/pkg/rpm_metadata.go @@ -29,7 +29,6 @@ type RpmMetadata struct { Release string `json:"release" cyclonedx:"release"` SourceRpm string `json:"sourceRpm" cyclonedx:"sourceRpm"` Size int `json:"size" cyclonedx:"size"` - License string `json:"license"` Vendor string `json:"vendor"` ModularityLabel string `json:"modularityLabel"` Files []RpmdbFileRecord `json:"files"` diff --git a/syft/sbom/sbom.go b/syft/sbom/sbom.go index 68f1b9601..777002718 100644 --- a/syft/sbom/sbom.go +++ b/syft/sbom/sbom.go @@ -24,6 +24,7 @@ type Artifacts struct { FileMetadata map[source.Coordinates]source.FileMetadata FileDigests map[source.Coordinates][]file.Digest FileContents map[source.Coordinates]string + FileLicenses map[source.Coordinates][]file.License Secrets map[source.Coordinates][]file.SearchResult LinuxDistribution *linux.Release } diff --git a/syft/source/location.go b/syft/source/location.go index 70e140a10..3abadd3fb 100644 --- a/syft/source/location.go +++ b/syft/source/location.go @@ -51,6 +51,12 @@ func (l Location) WithAnnotation(key, value string) Location { return l } +func (l Location) WithoutAnnotations() Location { + l.LocationMetadata.Annotations = map[string]string{} + + return l +} + // NewLocation creates a new Location representing a path without denoting a filesystem or FileCatalog reference. func NewLocation(realPath string) Location { return Location{ diff --git a/syft/source/location_set.go b/syft/source/location_set.go index 86d184c76..100bf95e5 100644 --- a/syft/source/location_set.go +++ b/syft/source/location_set.go @@ -82,6 +82,13 @@ func (s *LocationSet) CoordinateSet() CoordinateSet { return set } +func (s *LocationSet) Empty() bool { + if s.set == nil { + return true + } + return len(s.set) == 0 +} + func (s LocationSet) Hash() (uint64, error) { // access paths and filesystem IDs are not considered when hashing a location set, only the real paths return hashstructure.Hash(s.CoordinateSet().Paths(), hashstructure.FormatV2, &hashstructure.HashOptions{ diff --git a/syft/source/unindexed_directory_resolver.go b/syft/source/unindexed_directory_resolver.go index e0b74d2ed..e965fef5c 100644 --- a/syft/source/unindexed_directory_resolver.go +++ b/syft/source/unindexed_directory_resolver.go @@ -231,13 +231,7 @@ func (u UnindexedDirectoryResolver) AllLocations() <-chan Location { return nil } p = strings.TrimPrefix(p, "/") - out <- Location{ - LocationData: LocationData{ - Coordinates: Coordinates{ - RealPath: p, - }, - }, - } + out <- NewLocation(p) return nil }) if err != nil { @@ -283,14 +277,8 @@ func (u UnindexedDirectoryResolver) newLocation(filePath string, resolveLinks bo } } - return &Location{ - LocationData: LocationData{ - Coordinates: Coordinates{ - RealPath: realPath, - }, - VirtualPath: virtualPath, - }, - } + l := NewVirtualLocation(realPath, virtualPath) + return &l } //nolint:gocognit diff --git a/test/integration/encode_decode_cycle_test.go b/test/integration/encode_decode_cycle_test.go index 54304b8ac..67940212b 100644 --- a/test/integration/encode_decode_cycle_test.go +++ b/test/integration/encode_decode_cycle_test.go @@ -72,14 +72,14 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { require.NotNil(t, format) by1, err := formats.Encode(originalSBOM, format) - assert.NoError(t, err) + require.NoError(t, err) newSBOM, newFormat, err := formats.Decode(bytes.NewReader(by1)) - assert.NoError(t, err) - assert.Equal(t, format.ID(), newFormat.ID()) + require.NoError(t, err) + require.Equal(t, format.ID(), newFormat.ID()) by2, err := formats.Encode(*newSBOM, format) - assert.NoError(t, err) + require.NoError(t, err) if test.redactor != nil { by1 = test.redactor(by1) diff --git a/test/integration/package_deduplication_test.go b/test/integration/package_deduplication_test.go index 912267f12..e0760cd37 100644 --- a/test/integration/package_deduplication_test.go +++ b/test/integration/package_deduplication_test.go @@ -77,6 +77,7 @@ func TestPackageDeduplication(t *testing.T) { // with multiple packages with the same name, something is wrong (or this is the wrong fixture) require.Len(t, pkgs, expectedInstanceCount) + for _, p := range pkgs { nameVersion := fmt.Sprintf("%s-%s", name, p.Version) expectedLocationCount, ok := tt.locationCount[nameVersion]