From 76f8205936b257be62f5eba63362572551e06be8 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Wed, 9 Feb 2022 14:11:20 -0500 Subject: [PATCH] Suport SPDX SBOM decoding (#738) --- go.mod | 2 +- go.sum | 4 +- .../common/spdxhelpers/download_location.go | 5 +- .../spdxhelpers/download_location_test.go | 4 +- .../spdxhelpers}/external_ref.go | 2 +- .../common/spdxhelpers/external_refs.go | 41 +-- .../common/spdxhelpers/external_refs_test.go | 13 +- .../formats/common/spdxhelpers/file_type.go | 17 + .../formats/common/spdxhelpers/license.go | 4 +- .../common/spdxhelpers/license_test.go | 4 +- .../common/spdxhelpers/none_if_empty.go | 2 +- .../common/spdxhelpers/none_if_empty_test.go | 6 +- .../common/spdxhelpers/relationship_type.go | 173 +++++++++ .../common/spdxhelpers/to_syft_model.go | 339 ++++++++++++++++++ .../common/spdxhelpers/to_syft_model_test.go | 197 ++++++++++ internal/formats/spdx22json/decoder.go | 28 ++ internal/formats/spdx22json/decoder_test.go | 103 ++++++ internal/formats/spdx22json/format.go | 6 +- internal/formats/spdx22json/model/file.go | 16 - internal/formats/spdx22json/model/package.go | 4 +- .../formats/spdx22json/model/relationship.go | 176 +-------- .../spdx/alpine-3.10.syft.spdx.json | 170 +++++++++ .../spdx/alpine-3.10.vendor.spdx.json | 78 ++++ .../spdx/bad/example7-bin.spdx.json | 72 ++++ .../spdx/bad/example7-go-module.spdx.json | 29 ++ .../spdx/bad/example7-golang.spdx.json | 46 +++ .../example7-third-party-modules.spdx.json | 49 +++ .../test-fixtures/spdx/example7-bin.spdx.json | 68 ++++ .../spdx/example7-go-module.spdx.json | 25 ++ .../spdx/example7-golang.spdx.json | 42 +++ .../example7-third-party-modules.spdx.json | 45 +++ .../formats/spdx22json/to_format_model.go | 22 +- .../spdx22json/to_format_model_test.go | 25 +- internal/formats/spdx22json/validator.go | 10 + internal/formats/spdx22tagvalue/decoder.go | 20 ++ internal/formats/spdx22tagvalue/format.go | 4 +- .../formats/spdx22tagvalue/to_format_model.go | 8 +- internal/formats/spdx22tagvalue/validator.go | 10 + internal/formats/syftjson/to_syft_model.go | 63 +++- syft/pkg/apk_metadata.go | 4 +- syft/pkg/dpkg_metadata.go | 6 +- syft/pkg/language.go | 6 +- syft/pkg/python_package_metadata.go | 2 +- syft/pkg/rpmdb_metadata.go | 6 +- syft/pkg/type.go | 6 +- syft/pkg/url.go | 14 +- 46 files changed, 1679 insertions(+), 297 deletions(-) rename internal/formats/{spdx22json/model => common/spdxhelpers}/external_ref.go (98%) create mode 100644 internal/formats/common/spdxhelpers/file_type.go create mode 100644 internal/formats/common/spdxhelpers/relationship_type.go create mode 100644 internal/formats/common/spdxhelpers/to_syft_model.go create mode 100644 internal/formats/common/spdxhelpers/to_syft_model_test.go create mode 100644 internal/formats/spdx22json/decoder.go create mode 100644 internal/formats/spdx22json/decoder_test.go create mode 100644 internal/formats/spdx22json/test-fixtures/spdx/alpine-3.10.syft.spdx.json create mode 100644 internal/formats/spdx22json/test-fixtures/spdx/alpine-3.10.vendor.spdx.json create mode 100644 internal/formats/spdx22json/test-fixtures/spdx/bad/example7-bin.spdx.json create mode 100644 internal/formats/spdx22json/test-fixtures/spdx/bad/example7-go-module.spdx.json create mode 100644 internal/formats/spdx22json/test-fixtures/spdx/bad/example7-golang.spdx.json create mode 100644 internal/formats/spdx22json/test-fixtures/spdx/bad/example7-third-party-modules.spdx.json create mode 100644 internal/formats/spdx22json/test-fixtures/spdx/example7-bin.spdx.json create mode 100644 internal/formats/spdx22json/test-fixtures/spdx/example7-go-module.spdx.json create mode 100644 internal/formats/spdx22json/test-fixtures/spdx/example7-golang.spdx.json create mode 100644 internal/formats/spdx22json/test-fixtures/spdx/example7-third-party-modules.spdx.json create mode 100644 internal/formats/spdx22json/validator.go create mode 100644 internal/formats/spdx22tagvalue/decoder.go create mode 100644 internal/formats/spdx22tagvalue/validator.go diff --git a/go.mod b/go.mod index 22cb67026d5..75dfa6cd58f 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/scylladb/go-set v1.0.2 github.com/sergi/go-diff v1.1.0 github.com/sirupsen/logrus v1.8.1 - github.com/spdx/tools-golang v0.1.0 + github.com/spdx/tools-golang v0.2.0 github.com/spf13/afero v1.6.0 github.com/spf13/cobra v1.2.1 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index 87040014a2e..c815d2e1e36 100644 --- a/go.sum +++ b/go.sum @@ -759,8 +759,8 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= -github.com/spdx/tools-golang v0.1.0 h1:iDMNEPqQk6CdiDj6eWDIDw85j0wQ3IR3pH9p0X05TSQ= -github.com/spdx/tools-golang v0.1.0/go.mod h1:RO4Y3IFROJnz+43JKm1YOrbtgQNljW4gAPpA/sY2eqo= +github.com/spdx/tools-golang v0.2.0 h1:KBNcw7xvVycRWeCWZK/5xQJA+plymW1+rTCs8ekJDro= +github.com/spdx/tools-golang v0.2.0/go.mod h1:RO4Y3IFROJnz+43JKm1YOrbtgQNljW4gAPpA/sY2eqo= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= diff --git a/internal/formats/common/spdxhelpers/download_location.go b/internal/formats/common/spdxhelpers/download_location.go index 7c22aa87067..060f78359f3 100644 --- a/internal/formats/common/spdxhelpers/download_location.go +++ b/internal/formats/common/spdxhelpers/download_location.go @@ -2,6 +2,9 @@ package spdxhelpers import "github.com/anchore/syft/syft/pkg" +const NONE = "NONE" +const NOASSERTION = "NOASSERTION" + func DownloadLocation(p pkg.Package) string { // 3.7: Package Download Location // Cardinality: mandatory, one @@ -19,5 +22,5 @@ func DownloadLocation(p pkg.Package) string { return NoneIfEmpty(metadata.URL) } } - return "NOASSERTION" + return NOASSERTION } diff --git a/internal/formats/common/spdxhelpers/download_location_test.go b/internal/formats/common/spdxhelpers/download_location_test.go index a20bef6d12c..42b205a1e42 100644 --- a/internal/formats/common/spdxhelpers/download_location_test.go +++ b/internal/formats/common/spdxhelpers/download_location_test.go @@ -16,7 +16,7 @@ func Test_DownloadLocation(t *testing.T) { { name: "no metadata", input: pkg.Package{}, - expected: "NOASSERTION", + expected: NOASSERTION, }, { name: "from apk", @@ -43,7 +43,7 @@ func Test_DownloadLocation(t *testing.T) { URL: "", }, }, - expected: "NONE", + expected: NONE, }, } for _, test := range tests { diff --git a/internal/formats/spdx22json/model/external_ref.go b/internal/formats/common/spdxhelpers/external_ref.go similarity index 98% rename from internal/formats/spdx22json/model/external_ref.go rename to internal/formats/common/spdxhelpers/external_ref.go index 9dc49b3dd90..98a1bc37ece 100644 --- a/internal/formats/spdx22json/model/external_ref.go +++ b/internal/formats/common/spdxhelpers/external_ref.go @@ -1,4 +1,4 @@ -package model +package spdxhelpers type ReferenceCategory string diff --git a/internal/formats/common/spdxhelpers/external_refs.go b/internal/formats/common/spdxhelpers/external_refs.go index d961874733b..384282cb038 100644 --- a/internal/formats/common/spdxhelpers/external_refs.go +++ b/internal/formats/common/spdxhelpers/external_refs.go @@ -1,51 +1,26 @@ package spdxhelpers import ( - "github.com/anchore/syft/internal/formats/spdx22json/model" - "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/pkg" ) -func ExternalRefs(p pkg.Package) (externalRefs []model.ExternalRef) { - externalRefs = make([]model.ExternalRef, 0) +func ExternalRefs(p pkg.Package) (externalRefs []ExternalRef) { + externalRefs = make([]ExternalRef, 0) for _, c := range p.CPEs { - externalRefs = append(externalRefs, model.ExternalRef{ - ReferenceCategory: model.SecurityReferenceCategory, + externalRefs = append(externalRefs, ExternalRef{ + ReferenceCategory: SecurityReferenceCategory, ReferenceLocator: pkg.CPEString(c), - ReferenceType: model.Cpe23ExternalRefType, + ReferenceType: Cpe23ExternalRefType, }) } if p.PURL != "" { - externalRefs = append(externalRefs, model.ExternalRef{ - ReferenceCategory: model.PackageManagerReferenceCategory, + externalRefs = append(externalRefs, ExternalRef{ + ReferenceCategory: PackageManagerReferenceCategory, ReferenceLocator: p.PURL, - ReferenceType: model.PurlExternalRefType, + ReferenceType: PurlExternalRefType, }) } return externalRefs } - -func ExtractPURL(refs []model.ExternalRef) string { - for _, r := range refs { - if r.ReferenceType == model.PurlExternalRefType { - return r.ReferenceLocator - } - } - return "" -} - -func ExtractCPEs(refs []model.ExternalRef) (cpes []pkg.CPE) { - for _, r := range refs { - if r.ReferenceType == model.Cpe23ExternalRefType { - cpe, err := pkg.NewCPE(r.ReferenceLocator) - if err != nil { - log.Warnf("unable to extract SPDX CPE=%q: %+v", r.ReferenceLocator, err) - continue - } - cpes = append(cpes, cpe) - } - } - return cpes -} diff --git a/internal/formats/common/spdxhelpers/external_refs_test.go b/internal/formats/common/spdxhelpers/external_refs_test.go index 427901738d0..180a5c4dbd1 100644 --- a/internal/formats/common/spdxhelpers/external_refs_test.go +++ b/internal/formats/common/spdxhelpers/external_refs_test.go @@ -3,7 +3,6 @@ package spdxhelpers import ( "testing" - "github.com/anchore/syft/internal/formats/spdx22json/model" "github.com/anchore/syft/syft/pkg" "github.com/stretchr/testify/assert" ) @@ -13,7 +12,7 @@ func Test_ExternalRefs(t *testing.T) { tests := []struct { name string input pkg.Package - expected []model.ExternalRef + expected []ExternalRef }{ { name: "cpe + purl", @@ -23,16 +22,16 @@ func Test_ExternalRefs(t *testing.T) { }, PURL: "a-purl", }, - expected: []model.ExternalRef{ + expected: []ExternalRef{ { - ReferenceCategory: model.SecurityReferenceCategory, + ReferenceCategory: SecurityReferenceCategory, ReferenceLocator: pkg.CPEString(testCPE), - ReferenceType: model.Cpe23ExternalRefType, + ReferenceType: Cpe23ExternalRefType, }, { - ReferenceCategory: model.PackageManagerReferenceCategory, + ReferenceCategory: PackageManagerReferenceCategory, ReferenceLocator: "a-purl", - ReferenceType: model.PurlExternalRefType, + ReferenceType: PurlExternalRefType, }, }, }, diff --git a/internal/formats/common/spdxhelpers/file_type.go b/internal/formats/common/spdxhelpers/file_type.go new file mode 100644 index 00000000000..97e4ab4786d --- /dev/null +++ b/internal/formats/common/spdxhelpers/file_type.go @@ -0,0 +1,17 @@ +package spdxhelpers + +type FileType string + +const ( + DocumentationFileType FileType = "DOCUMENTATION" // if the file serves as documentation + ImageFileType FileType = "IMAGE" // if the file is associated with a picture image file (MIME type of image/*, e.g., .jpg, .gif) + VideoFileType FileType = "VIDEO" // if the file is associated with a video file type (MIME type of video/*) + ArchiveFileType FileType = "ARCHIVE" // if the file represents an archive (.tar, .jar, etc.) + SpdxFileType FileType = "SPDX" // if the file is an SPDX document + ApplicationFileType FileType = "APPLICATION" // if the file is associated with a specific application type (MIME type of application/*) + SourceFileType FileType = "SOURCE" // if the file is human readable source code (.c, .html, etc.) + BinaryFileType FileType = "BINARY" // if the file is a compiled object, target image or binary executable (.o, .a, etc.) + TextFileType FileType = "TEXT" // if the file is human readable text file (MIME type of text/*) + AudioFileType FileType = "AUDIO" // if the file is associated with an audio file (MIME type of audio/* , e.g. .mp3) + OtherFileType FileType = "OTHER" // if the file doesn't fit into the above categories (generated artifacts, data files, etc.) +) diff --git a/internal/formats/common/spdxhelpers/license.go b/internal/formats/common/spdxhelpers/license.go index 80d84bb3dd6..3feaee9944b 100644 --- a/internal/formats/common/spdxhelpers/license.go +++ b/internal/formats/common/spdxhelpers/license.go @@ -18,7 +18,7 @@ func License(p pkg.Package) string { // (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" + return NONE } // 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/ @@ -30,7 +30,7 @@ func License(p pkg.Package) string { } if len(parsedLicenses) == 0 { - return "NOASSERTION" + return NOASSERTION } return strings.Join(parsedLicenses, " AND ") diff --git a/internal/formats/common/spdxhelpers/license_test.go b/internal/formats/common/spdxhelpers/license_test.go index 13a62fe9899..ad4eef6bcd2 100644 --- a/internal/formats/common/spdxhelpers/license_test.go +++ b/internal/formats/common/spdxhelpers/license_test.go @@ -16,7 +16,7 @@ func Test_License(t *testing.T) { { name: "no licenses", input: pkg.Package{}, - expected: "NONE", + expected: NONE, }, { name: "no SPDX licenses", @@ -25,7 +25,7 @@ func Test_License(t *testing.T) { "made-up", }, }, - expected: "NOASSERTION", + expected: NOASSERTION, }, { name: "with SPDX license", diff --git a/internal/formats/common/spdxhelpers/none_if_empty.go b/internal/formats/common/spdxhelpers/none_if_empty.go index fa97a0d7e59..6cc272f1f84 100644 --- a/internal/formats/common/spdxhelpers/none_if_empty.go +++ b/internal/formats/common/spdxhelpers/none_if_empty.go @@ -6,7 +6,7 @@ import ( func NoneIfEmpty(value string) string { if strings.TrimSpace(value) == "" { - return "NONE" + return NONE } return value } diff --git a/internal/formats/common/spdxhelpers/none_if_empty_test.go b/internal/formats/common/spdxhelpers/none_if_empty_test.go index 4c447f1220a..3fc95893493 100644 --- a/internal/formats/common/spdxhelpers/none_if_empty_test.go +++ b/internal/formats/common/spdxhelpers/none_if_empty_test.go @@ -20,17 +20,17 @@ func Test_noneIfEmpty(t *testing.T) { { name: "empty", value: "", - expected: "NONE", + expected: NONE, }, { name: "space", value: " ", - expected: "NONE", + expected: NONE, }, { name: "tab", value: "\t", - expected: "NONE", + expected: NONE, }, } for _, test := range tests { diff --git a/internal/formats/common/spdxhelpers/relationship_type.go b/internal/formats/common/spdxhelpers/relationship_type.go new file mode 100644 index 00000000000..564dd32ec02 --- /dev/null +++ b/internal/formats/common/spdxhelpers/relationship_type.go @@ -0,0 +1,173 @@ +package spdxhelpers + +// source: https://spdx.github.io/spdx-spec/7-relationships-between-SPDX-elements/ +type RelationshipType string + +const ( + // DescribedByRelationship is to be used when SPDXRef-A is described by SPDXREF-Document. + // Example: The package 'WildFly' is described by SPDX document WildFly.spdx. + DescribedByRelationship RelationshipType = "DESCRIBED_BY" + + // ContainsRelationship is to be used when SPDXRef-A contains SPDXRef-B. + // Example: An ARCHIVE file bar.tgz contains a SOURCE file foo.c. + ContainsRelationship RelationshipType = "CONTAINS" + + // ContainedByRelationship is to be used when SPDXRef-A is contained by SPDXRef-B. + // Example: A SOURCE file foo.c is contained by ARCHIVE file bar.tgz + ContainedByRelationship RelationshipType = "CONTAINED_BY" + + // DependsOnRelationship is to be used when SPDXRef-A depends on SPDXRef-B. + // Example: Package A depends on the presence of package B in order to build and run + DependsOnRelationship RelationshipType = "DEPENDS_ON" + + // DependencyOfRelationship is to be used when SPDXRef-A is dependency of SPDXRef-B. + // Example: A is explicitly stated as a dependency of B in a machine-readable file. Use when a package manager does not define scopes. + DependencyOfRelationship RelationshipType = "DEPENDENCY_OF" + + // DependencyManifestOfRelationship is to be used when SPDXRef-A is a manifest file that lists a set of dependencies for SPDXRef-B. + // Example: A file package.json is the dependency manifest of a package foo. Note that only one manifest should be used to define the same dependency graph. + DependencyManifestOfRelationship RelationshipType = "DEPENDENCY_MANIFEST_OF" + + // BuildDependencyOfRelationship is to be used when SPDXRef-A is a build dependency of SPDXRef-B. + // Example: A is in the compile scope of B in a Maven project. + BuildDependencyOfRelationship RelationshipType = "BUILD_DEPENDENCY_OF" + + // DevDependencyOfRelationship is to be used when SPDXRef-A is a development dependency of SPDXRef-B. + // Example: A is in the devDependencies scope of B in a Maven project. + DevDependencyOfRelationship RelationshipType = "DEV_DEPENDENCY_OF" + + // OptionalDependencyOfRelationship is to be used when SPDXRef-A is an optional dependency of SPDXRef-B. + // Example: Use when building the code will proceed even if a dependency cannot be found, fails to install, or is only installed on a specific platform. For example, A is in the optionalDependencies scope of npm project B. + OptionalDependencyOfRelationship RelationshipType = "OPTIONAL_DEPENDENCY_OF" + + // ProvidedDependencyOfRelationship is to be used when SPDXRef-A is a to be provided dependency of SPDXRef-B. + // Example: A is in the provided scope of B in a Maven project, indicating that the project expects it to be provided, for instance, by the container or JDK. + ProvidedDependencyOfRelationship RelationshipType = "PROVIDED_DEPENDENCY_OF" + + // TestDependencyOfRelationship is to be used when SPDXRef-A is a test dependency of SPDXRef-B. + // Example: A is in the test scope of B in a Maven project. + TestDependencyOfRelationship RelationshipType = "TEST_DEPENDENCY_OF" + + // RuntimeDependencyOfRelationship is to be used when SPDXRef-A is a dependency required for the execution of SPDXRef-B. + // Example: A is in the runtime scope of B in a Maven project. + RuntimeDependencyOfRelationship RelationshipType = "RUNTIME_DEPENDENCY_OF" + + // ExampleOfRelationship is to be used when SPDXRef-A is an example of SPDXRef-B. + // Example: The file or snippet that illustrates how to use an application or library. + ExampleOfRelationship RelationshipType = "EXAMPLE_OF" + + // GeneratesRelationship is to be used when SPDXRef-A generates SPDXRef-B. + // Example: A SOURCE file makefile.mk generates a BINARY file a.out + GeneratesRelationship RelationshipType = "GENERATES" + + // GeneratedFromRelationship is to be used when SPDXRef-A was generated from SPDXRef-B. + // Example: A BINARY file a.out has been generated from a SOURCE file makefile.mk. A BINARY file foolib.a is generated from a SOURCE file bar.c. + GeneratedFromRelationship RelationshipType = "GENERATED_FROM" + + // AncestorOfRelationship is to be used when SPDXRef-A is an ancestor (same lineage but pre-dates) SPDXRef-B. + // Example: A SOURCE file makefile.mk is a version of the original ancestor SOURCE file 'makefile2.mk' + AncestorOfRelationship RelationshipType = "ANCESTOR_OF" + + // DescendantOfRelationship is to be used when SPDXRef-A is a descendant of (same lineage but postdates) SPDXRef-B. + // Example: A SOURCE file makefile2.mk is a descendant of the original SOURCE file 'makefile.mk' + DescendantOfRelationship RelationshipType = "DESCENDANT_OF" + + // VariantOfRelationship is to be used when SPDXRef-A is a variant of (same lineage but not clear which came first) SPDXRef-B. + // Example: A SOURCE file makefile2.mk is a variant of SOURCE file makefile.mk if they differ by some edit, but there is no way to tell which came first (no reliable date information). + VariantOfRelationship RelationshipType = "VARIANT_OF" + + // DistributionArtifactRelationship is to be used when distributing SPDXRef-A requires that SPDXRef-B also be distributed. + // Example: A BINARY file foo.o requires that the ARCHIVE file bar-sources.tgz be made available on distribution. + DistributionArtifactRelationship RelationshipType = "DISTRIBUTION_ARTIFACT" + + // PatchForRelationship is to be used when SPDXRef-A is a patch file for (to be applied to) SPDXRef-B. + // Example: A SOURCE file foo.diff is a patch file for SOURCE file foo.c. + PatchForRelationship RelationshipType = "PATCH_FOR" + + // PatchAppliedRelationship is to be used when SPDXRef-A is a patch file that has been applied to SPDXRef-B. + // Example: A SOURCE file foo.diff is a patch file that has been applied to SOURCE file 'foo-patched.c'. + PatchAppliedRelationship RelationshipType = "PATCH_APPLIED" + + // CopyOfRelationship is to be used when SPDXRef-A is an exact copy of SPDXRef-B. + // Example: A BINARY file alib.a is an exact copy of BINARY file a2lib.a. + CopyOfRelationship RelationshipType = "COPY_OF" + + // FileAddedRelationship is to be used when SPDXRef-A is a file that was added to SPDXRef-B. + // Example: A SOURCE file foo.c has been added to package ARCHIVE bar.tgz. + FileAddedRelationship RelationshipType = "FILE_ADDED" + + // FileDeletedRelationship is to be used when SPDXRef-A is a file that was deleted from SPDXRef-B. + // Example: A SOURCE file foo.diff has been deleted from package ARCHIVE bar.tgz. + FileDeletedRelationship RelationshipType = "FILE_DELETED" + + // FileModifiedRelationship is to be used when SPDXRef-A is a file that was modified from SPDXRef-B. + // Example: A SOURCE file foo.c has been modified from SOURCE file foo.orig.c. + FileModifiedRelationship RelationshipType = "FILE_MODIFIED" + + // ExpandedFromArchiveRelationship is to be used when SPDXRef-A is expanded from the archive SPDXRef-B. + // Example: A SOURCE file foo.c, has been expanded from the archive ARCHIVE file xyz.tgz. + ExpandedFromArchiveRelationship RelationshipType = "EXPANDED_FROM_ARCHIVE" + + // DynamicLinkRelationship is to be used when SPDXRef-A dynamically links to SPDXRef-B. + // Example: An APPLICATION file 'myapp' dynamically links to BINARY file zlib.so. + DynamicLinkRelationship RelationshipType = "DYNAMIC_LINK" + + // StaticLinkRelationship is to be used when SPDXRef-A statically links to SPDXRef-B. + // Example: An APPLICATION file 'myapp' statically links to BINARY zlib.a. + StaticLinkRelationship RelationshipType = "STATIC_LINK" + + // DataFileOfRelationship is to be used when SPDXRef-A is a data file used in SPDXRef-B. + // Example: An IMAGE file 'kitty.jpg' is a data file of an APPLICATION 'hellokitty'. + DataFileOfRelationship RelationshipType = "DATA_FILE_OF" + + // TestCaseOfRelationship is to be used when SPDXRef-A is a test case used in testing SPDXRef-B. + // Example: A SOURCE file testMyCode.java is a unit test file used to test an APPLICATION MyPackage. + TestCaseOfRelationship RelationshipType = "TEST_CASE_OF" + + // BuildToolOfRelationship is to be used when SPDXRef-A is used to build SPDXRef-B. + // Example: A SOURCE file makefile.mk is used to build an APPLICATION 'zlib'. + BuildToolOfRelationship RelationshipType = "BUILD_TOOL_OF" + + // DevToolOfRelationship is to be used when SPDXRef-A is used as a development tool for SPDXRef-B. + // Example: Any tool used for development such as a code debugger. + DevToolOfRelationship RelationshipType = "DEV_TOOL_OF" + + // TestOfRelationship is to be used when SPDXRef-A is used for testing SPDXRef-B. + // Example: Generic relationship for cases where it's clear that something is used for testing but unclear whether it's TEST_CASE_OF or TEST_TOOL_OF. + TestOfRelationship RelationshipType = "TEST_OF" + + // TestToolOfRelationship is to be used when SPDXRef-A is used as a test tool for SPDXRef-B. + // Example: Any tool used to test the code such as ESlint. + TestToolOfRelationship RelationshipType = "TEST_TOOL_OF" + + // DocumentationOfRelationship is to be used when SPDXRef-A provides documentation of SPDXRef-B. + // Example: A DOCUMENTATION file readme.txt documents the APPLICATION 'zlib'. + DocumentationOfRelationship RelationshipType = "DOCUMENTATION_OF" + + // OptionalComponentOfRelationship is to be used when SPDXRef-A is an optional component of SPDXRef-B. + // Example: A SOURCE file fool.c (which is in the contributors directory) may or may not be included in the build of APPLICATION 'atthebar'. + OptionalComponentOfRelationship RelationshipType = "OPTIONAL_COMPONENT_OF" + + // MetafileOfRelationship is to be used when SPDXRef-A is a metafile of SPDXRef-B. + // Example: A SOURCE file pom.xml is a metafile of the APPLICATION 'Apache Xerces'. + MetafileOfRelationship RelationshipType = "METAFILE_OF" + + // PackageOfRelationship is to be used when SPDXRef-A is used as a package as part of SPDXRef-B. + // Example: A Linux distribution contains an APPLICATION package gawk as part of the distribution MyLinuxDistro. + PackageOfRelationship RelationshipType = "PACKAGE_OF" + + // AmendsRelationship is to be used when (current) SPDXRef-DOCUMENT amends the SPDX information in SPDXRef-B. + // Example: (Current) SPDX document A version 2 contains a correction to a previous version of the SPDX document A version 1. Note the reserved identifier SPDXRef-DOCUMENT for the current document is required. + AmendsRelationship RelationshipType = "AMENDS" + + // PrerequisiteForRelationship is to be used when SPDXRef-A is a prerequisite for SPDXRef-B. + // Example: A library bar.dll is a prerequisite or dependency for APPLICATION foo.exe + PrerequisiteForRelationship RelationshipType = "PREREQUISITE_FOR" + + // HasPrerequisiteRelationship is to be used when SPDXRef-A has as a prerequisite SPDXRef-B. + // Example: An APPLICATION foo.exe has prerequisite or dependency on bar.dll + HasPrerequisiteRelationship RelationshipType = "HAS_PREREQUISITE" + + // OtherRelationship is to be used for a relationship which has not been defined in the formal SPDX specification. A description of the relationship should be included in the Relationship comments field. + OtherRelationship RelationshipType = "OTHER" +) diff --git a/internal/formats/common/spdxhelpers/to_syft_model.go b/internal/formats/common/spdxhelpers/to_syft_model.go new file mode 100644 index 00000000000..63ed039a83d --- /dev/null +++ b/internal/formats/common/spdxhelpers/to_syft_model.go @@ -0,0 +1,339 @@ +package spdxhelpers + +import ( + "strconv" + "strings" + + "github.com/spdx/tools-golang/spdx" + + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" +) + +func ToSyftModel(doc *spdx.Document2_2) (*sbom.SBOM, error) { + spdxIDMap := make(map[string]interface{}) + + s := &sbom.SBOM{ + Artifacts: sbom.Artifacts{ + PackageCatalog: pkg.NewCatalog(), + FileMetadata: map[source.Coordinates]source.FileMetadata{}, + FileDigests: map[source.Coordinates][]file.Digest{}, + LinuxDistribution: findLinuxReleaseByPURL(doc), + }, + } + + collectSyftPackages(s, spdxIDMap, doc) + + collectSyftFiles(s, spdxIDMap, doc) + + s.Relationships = toSyftRelationships(spdxIDMap, doc) + + return s, nil +} + +func findLinuxReleaseByPURL(doc *spdx.Document2_2) *linux.Release { + for _, p := range doc.Packages { + purlValue := findPURLValue(p) + if purlValue == "" { + continue + } + purl, err := packageurl.FromString(purlValue) + if err != nil { + log.Warnf("unable to parse purl: %s", purlValue) + continue + } + distro := findQualifierValue(purl, pkg.PURLQualifierDistro) + if distro != "" { + parts := strings.Split(distro, "-") + name := parts[0] + version := "" + if len(parts) > 1 { + version = parts[1] + } + return &linux.Release{ + PrettyName: name, + Name: name, + ID: name, + IDLike: []string{name}, + Version: version, + VersionID: version, + } + } + } + + return nil +} + +func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document2_2) { + for _, p := range doc.Packages { + syftPkg := toSyftPackage(p) + spdxIDMap[string(p.PackageSPDXIdentifier)] = syftPkg + s.Artifacts.PackageCatalog.Add(*syftPkg) + } +} + +func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document2_2) { + for _, f := range doc.UnpackagedFiles { + l := toSyftLocation(f) + spdxIDMap[string(f.FileSPDXIdentifier)] = l + + s.Artifacts.FileMetadata[l.Coordinates] = toFileMetadata(f) + s.Artifacts.FileDigests[l.Coordinates] = toFileDigests(f) + } +} + +func toFileDigests(f *spdx.File2_2) (digests []file.Digest) { + for _, digest := range f.FileChecksums { + digests = append(digests, file.Digest{ + Algorithm: string(digest.Algorithm), + Value: digest.Value, + }) + } + return digests +} + +func toFileMetadata(f *spdx.File2_2) (meta source.FileMetadata) { + // FIXME Syft is currently lossy due to the SPDX 2.2.1 spec not supporting arbitrary mimetypes + for _, typ := range f.FileType { + switch FileType(typ) { + case ImageFileType: + meta.MIMEType = "image/" + case VideoFileType: + meta.MIMEType = "video/" + case ApplicationFileType: + meta.MIMEType = "application/" + case TextFileType: + meta.MIMEType = "text/" + case AudioFileType: + meta.MIMEType = "audio/" + case BinaryFileType: + case ArchiveFileType: + case OtherFileType: + } + } + return meta +} + +func toSyftRelationships(spdxIDMap map[string]interface{}, doc *spdx.Document2_2) []artifact.Relationship { + var out []artifact.Relationship + for _, r := range doc.Relationships { + // FIXME what to do with r.RefA.DocumentRefID and r.RefA.SpecialID + if r.RefA.DocumentRefID != "" && requireAndTrimPrefix(r.RefA.DocumentRefID, "DocumentRef-") != string(doc.CreationInfo.SPDXIdentifier) { + log.Debugf("ignoring relationship to external document: %+v", r) + continue + } + a := spdxIDMap[string(r.RefA.ElementRefID)] + b := spdxIDMap[string(r.RefB.ElementRefID)] + from, fromOk := a.(*pkg.Package) + toPackage, toPackageOk := b.(*pkg.Package) + toLocation, toLocationOk := b.(*source.Location) + if !fromOk || !(toPackageOk || toLocationOk) { + log.Debugf("unable to find valid relationship mapping from SPDX 2.2 JSON, ignoring: (from: %+v) (to: %+v)", a, b) + continue + } + var to artifact.Identifiable + var typ artifact.RelationshipType + if toLocationOk { + if r.Relationship == string(ContainsRelationship) { + typ = artifact.ContainsRelationship + to = toLocation + } + } else { + switch RelationshipType(r.Relationship) { + case ContainsRelationship: + typ = artifact.ContainsRelationship + to = toPackage + case BuildDependencyOfRelationship: + typ = artifact.BuildDependencyOfRelationship + to = toPackage + case RuntimeDependencyOfRelationship: + typ = artifact.RuntimeDependencyOfRelationship + to = toPackage + case OtherRelationship: + // Encoding uses a specifically formatted comment... + if strings.Index(r.RelationshipComment, string(artifact.OwnershipByFileOverlapRelationship)) == 0 { + typ = artifact.RuntimeDependencyOfRelationship + to = toPackage + } + } + } + if typ != "" && to != nil { + out = append(out, artifact.Relationship{ + From: from, + To: to, + Type: typ, + }) + } + } + return out +} + +func toSyftCoordinates(f *spdx.File2_2) source.Coordinates { + const layerIDPrefix = "layerID: " + var fileSystemID string + if strings.Index(f.FileComment, layerIDPrefix) == 0 { + fileSystemID = strings.TrimPrefix(f.FileComment, layerIDPrefix) + } + if strings.Index(string(f.FileSPDXIdentifier), layerIDPrefix) == 0 { + fileSystemID = strings.TrimPrefix(string(f.FileSPDXIdentifier), layerIDPrefix) + } + return source.Coordinates{ + RealPath: f.FileName, + FileSystemID: fileSystemID, + } +} + +func toSyftLocation(f *spdx.File2_2) *source.Location { + return &source.Location{ + Coordinates: toSyftCoordinates(f), + VirtualPath: f.FileName, + } +} + +func requireAndTrimPrefix(val interface{}, prefix string) string { + if v, ok := val.(string); ok { + if i := strings.Index(v, prefix); i == 0 { + return strings.Replace(v, prefix, "", 1) + } + } + return "" +} + +type pkgInfo struct { + purl packageurl.PackageURL + typ pkg.Type + lang pkg.Language +} + +func (p *pkgInfo) qualifierValue(name string) string { + return findQualifierValue(p.purl, name) +} + +func findQualifierValue(purl packageurl.PackageURL, qualifier string) string { + for _, q := range purl.Qualifiers { + if q.Key == qualifier { + return q.Value + } + } + return "" +} + +func extractPkgInfo(p *spdx.Package2_2) pkgInfo { + pu := findPURLValue(p) + purl, err := packageurl.FromString(pu) + if err != nil { + return pkgInfo{} + } + return pkgInfo{ + purl, + pkg.TypeByName(purl.Type), + pkg.LanguageByName(purl.Type), + } +} + +func toSyftPackage(p *spdx.Package2_2) *pkg.Package { + info := extractPkgInfo(p) + metadataType, metadata := extractMetadata(p, info) + sP := pkg.Package{ + Type: info.typ, + Name: p.PackageName, + Version: p.PackageVersion, + Licenses: parseLicense(p.PackageLicenseDeclared), + CPEs: extractCPEs(p), + PURL: info.purl.String(), + Language: info.lang, + MetadataType: metadataType, + Metadata: metadata, + } + + sP.SetID() + + return &sP +} + +func extractMetadata(p *spdx.Package2_2, info pkgInfo) (pkg.MetadataType, interface{}) { + arch := info.qualifierValue(pkg.PURLQualifierArch) + upstreamValue := info.qualifierValue(pkg.PURLQualifierUpstream) + upstream := strings.SplitN(upstreamValue, "@", 2) + upstreamName := upstream[0] + upstreamVersion := "" + if len(upstream) > 1 { + upstreamVersion = upstream[1] + } + switch info.typ { + case pkg.ApkPkg: + return pkg.ApkMetadataType, pkg.ApkMetadata{ + Package: p.PackageName, + OriginPackage: upstreamName, + Maintainer: p.PackageSupplierPerson, + Version: p.PackageVersion, + License: p.PackageLicenseDeclared, + Architecture: arch, + URL: p.PackageHomePage, + Description: p.PackageDescription, + } + case pkg.RpmPkg: + converted, err := strconv.Atoi(info.qualifierValue(pkg.PURLQualifierEpoch)) + var epoch *int + if err != nil { + epoch = nil + } else { + epoch = &converted + } + return pkg.RpmdbMetadataType, pkg.RpmdbMetadata{ + Name: p.PackageName, + Version: p.PackageVersion, + Epoch: epoch, + Arch: arch, + SourceRpm: upstreamValue, + License: p.PackageLicenseConcluded, + Vendor: p.PackageOriginatorOrganization, + } + case pkg.DebPkg: + return pkg.DpkgMetadataType, pkg.DpkgMetadata{ + Package: p.PackageName, + Source: upstreamName, + Version: p.PackageVersion, + SourceVersion: upstreamVersion, + Architecture: arch, + Maintainer: p.PackageOriginatorPerson, + } + } + return pkg.UnknownMetadataType, nil +} + +func findPURLValue(p *spdx.Package2_2) string { + for _, r := range p.PackageExternalReferences { + if r.RefType == string(PurlExternalRefType) { + return r.Locator + } + } + return "" +} + +func extractCPEs(p *spdx.Package2_2) (cpes []pkg.CPE) { + for _, r := range p.PackageExternalReferences { + if r.RefType == string(Cpe23ExternalRefType) { + cpe, err := pkg.NewCPE(r.Locator) + if err != nil { + log.Warnf("unable to extract SPDX CPE=%q: %+v", r.Locator, err) + continue + } + cpes = append(cpes, cpe) + } + } + return cpes +} + +func parseLicense(l string) []string { + if l == NOASSERTION || l == NONE { + return nil + } + return strings.Split(l, " AND ") +} diff --git a/internal/formats/common/spdxhelpers/to_syft_model_test.go b/internal/formats/common/spdxhelpers/to_syft_model_test.go new file mode 100644 index 00000000000..9864c5bef57 --- /dev/null +++ b/internal/formats/common/spdxhelpers/to_syft_model_test.go @@ -0,0 +1,197 @@ +package spdxhelpers + +import ( + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/spdx/tools-golang/spdx" + "github.com/stretchr/testify/assert" +) + +func TestToSyftModel(t *testing.T) { + sbom, err := ToSyftModel(&spdx.Document2_2{ + CreationInfo: &spdx.CreationInfo2_2{ + SPDXVersion: "1", + DataLicense: "GPL", + SPDXIdentifier: "id-doc-1", + DocumentName: "docName", + DocumentNamespace: "docNamespace", + ExternalDocumentReferences: nil, + LicenseListVersion: "", + CreatorPersons: nil, + CreatorOrganizations: nil, + CreatorTools: nil, + Created: "", + CreatorComment: "", + DocumentComment: "", + }, + Packages: map[spdx.ElementID]*spdx.Package2_2{ + "id-pkg-1": { + PackageName: "pkg-1", + PackageSPDXIdentifier: "id-pkg-1", + PackageVersion: "5.4.3", + PackageSupplierPerson: "", + PackageSupplierOrganization: "", + PackageLicenseDeclared: "", + PackageDescription: "", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: "SECURITY", + Locator: "cpe:2.3:a:pkg-1:pkg-1:5.4.3:*:*:*:*:*:*:*", + RefType: "cpe23Type", + }, + { + Category: "SECURITY", + Locator: "cpe:2.3:a:pkg_1:pkg_1:5.4.3:*:*:*:*:*:*:*", + RefType: "cpe23Type", + }, + { + Category: "PACKAGE_MANAGER", + Locator: "pkg:alpine/pkg-1@5.4.3?arch=x86_64&upstream=p1-origin&distro=alpine-3.10.9", + RefType: "purl", + }, + }, + Files: nil, + }, + "id-pkg-2": { + PackageName: "pkg-2", + PackageSPDXIdentifier: "id-pkg-2", + PackageVersion: "7.3.1", + PackageSupplierPerson: "", + PackageSupplierOrganization: "", + PackageLicenseDeclared: "", + PackageDescription: "", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: "SECURITY", + Locator: "cpe:2.3:a:pkg-2:pkg-2:7.3.1:*:*:*:*:*:*:*", + RefType: "cpe23Type", + }, + { + Category: "SECURITY", + Locator: "cpe:2.3:a:pkg_2:pkg_2:7.3.1:*:*:*:*:*:*:*", + RefType: "cpe23Type", + }, + { + Category: "SECURITY", + Locator: "cpe:2.3:a:pkg-2:pkg_2:7.3.1:*:*:*:*:*:*:*", + RefType: "cpe23Type", + }, + { + Category: "PACKAGE_MANAGER", + Locator: "pkg:deb/pkg-2@7.3.1?arch=x86_64&upstream=p2-origin@9.1.3&distro=debian-3.10.9", + RefType: "purl", + }, + }, + Files: nil, + }, + }, + UnpackagedFiles: map[spdx.ElementID]*spdx.File2_2{}, + Relationships: []*spdx.Relationship2_2{}, + }) + + assert.NoError(t, err) + + assert.NotNil(t, sbom) + + pkgs := sbom.Artifacts.PackageCatalog.Sorted() + + assert.Len(t, pkgs, 2) + + p1 := pkgs[0] + assert.Equal(t, p1.Name, "pkg-1") + assert.Equal(t, p1.MetadataType, pkg.ApkMetadataType) + p1meta := p1.Metadata.(pkg.ApkMetadata) + assert.Equal(t, p1meta.OriginPackage, "p1-origin") + assert.Len(t, p1.CPEs, 2) + + p2 := pkgs[1] + assert.Equal(t, p2.Name, "pkg-2") + assert.Equal(t, p2.MetadataType, pkg.DpkgMetadataType) + p2meta := p2.Metadata.(pkg.DpkgMetadata) + assert.Equal(t, p2meta.Source, "p2-origin") + assert.Equal(t, p2meta.SourceVersion, "9.1.3") + assert.Len(t, p2.CPEs, 3) +} + +func Test_extractMetadata(t *testing.T) { + oneTwoThreeFour := 1234 + tests := []struct { + pkg spdx.Package2_2 + metaType pkg.MetadataType + meta interface{} + }{ + { + pkg: spdx.Package2_2{ + PackageName: "SomeDebPkg", + PackageVersion: "43.1.235", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: "PACKAGE_MANAGER", + Locator: "pkg:deb/pkg-2@7.3.1?arch=x86_64&upstream=somedebpkg-origin@9.1.3&distro=debian-3.10.9", + RefType: "purl", + }, + }, + }, + metaType: pkg.DpkgMetadataType, + meta: pkg.DpkgMetadata{ + Package: "SomeDebPkg", + Source: "somedebpkg-origin", + Version: "43.1.235", + SourceVersion: "9.1.3", + Architecture: "x86_64", + }, + }, + { + pkg: spdx.Package2_2{ + PackageName: "SomeApkPkg", + PackageVersion: "3.2.9", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: "PACKAGE_MANAGER", + Locator: "pkg:alpine/pkg-2@7.3.1?arch=x86_64&upstream=apk-origin@9.1.3&distro=alpine-3.10.9", + RefType: "purl", + }, + }, + }, + metaType: pkg.ApkMetadataType, + meta: pkg.ApkMetadata{ + Package: "SomeApkPkg", + OriginPackage: "apk-origin", + Version: "3.2.9", + Architecture: "x86_64", + }, + }, + { + pkg: spdx.Package2_2{ + PackageName: "SomeRpmPkg", + PackageVersion: "13.2.79", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: "PACKAGE_MANAGER", + Locator: "pkg:rpm/pkg-2@7.3.1?arch=x86_64&epoch=1234&upstream=some-rpm-origin-1.16.3&distro=alpine-3.10.9", + RefType: "purl", + }, + }, + }, + metaType: pkg.RpmdbMetadataType, + meta: pkg.RpmdbMetadata{ + Name: "SomeRpmPkg", + Version: "13.2.79", + Epoch: &oneTwoThreeFour, + Arch: "x86_64", + Release: "", + SourceRpm: "some-rpm-origin-1.16.3", + }, + }, + } + + for _, test := range tests { + t.Run(test.pkg.PackageName, func(t *testing.T) { + info := extractPkgInfo(&test.pkg) + metaType, meta := extractMetadata(&test.pkg, info) + assert.Equal(t, test.metaType, metaType) + assert.EqualValues(t, test.meta, meta) + }) + } +} diff --git a/internal/formats/spdx22json/decoder.go b/internal/formats/spdx22json/decoder.go new file mode 100644 index 00000000000..f037cd5f402 --- /dev/null +++ b/internal/formats/spdx22json/decoder.go @@ -0,0 +1,28 @@ +package spdx22json + +import ( + "fmt" + "io" + + "github.com/spdx/tools-golang/jsonloader" + + "github.com/anchore/syft/internal/formats/common/spdxhelpers" + "github.com/anchore/syft/syft/sbom" +) + +func decoder(reader io.Reader) (s *sbom.SBOM, err error) { + defer func() { + // The spdx tools JSON parser panics in quite a lot of situations, just handle this as a parse failure + if v := recover(); v != nil { + s = nil + err = fmt.Errorf("an error occurred during SPDX JSON document parsing: %+v", v) + } + }() + + doc, err := jsonloader.Load2_2(reader) + if err != nil { + return nil, fmt.Errorf("unable to decode spdx-json: %w", err) + } + + return spdxhelpers.ToSyftModel(doc) +} diff --git a/internal/formats/spdx22json/decoder_test.go b/internal/formats/spdx22json/decoder_test.go new file mode 100644 index 00000000000..6f718dc4d7a --- /dev/null +++ b/internal/formats/spdx22json/decoder_test.go @@ -0,0 +1,103 @@ +package spdx22json + +import ( + "fmt" + "os" + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/stretchr/testify/assert" +) + +func TestSPDXJSONDecoder(t *testing.T) { + tests := []struct { + path string + fail bool + packages []string + relationships []string + }{ + { + path: "alpine-3.10.syft.spdx.json", + packages: []string{"busybox", "libssl1.1", "ssl_client"}, + relationships: []string{"busybox", "busybox", "libssl1.1", "libssl1.1"}, + }, + { + path: "alpine-3.10.vendor.spdx.json", + packages: []string{"alpine", "busybox", "ssl_client"}, + relationships: []string{}, + }, + { + path: "example7-bin.spdx.json", + }, + { + path: "example7-go-module.spdx.json", + }, + { + path: "example7-golang.spdx.json", + }, + { + path: "example7-third-party-modules.spdx.json", + }, + { + path: "bad/example7-bin.spdx.json", + fail: true, + }, + { + path: "bad/example7-go-module.spdx.json", + fail: true, + }, + { + path: "bad/example7-golang.spdx.json", + fail: true, + }, + { + path: "bad/example7-third-party-modules.spdx.json", + fail: true, + }, + } + + for _, test := range tests { + t.Run(test.path, func(t *testing.T) { + f, err := os.Open("test-fixtures/spdx/" + test.path) + assert.NoError(t, err) + + sbom, err := decoder(f) + + if test.fail { + assert.Error(t, err) + return + } else { + assert.NoError(t, err) + } + + if test.packages != nil { + assert.Equal(t, sbom.Artifacts.PackageCatalog.PackageCount(), len(test.packages)) + + packages: + for _, pkgName := range test.packages { + for _, p := range sbom.Artifacts.PackageCatalog.Sorted() { + if p.Name == pkgName { + continue packages + } + } + assert.NoError(t, fmt.Errorf("Unable to find package: %s", pkgName)) + } + } + + if test.relationships != nil { + assert.Len(t, sbom.Relationships, len(test.relationships)) + + relationships: + for _, pkgName := range test.relationships { + for _, rel := range sbom.Relationships { + p, ok := rel.From.(*pkg.Package) + if ok && p.Name == pkgName { + continue relationships + } + } + assert.NoError(t, fmt.Errorf("Unable to find relationship: %s", pkgName)) + } + } + }) + } +} diff --git a/internal/formats/spdx22json/format.go b/internal/formats/spdx22json/format.go index 232113be8f2..468698ce40e 100644 --- a/internal/formats/spdx22json/format.go +++ b/internal/formats/spdx22json/format.go @@ -2,12 +2,12 @@ package spdx22json import "github.com/anchore/syft/syft/format" -// note: this format is LOSSY relative to the syftjson formation, which means that decoding and validation is not supported at this time +// note: this format is LOSSY relative to the syftjson format func Format() format.Format { return format.NewFormat( format.SPDXJSONOption, encoder, - nil, - nil, + decoder, + validator, ) } diff --git a/internal/formats/spdx22json/model/file.go b/internal/formats/spdx22json/model/file.go index c01764cdcba..cc039facb4a 100644 --- a/internal/formats/spdx22json/model/file.go +++ b/internal/formats/spdx22json/model/file.go @@ -1,21 +1,5 @@ package model -type FileType string - -const ( - DocumentationFileType FileType = "DOCUMENTATION" // if the file serves as documentation - ImageFileType FileType = "IMAGE" // if the file is associated with a picture image file (MIME type of image/*, e.g., .jpg, .gif) - VideoFileType FileType = "VIDEO" // if the file is associated with a video file type (MIME type of video/*) - ArchiveFileType FileType = "ARCHIVE" // if the file represents an archive (.tar, .jar, etc.) - SpdxFileType FileType = "SPDX" // if the file is an SPDX document - ApplicationFileType FileType = "APPLICATION" // if the file is associated with a specific application type (MIME type of application/*) - SourceFileType FileType = "SOURCE" // if the file is human readable source code (.c, .html, etc.) - BinaryFileType FileType = "BINARY" // if the file is a compiled object, target image or binary executable (.o, .a, etc.) - TextFileType FileType = "TEXT" // if the file is human readable text file (MIME type of text/*) - AudioFileType FileType = "AUDIO" // if the file is associated with an audio file (MIME type of audio/* , e.g. .mp3) - OtherFileType FileType = "OTHER" // if the file doesn't fit into the above categories (generated artifacts, data files, etc.) -) - type File struct { Item // (At least one is required.) The checksum property provides a mechanism that can be used to verify that the diff --git a/internal/formats/spdx22json/model/package.go b/internal/formats/spdx22json/model/package.go index 19f512d5568..a4808e7e21e 100644 --- a/internal/formats/spdx22json/model/package.go +++ b/internal/formats/spdx22json/model/package.go @@ -1,5 +1,7 @@ package model +import "github.com/anchore/syft/internal/formats/common/spdxhelpers" + type Package struct { Item // The checksum property provides a mechanism that can be used to verify that the contents of a File or @@ -14,7 +16,7 @@ type Package struct { DownloadLocation string `json:"downloadLocation,omitempty"` // An External Reference allows a Package to reference an external source of additional information, metadata, // enumerations, asset identifiers, or downloadable content believed to be relevant to the Package. - ExternalRefs []ExternalRef `json:"externalRefs,omitempty"` + ExternalRefs []spdxhelpers.ExternalRef `json:"externalRefs,omitempty"` // Indicates whether the file content of this package has been available for or subjected to analysis when // creating the SPDX document. If false indicates packages that represent metadata or URI references to a // project, product, artifact, distribution or a component. If set to false, the package must not contain any files diff --git a/internal/formats/spdx22json/model/relationship.go b/internal/formats/spdx22json/model/relationship.go index 51c52233b14..9c806c41c12 100644 --- a/internal/formats/spdx22json/model/relationship.go +++ b/internal/formats/spdx22json/model/relationship.go @@ -1,183 +1,13 @@ package model +import "github.com/anchore/syft/internal/formats/common/spdxhelpers" + type Relationship struct { // Id to which the SPDX element is related SpdxElementID string `json:"spdxElementId"` // Describes the type of relationship between two SPDX elements. - RelationshipType RelationshipType `json:"relationshipType"` + RelationshipType spdxhelpers.RelationshipType `json:"relationshipType"` // SPDX ID for SpdxElement. A related SpdxElement. RelatedSpdxElement string `json:"relatedSpdxElement"` Comment string `json:"comment,omitempty"` } - -// source: https://spdx.github.io/spdx-spec/7-relationships-between-SPDX-elements/ -type RelationshipType string - -const ( - // DescribedByRelationship is to be used when SPDXRef-A is described by SPDXREF-Document. - // Example: The package 'WildFly' is described by SPDX document WildFly.spdx. - DescribedByRelationship RelationshipType = "DESCRIBED_BY" - - // ContainsRelationship is to be used when SPDXRef-A contains SPDXRef-B. - // Example: An ARCHIVE file bar.tgz contains a SOURCE file foo.c. - ContainsRelationship RelationshipType = "CONTAINS" - - // ContainedByRelationship is to be used when SPDXRef-A is contained by SPDXRef-B. - // Example: A SOURCE file foo.c is contained by ARCHIVE file bar.tgz - ContainedByRelationship RelationshipType = "CONTAINED_BY" - - // DependsOnRelationship is to be used when SPDXRef-A depends on SPDXRef-B. - // Example: Package A depends on the presence of package B in order to build and run - DependsOnRelationship RelationshipType = "DEPENDS_ON" - - // DependencyOfRelationship is to be used when SPDXRef-A is dependency of SPDXRef-B. - // Example: A is explicitly stated as a dependency of B in a machine-readable file. Use when a package manager does not define scopes. - DependencyOfRelationship RelationshipType = "DEPENDENCY_OF" - - // DependencyManifestOfRelationship is to be used when SPDXRef-A is a manifest file that lists a set of dependencies for SPDXRef-B. - // Example: A file package.json is the dependency manifest of a package foo. Note that only one manifest should be used to define the same dependency graph. - DependencyManifestOfRelationship RelationshipType = "DEPENDENCY_MANIFEST_OF" - - // BuildDependencyOfRelationship is to be used when SPDXRef-A is a build dependency of SPDXRef-B. - // Example: A is in the compile scope of B in a Maven project. - BuildDependencyOfRelationship RelationshipType = "BUILD_DEPENDENCY_OF" - - // DevDependencyOfRelationship is to be used when SPDXRef-A is a development dependency of SPDXRef-B. - // Example: A is in the devDependencies scope of B in a Maven project. - DevDependencyOfRelationship RelationshipType = "DEV_DEPENDENCY_OF" - - // OptionalDependencyOfRelationship is to be used when SPDXRef-A is an optional dependency of SPDXRef-B. - // Example: Use when building the code will proceed even if a dependency cannot be found, fails to install, or is only installed on a specific platform. For example, A is in the optionalDependencies scope of npm project B. - OptionalDependencyOfRelationship RelationshipType = "OPTIONAL_DEPENDENCY_OF" - - // ProvidedDependencyOfRelationship is to be used when SPDXRef-A is a to be provided dependency of SPDXRef-B. - // Example: A is in the provided scope of B in a Maven project, indicating that the project expects it to be provided, for instance, by the container or JDK. - ProvidedDependencyOfRelationship RelationshipType = "PROVIDED_DEPENDENCY_OF" - - // TestDependencyOfRelationship is to be used when SPDXRef-A is a test dependency of SPDXRef-B. - // Example: A is in the test scope of B in a Maven project. - TestDependencyOfRelationship RelationshipType = "TEST_DEPENDENCY_OF" - - // RuntimeDependencyOfRelationship is to be used when SPDXRef-A is a dependency required for the execution of SPDXRef-B. - // Example: A is in the runtime scope of B in a Maven project. - RuntimeDependencyOfRelationship RelationshipType = "RUNTIME_DEPENDENCY_OF" - - // ExampleOfRelationship is to be used when SPDXRef-A is an example of SPDXRef-B. - // Example: The file or snippet that illustrates how to use an application or library. - ExampleOfRelationship RelationshipType = "EXAMPLE_OF" - - // GeneratesRelationship is to be used when SPDXRef-A generates SPDXRef-B. - // Example: A SOURCE file makefile.mk generates a BINARY file a.out - GeneratesRelationship RelationshipType = "GENERATES" - - // GeneratedFromRelationship is to be used when SPDXRef-A was generated from SPDXRef-B. - // Example: A BINARY file a.out has been generated from a SOURCE file makefile.mk. A BINARY file foolib.a is generated from a SOURCE file bar.c. - GeneratedFromRelationship RelationshipType = "GENERATED_FROM" - - // AncestorOfRelationship is to be used when SPDXRef-A is an ancestor (same lineage but pre-dates) SPDXRef-B. - // Example: A SOURCE file makefile.mk is a version of the original ancestor SOURCE file 'makefile2.mk' - AncestorOfRelationship RelationshipType = "ANCESTOR_OF" - - // DescendantOfRelationship is to be used when SPDXRef-A is a descendant of (same lineage but postdates) SPDXRef-B. - // Example: A SOURCE file makefile2.mk is a descendant of the original SOURCE file 'makefile.mk' - DescendantOfRelationship RelationshipType = "DESCENDANT_OF" - - // VariantOfRelationship is to be used when SPDXRef-A is a variant of (same lineage but not clear which came first) SPDXRef-B. - // Example: A SOURCE file makefile2.mk is a variant of SOURCE file makefile.mk if they differ by some edit, but there is no way to tell which came first (no reliable date information). - VariantOfRelationship RelationshipType = "VARIANT_OF" - - // DistributionArtifactRelationship is to be used when distributing SPDXRef-A requires that SPDXRef-B also be distributed. - // Example: A BINARY file foo.o requires that the ARCHIVE file bar-sources.tgz be made available on distribution. - DistributionArtifactRelationship RelationshipType = "DISTRIBUTION_ARTIFACT" - - // PatchForRelationship is to be used when SPDXRef-A is a patch file for (to be applied to) SPDXRef-B. - // Example: A SOURCE file foo.diff is a patch file for SOURCE file foo.c. - PatchForRelationship RelationshipType = "PATCH_FOR" - - // PatchAppliedRelationship is to be used when SPDXRef-A is a patch file that has been applied to SPDXRef-B. - // Example: A SOURCE file foo.diff is a patch file that has been applied to SOURCE file 'foo-patched.c'. - PatchAppliedRelationship RelationshipType = "PATCH_APPLIED" - - // CopyOfRelationship is to be used when SPDXRef-A is an exact copy of SPDXRef-B. - // Example: A BINARY file alib.a is an exact copy of BINARY file a2lib.a. - CopyOfRelationship RelationshipType = "COPY_OF" - - // FileAddedRelationship is to be used when SPDXRef-A is a file that was added to SPDXRef-B. - // Example: A SOURCE file foo.c has been added to package ARCHIVE bar.tgz. - FileAddedRelationship RelationshipType = "FILE_ADDED" - - // FileDeletedRelationship is to be used when SPDXRef-A is a file that was deleted from SPDXRef-B. - // Example: A SOURCE file foo.diff has been deleted from package ARCHIVE bar.tgz. - FileDeletedRelationship RelationshipType = "FILE_DELETED" - - // FileModifiedRelationship is to be used when SPDXRef-A is a file that was modified from SPDXRef-B. - // Example: A SOURCE file foo.c has been modified from SOURCE file foo.orig.c. - FileModifiedRelationship RelationshipType = "FILE_MODIFIED" - - // ExpandedFromArchiveRelationship is to be used when SPDXRef-A is expanded from the archive SPDXRef-B. - // Example: A SOURCE file foo.c, has been expanded from the archive ARCHIVE file xyz.tgz. - ExpandedFromArchiveRelationship RelationshipType = "EXPANDED_FROM_ARCHIVE" - - // DynamicLinkRelationship is to be used when SPDXRef-A dynamically links to SPDXRef-B. - // Example: An APPLICATION file 'myapp' dynamically links to BINARY file zlib.so. - DynamicLinkRelationship RelationshipType = "DYNAMIC_LINK" - - // StaticLinkRelationship is to be used when SPDXRef-A statically links to SPDXRef-B. - // Example: An APPLICATION file 'myapp' statically links to BINARY zlib.a. - StaticLinkRelationship RelationshipType = "STATIC_LINK" - - // DataFileOfRelationship is to be used when SPDXRef-A is a data file used in SPDXRef-B. - // Example: An IMAGE file 'kitty.jpg' is a data file of an APPLICATION 'hellokitty'. - DataFileOfRelationship RelationshipType = "DATA_FILE_OF" - - // TestCaseOfRelationship is to be used when SPDXRef-A is a test case used in testing SPDXRef-B. - // Example: A SOURCE file testMyCode.java is a unit test file used to test an APPLICATION MyPackage. - TestCaseOfRelationship RelationshipType = "TEST_CASE_OF" - - // BuildToolOfRelationship is to be used when SPDXRef-A is used to build SPDXRef-B. - // Example: A SOURCE file makefile.mk is used to build an APPLICATION 'zlib'. - BuildToolOfRelationship RelationshipType = "BUILD_TOOL_OF" - - // DevToolOfRelationship is to be used when SPDXRef-A is used as a development tool for SPDXRef-B. - // Example: Any tool used for development such as a code debugger. - DevToolOfRelationship RelationshipType = "DEV_TOOL_OF" - - // TestOfRelationship is to be used when SPDXRef-A is used for testing SPDXRef-B. - // Example: Generic relationship for cases where it's clear that something is used for testing but unclear whether it's TEST_CASE_OF or TEST_TOOL_OF. - TestOfRelationship RelationshipType = "TEST_OF" - - // TestToolOfRelationship is to be used when SPDXRef-A is used as a test tool for SPDXRef-B. - // Example: Any tool used to test the code such as ESlint. - TestToolOfRelationship RelationshipType = "TEST_TOOL_OF" - - // DocumentationOfRelationship is to be used when SPDXRef-A provides documentation of SPDXRef-B. - // Example: A DOCUMENTATION file readme.txt documents the APPLICATION 'zlib'. - DocumentationOfRelationship RelationshipType = "DOCUMENTATION_OF" - - // OptionalComponentOfRelationship is to be used when SPDXRef-A is an optional component of SPDXRef-B. - // Example: A SOURCE file fool.c (which is in the contributors directory) may or may not be included in the build of APPLICATION 'atthebar'. - OptionalComponentOfRelationship RelationshipType = "OPTIONAL_COMPONENT_OF" - - // MetafileOfRelationship is to be used when SPDXRef-A is a metafile of SPDXRef-B. - // Example: A SOURCE file pom.xml is a metafile of the APPLICATION 'Apache Xerces'. - MetafileOfRelationship RelationshipType = "METAFILE_OF" - - // PackageOfRelationship is to be used when SPDXRef-A is used as a package as part of SPDXRef-B. - // Example: A Linux distribution contains an APPLICATION package gawk as part of the distribution MyLinuxDistro. - PackageOfRelationship RelationshipType = "PACKAGE_OF" - - // AmendsRelationship is to be used when (current) SPDXRef-DOCUMENT amends the SPDX information in SPDXRef-B. - // Example: (Current) SPDX document A version 2 contains a correction to a previous version of the SPDX document A version 1. Note the reserved identifier SPDXRef-DOCUMENT for the current document is required. - AmendsRelationship RelationshipType = "AMENDS" - - // PrerequisiteForRelationship is to be used when SPDXRef-A is a prerequisite for SPDXRef-B. - // Example: A library bar.dll is a prerequisite or dependency for APPLICATION foo.exe - PrerequisiteForRelationship RelationshipType = "PREREQUISITE_FOR" - - // HasPrerequisiteRelationship is to be used when SPDXRef-A has as a prerequisite SPDXRef-B. - // Example: An APPLICATION foo.exe has prerequisite or dependency on bar.dll - HasPrerequisiteRelationship RelationshipType = "HAS_PREREQUISITE" - - // OtherRelationship is to be used for a relationship which has not been defined in the formal SPDX specification. A description of the relationship should be included in the Relationship comments field. - OtherRelationship RelationshipType = "OTHER" -) diff --git a/internal/formats/spdx22json/test-fixtures/spdx/alpine-3.10.syft.spdx.json b/internal/formats/spdx22json/test-fixtures/spdx/alpine-3.10.syft.spdx.json new file mode 100644 index 00000000000..652aaeca430 --- /dev/null +++ b/internal/formats/spdx22json/test-fixtures/spdx/alpine-3.10.syft.spdx.json @@ -0,0 +1,170 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "name": "alpine-3.10", + "spdxVersion": "SPDX-2.2", + "creationInfo": { + "created": "2022-01-20T21:40:24.439211Z", + "creators": [ + "Organization: Anchore, Inc", + "Tool: syft-[not provided]" + ], + "licenseListVersion": "3.15" + }, + "dataLicense": "CC0-1.0", + "documentNamespace": "https://anchore.com/syft/image/alpine-3.10-204b304b-beb3-4413-9b38-d8a2e58e3dfb", + "packages": [ + { + "SPDXID": "SPDXRef-a61243292e73923", + "name": "busybox", + "licenseConcluded": "GPL-2.0", + "description": "Size optimized toolbox of many common UNIX utilities", + "downloadLocation": "https://busybox.net/", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:busybox:busybox:1.30.1-r5:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceLocator": "pkg:alpine/busybox@1.30.1-r5?arch=x86_64&distro=alpine-3.10.9", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseDeclared": "GPL-2.0", + "originator": "Person: Natanael Copa ", + "sourceInfo": "acquired package info from APK DB: /lib/apk/db/installed", + "versionInfo": "1.30.1-r5" + }, + { + "SPDXID": "SPDXRef-d2f55e316dbe92e4", + "name": "libssl1.1", + "licenseConcluded": "OpenSSL", + "description": "SSL shared libraries", + "downloadLocation": "https://www.openssl.org", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:libssl1.1:libssl1.1:1.1.1k-r0:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceLocator": "pkg:alpine/libssl1.1@1.1.1k-r0?arch=x86_64&distro=alpine-3.10.9", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseDeclared": "OpenSSL", + "originator": "Person: Timo Teras ", + "sourceInfo": "acquired package info from APK DB: /lib/apk/db/installed", + "versionInfo": "1.1.1k-r0" + }, + { + "SPDXID": "SPDXRef-2b24657ad7aaafea", + "name": "ssl_client", + "licenseConcluded": "GPL-2.0", + "description": "EXternal ssl_client for busybox wget", + "downloadLocation": "https://busybox.net/", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:ssl-client:ssl-client:1.30.1-r5:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:ssl-client:ssl_client:1.30.1-r5:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:ssl_client:ssl-client:1.30.1-r5:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:ssl_client:ssl_client:1.30.1-r5:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:ssl:ssl-client:1.30.1-r5:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:ssl:ssl_client:1.30.1-r5:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceLocator": "pkg:alpine/ssl_client@1.30.1-r5?arch=x86_64&upstream=busybox&distro=alpine-3.10.9", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseDeclared": "GPL-2.0", + "originator": "Person: Natanael Copa ", + "sourceInfo": "acquired package info from APK DB: /lib/apk/db/installed", + "versionInfo": "1.30.1-r5" + } + ], + "files": [ + { + "SPDXID": "SPDXRef-a07392483a2d0750", + "comment": "layerID: sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635", + "licenseConcluded": "NOASSERTION", + "fileName": "/bin/busybox" + }, + { + "SPDXID": "SPDXRef-aa3cfed221706d80", + "comment": "layerID: sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635", + "licenseConcluded": "NOASSERTION", + "fileName": "/lib/libssl.so.1.1" + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-a61243292e73923", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-a07392483a2d0750" + }, + { + "spdxElementId": "SPDXRef-a61243292e73923", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-a07392483a2d0750" + }, + { + "spdxElementId": "SPDXRef-a61243292e73923", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-499bb68237b0f2b8" + }, + { + "spdxElementId": "SPDXRef-a61243292e73923", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-df78c68c8206be69" + }, + { + "spdxElementId": "SPDXRef-a61243292e73923", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-7c980486fc17af43" + }, + { + "spdxElementId": "SPDXRef-a61243292e73923", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-8762661e65166719" + }, + { + "spdxElementId": "SPDXRef-d2f55e316dbe92e4", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-aa3cfed221706d80" + }, + { + "spdxElementId": "SPDXRef-d2f55e316dbe92e4", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-aa3cfed221706d80" + } + ] +} diff --git a/internal/formats/spdx22json/test-fixtures/spdx/alpine-3.10.vendor.spdx.json b/internal/formats/spdx22json/test-fixtures/spdx/alpine-3.10.vendor.spdx.json new file mode 100644 index 00000000000..4cf87637081 --- /dev/null +++ b/internal/formats/spdx22json/test-fixtures/spdx/alpine-3.10.vendor.spdx.json @@ -0,0 +1,78 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "spdxVersion": "SPDX-2.2", + "creationInfo": { + "created": "2022-01-18T22:16:16Z", + "creators": [ + "Tool: vendor" + ], + "licenseListVersion": "3.8" + }, + "name": "alpine:3.10", + "dataLicense": "CC0-1.0", + "documentNamespace": "https://spdx.org/spdxdocs/alpine-154c794c-4264-4e9f-a2ff-5c01bfbfc02c", + "documentDescribes": [ + "SPDXRef-alpine-3.10" + ], + "packages": [ + { + "name": "alpine", + "SPDXID": "SPDXRef-alpine-3.10", + "versionInfo": "3.10", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + }, + { + "name": "busybox", + "SPDXID": "SPDXRef-busybox-1.30.1-r5", + "versionInfo": "1.30.1-r5", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "LicenseRef-7d19a72", + "copyrightText": "NONE", + "comment": "busybox:\n\twarning: No metadata for key: copyright\n\twarning: No metadata for key: download_url\n\twarning: No metadata for key: checksum\n\twarning: No metadata for key: pkg_licenses\n\twarning: No metadata for key: pkg_format\n\twarning: No metadata for key: src_name\n\twarning: No metadata for key: src_version\n" + }, + { + "name": "ssl_client", + "SPDXID": "SPDXRef-ssl_client-1.30.1-r5", + "versionInfo": "1.30.1-r5", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "LicenseRef-de5acdd", + "copyrightText": "NONE", + "comment": "ssl_client:\n\twarning: No metadata for key: copyright\n\twarning: No metadata for key: download_url\n\twarning: No metadata for key: checksum\n\twarning: No metadata for key: pkg_licenses\n\twarning: No metadata for key: pkg_format\n\twarning: No metadata for key: src_name\n\twarning: No metadata for key: src_version\n" + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-alpine-3.10", + "relationshipType": "DESCRIBES" + }, + { + "spdxElementId": "SPDXRef-9fb3aa2f8b", + "relatedSpdxElement": "SPDXRef-busybox-1.30.1-r5", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-9fb3aa2f8b", + "relatedSpdxElement": "SPDXRef-ssl_client-1.30.1-r5", + "relationshipType": "CONTAINS" + } + ], + "hasExtractedLicensingInfos": [ + { + "extractedText": "OpenSSL", + "licenseId": "LicenseRef-de5acdd" + }, + { + "extractedText": "GPL-2.0", + "licenseId": "LicenseRef-7d19a72" + } + ] +} diff --git a/internal/formats/spdx22json/test-fixtures/spdx/bad/example7-bin.spdx.json b/internal/formats/spdx22json/test-fixtures/spdx/bad/example7-bin.spdx.json new file mode 100644 index 00000000000..0d4ad527911 --- /dev/null +++ b/internal/formats/spdx22json/test-fixtures/spdx/bad/example7-bin.spdx.json @@ -0,0 +1,72 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ + { + "Person": "Nisha K (nishak@vmware.com)" + } + ] + }, + "name": "hello-go-binary.spdx.json", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/hello-go-binary", + "externalDocumentRefs": [ + { + "externalDocumentId": "DocumentRef-hello-go-module", + "checksum": { + "algorithm": "SHA1", + "checksumValue": "d661f8f831a99c288a64e5843b4794ad5181224a" + }, + "spdxDocument": "https://swinslow.net/spdx-examples/example7/hello-go-module-cfa0c58d-79db-4860-99b6-258477e4838b" + }, + { + "externalDocumentId": "DocumentRef-golang-dist", + "checksum": { + "algorithm": "SHA1", + "checksumValue": "b6cf54a46329e7cc7610aa5d244018b80103d111" + }, + "spdxDocument": "https://swinslow.net/spdx-examples/example7/golang-dist-492dfde4-318b-49f7-b48c-934bfafbde48" + }, + { + "externalDocumentId": "DocumentRef-hello-imports", + "checksum": { + "algorithm": "SHA1", + "checksumValue": "14ff98203c3ddd2bd4803c00b5225d2551ca603c" + }, + "spdxDocument": "https://swinslow.net/spdx-examples/example7/hello-imports-c2d068df-67aa-4c68-98c8-100b450fc408" + } + ], + "documentDescribes": [ + "SPDXRef-go-bin-hello" + ], + "packages": [ + { + "packageName": "hello", + "SPDXID": "SPDXRef-go-bin-hello", + "downloadLocation": "git@github.com:swinslow/spdx-examples.git#example7/content/build/hello", + "filesAnalyzed": "false", + "packageLicenseConcluded": "NOASSERTION", + "packageLicenseDeclared": "NOASSERTION", + "packageCopyrightText": "NOASSERTION" + } + ], + "relationships": [ + { + "spdxElementId": "DocumentRef-golang-dist", + "relatedSpdxElement": "DocumentRef-hello-go-module", + "relationshipType": "BUILD_TOOL_OF" + }, + { + "spdxElementId": "DocumentRef-golang-dist:SPDXRef-go-compiler", + "relatedSpdxElement": "SPDXRef-go-bin-hello", + "relationshipType": "GENERATES" + }, + { + "spdxElementId": "DocumentRef-hello-imports", + "relatedSpdxElement": "SPDXRef-go-bin-hello", + "relationshipType": "STATIC_LINK" + } + ] +} diff --git a/internal/formats/spdx22json/test-fixtures/spdx/bad/example7-go-module.spdx.json b/internal/formats/spdx22json/test-fixtures/spdx/bad/example7-go-module.spdx.json new file mode 100644 index 00000000000..9d0e2e8bbae --- /dev/null +++ b/internal/formats/spdx22json/test-fixtures/spdx/bad/example7-go-module.spdx.json @@ -0,0 +1,29 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ + { + "Person": "Nisha K (nishak@vmware.com)" + } + ] + }, + "name": "hello-go-module.spdx.json", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/hello-go-module", + "documentDescribes": [ + "SPDXRef-go-module-example.com/hello" + ], + "packages": [ + { + "packageName": "example.com/hello", + "SPDXID": "SPDXRef-go-module-example.com/hello", + "downloadLocation": "git@github.com:swinslow/spdx-examples.git#example7/content/src/hello", + "filesAnalyzed": "false", + "packageLicenseConcluded": "NOASSERTION", + "packageLicenseDeclared": "NOASSERTION", + "packageCopyrightText": "NOASSERTION" + } + ] +} diff --git a/internal/formats/spdx22json/test-fixtures/spdx/bad/example7-golang.spdx.json b/internal/formats/spdx22json/test-fixtures/spdx/bad/example7-golang.spdx.json new file mode 100644 index 00000000000..9c1dc5c392c --- /dev/null +++ b/internal/formats/spdx22json/test-fixtures/spdx/bad/example7-golang.spdx.json @@ -0,0 +1,46 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ + { + "Person": "Nisha K (nishak@vmware.com)" + } + ] + }, + "name": "golang-dist", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/golang-dist", + "documentDescribes": [ + "SPDXRef-golang-dist" + ], + "packages": [ + { + "packageName": "go1.16.4.linux-amd64", + "SPDXID": "SPDXRef-golang-dist", + "downloadLocation": "https://golang.org/dl/go1.16.4.linux-amd64.tar.gz", + "packageVersion": "1.16.4", + "filesAnalyzed": "false", + "checksums": [ + { + "algorithm": "SHA256", + "checksumValue": "7154e88f5a8047aad4b80ebace58a059e36e7e2e4eb3b383127a28c711b4ff59" + } + ], + "packageLicenseConcluded": "NOASSERTION", + "packageLicenseDeclared": "LicenseRef-Golang-BSD-plus-Patents", + "packageCopyrightText": "Copyright (c) 2009 The Go Authors. All rights reserved." + }, + { + "packageName": "go", + "SPDXID": "SPDXRef-go-compiler", + "downloadLocation": "https://golang.org/dl/go1.16.4.linux-amd64.tar.gz", + "packageVersion": "1.16.4", + "filesAnalyzed": "false", + "packageLicenseConcluded": "NOASSERTION", + "packageLicenseDeclared": "NOASSERTION", + "packageCopyrightText": "NOASSERTION" + } + ] +} diff --git a/internal/formats/spdx22json/test-fixtures/spdx/bad/example7-third-party-modules.spdx.json b/internal/formats/spdx22json/test-fixtures/spdx/bad/example7-third-party-modules.spdx.json new file mode 100644 index 00000000000..626ad04a266 --- /dev/null +++ b/internal/formats/spdx22json/test-fixtures/spdx/bad/example7-third-party-modules.spdx.json @@ -0,0 +1,49 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ + { + "Person": "Nisha K (nishak@vmware.com)" + } + ] + }, + "name": "hello-imports.spdx.json", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/hello-imports", + "documentDescribes": [ + "SPDXRef-go-module-golang.org/x/text", + "SPDXRef-go-module-rsc.io/quote", + "SPDXRef-go-module-rsc.io/sampler" + ], + "packages": [ + { + "packageName": "golang.org/x/text", + "SPDXID": "SPDXRef-go-module-golang.org/x/text", + "downloadLocation": "go://golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c", + "filesAnalyzed": "false", + "packageLicenseConcluded": "NOASSERTION", + "packageLicenseDeclared": "NOASSERTION", + "packageCopyrightText": "NOASSERTION" + }, + { + "packageName": "rsc.io/quote", + "SPDXID": "SPDXRef-go-module-rsc.io/quote", + "downloadLocation": "go://rsc.io/quote@v1.5.2", + "filesAnalyzed": "false", + "packageLicenseConcluded": "NOASSERTION", + "packageLicenseDeclared": "NOASSERTION", + "packageCopyrightText": "NOASSERTION" + }, + { + "packageName": "rsc.io/sampler", + "SPDXID": "SPDXRef-go-module-rsc.io/sampler", + "downloadLocation": "go://rsc.io/sampler@v1.3.0", + "filesAnalyzed": "false", + "packageLicenseConcluded": "NOASSERTION", + "packageLicenseDeclared": "NOASSERTION", + "packageCopyrightText": "NOASSERTION" + } + ] +} diff --git a/internal/formats/spdx22json/test-fixtures/spdx/example7-bin.spdx.json b/internal/formats/spdx22json/test-fixtures/spdx/example7-bin.spdx.json new file mode 100644 index 00000000000..563b40a785f --- /dev/null +++ b/internal/formats/spdx22json/test-fixtures/spdx/example7-bin.spdx.json @@ -0,0 +1,68 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ "Person: Nisha K (nishak@vmware.com)" ] + }, + "name": "hello-go-binary.spdx.json", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/hello-go-binary", + "externalDocumentRefs": [ + { + "externalDocumentId": "DocumentRef-hello-go-module", + "checksum": { + "algorithm": "SHA1", + "checksumValue": "d661f8f831a99c288a64e5843b4794ad5181224a" + }, + "spdxDocument": "https://swinslow.net/spdx-examples/example7/hello-go-module-cfa0c58d-79db-4860-99b6-258477e4838b" + }, + { + "externalDocumentId": "DocumentRef-golang-dist", + "checksum": { + "algorithm": "SHA1", + "checksumValue": "b6cf54a46329e7cc7610aa5d244018b80103d111" + }, + "spdxDocument": "https://swinslow.net/spdx-examples/example7/golang-dist-492dfde4-318b-49f7-b48c-934bfafbde48" + }, + { + "externalDocumentId": "DocumentRef-hello-imports", + "checksum": { + "algorithm": "SHA1", + "checksumValue": "14ff98203c3ddd2bd4803c00b5225d2551ca603c" + }, + "spdxDocument": "https://swinslow.net/spdx-examples/example7/hello-imports-c2d068df-67aa-4c68-98c8-100b450fc408" + } + ], + "documentDescribes": [ + "SPDXRef-go-bin-hello" + ], + "packages": [ + { + "name": "hello", + "SPDXID": "SPDXRef-go-bin-hello", + "downloadLocation": "git@github.com:swinslow/spdx-examples.git#example7/content/build/hello", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + } + ], + "relationships": [ + { + "spdxElementId": "DocumentRef-golang-dist:SPDXRef-golang-dist", + "relatedSpdxElement": "DocumentRef-hello-go-module:SPDXRef-hello-go-module", + "relationshipType": "BUILD_TOOL_OF" + }, + { + "spdxElementId": "DocumentRef-golang-dist:SPDXRef-go-compiler", + "relatedSpdxElement": "SPDXRef-go-bin-hello", + "relationshipType": "GENERATES" + }, + { + "spdxElementId": "DocumentRef-hello-imports:SPDXRef-hello-imports", + "relatedSpdxElement": "SPDXRef-go-bin-hello", + "relationshipType": "STATIC_LINK" + } + ] +} diff --git a/internal/formats/spdx22json/test-fixtures/spdx/example7-go-module.spdx.json b/internal/formats/spdx22json/test-fixtures/spdx/example7-go-module.spdx.json new file mode 100644 index 00000000000..682b0642510 --- /dev/null +++ b/internal/formats/spdx22json/test-fixtures/spdx/example7-go-module.spdx.json @@ -0,0 +1,25 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ "Person: Nisha K (nishak@vmware.com)" ] + }, + "name": "hello-go-module.spdx.json", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/hello-go-module", + "documentDescribes": [ + "SPDXRef-go-module-example.com/hello" + ], + "packages": [ + { + "name": "example.com/hello", + "SPDXID": "SPDXRef-go-module-example.com/hello", + "downloadLocation": "git@github.com:swinslow/spdx-examples.git#example7/content/src/hello", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + } + ] +} diff --git a/internal/formats/spdx22json/test-fixtures/spdx/example7-golang.spdx.json b/internal/formats/spdx22json/test-fixtures/spdx/example7-golang.spdx.json new file mode 100644 index 00000000000..e9800309975 --- /dev/null +++ b/internal/formats/spdx22json/test-fixtures/spdx/example7-golang.spdx.json @@ -0,0 +1,42 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ "Person: Nisha K (nishak@vmware.com)" ] + }, + "name": "golang-dist", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/golang-dist", + "documentDescribes": [ + "SPDXRef-golang-dist" + ], + "packages": [ + { + "name": "go1.16.4.linux-amd64", + "SPDXID": "SPDXRef-golang-dist", + "downloadLocation": "https://golang.org/dl/go1.16.4.linux-amd64.tar.gz", + "versionInfo": "1.16.4", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "checksumValue": "7154e88f5a8047aad4b80ebace58a059e36e7e2e4eb3b383127a28c711b4ff59" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "LicenseRef-Golang-BSD-plus-Patents", + "copyrightText": "Copyright (c) 2009 The Go Authors. All rights reserved." + }, + { + "name": "go", + "SPDXID": "SPDXRef-go-compiler", + "downloadLocation": "https://golang.org/dl/go1.16.4.linux-amd64.tar.gz", + "versionInfo": "1.16.4", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + } + ] +} diff --git a/internal/formats/spdx22json/test-fixtures/spdx/example7-third-party-modules.spdx.json b/internal/formats/spdx22json/test-fixtures/spdx/example7-third-party-modules.spdx.json new file mode 100644 index 00000000000..fa7ce80a3ec --- /dev/null +++ b/internal/formats/spdx22json/test-fixtures/spdx/example7-third-party-modules.spdx.json @@ -0,0 +1,45 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ "Person: Nisha K (nishak@vmware.com)" ] + }, + "name": "hello-imports.spdx.json", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/hello-imports", + "documentDescribes": [ + "SPDXRef-go-module-golang.org/x/text", + "SPDXRef-go-module-rsc.io/quote", + "SPDXRef-go-module-rsc.io/sampler" + ], + "packages": [ + { + "name": "golang.org/x/text", + "SPDXID": "SPDXRef-go-module-golang.org/x/text", + "downloadLocation": "go://golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + }, + { + "name": "rsc.io/quote", + "SPDXID": "SPDXRef-go-module-rsc.io/quote", + "downloadLocation": "go://rsc.io/quote@v1.5.2", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + }, + { + "name": "rsc.io/sampler", + "SPDXID": "SPDXRef-go-module-rsc.io/sampler", + "downloadLocation": "go://rsc.io/sampler@v1.3.0", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + } + ] +} diff --git a/internal/formats/spdx22json/to_format_model.go b/internal/formats/spdx22json/to_format_model.go index bd721c17e57..ab5d6348e36 100644 --- a/internal/formats/spdx22json/to_format_model.go +++ b/internal/formats/spdx22json/to_format_model.go @@ -167,28 +167,28 @@ func toFileTypes(metadata *source.FileMetadata) (ty []string) { mimeTypePrefix := strings.Split(metadata.MIMEType, "/")[0] switch mimeTypePrefix { case "image": - ty = append(ty, string(model.ImageFileType)) + ty = append(ty, string(spdxhelpers.ImageFileType)) case "video": - ty = append(ty, string(model.VideoFileType)) + ty = append(ty, string(spdxhelpers.VideoFileType)) case "application": - ty = append(ty, string(model.ApplicationFileType)) + ty = append(ty, string(spdxhelpers.ApplicationFileType)) case "text": - ty = append(ty, string(model.TextFileType)) + ty = append(ty, string(spdxhelpers.TextFileType)) case "audio": - ty = append(ty, string(model.AudioFileType)) + ty = append(ty, string(spdxhelpers.AudioFileType)) } if internal.IsExecutable(metadata.MIMEType) { - ty = append(ty, string(model.BinaryFileType)) + ty = append(ty, string(spdxhelpers.BinaryFileType)) } if internal.IsArchive(metadata.MIMEType) { - ty = append(ty, string(model.ArchiveFileType)) + ty = append(ty, string(spdxhelpers.ArchiveFileType)) } // TODO: add support for source, spdx, and documentation file types if len(ty) == 0 { - ty = append(ty, string(model.OtherFileType)) + ty = append(ty, string(spdxhelpers.OtherFileType)) } return ty @@ -213,12 +213,12 @@ func toRelationships(relationships []artifact.Relationship) (result []model.Rela return result } -func lookupRelationship(ty artifact.RelationshipType) (bool, model.RelationshipType, string) { +func lookupRelationship(ty artifact.RelationshipType) (bool, spdxhelpers.RelationshipType, string) { switch ty { case artifact.ContainsRelationship: - return true, model.ContainsRelationship, "" + return true, spdxhelpers.ContainsRelationship, "" case artifact.OwnershipByFileOverlapRelationship: - return true, model.OtherRelationship, fmt.Sprintf("%s: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by", ty) + return true, spdxhelpers.OtherRelationship, fmt.Sprintf("%s: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by", ty) } return false, "", "" } diff --git a/internal/formats/spdx22json/to_format_model_test.go b/internal/formats/spdx22json/to_format_model_test.go index cea544c28bb..59eb38f2ed3 100644 --- a/internal/formats/spdx22json/to_format_model_test.go +++ b/internal/formats/spdx22json/to_format_model_test.go @@ -9,6 +9,7 @@ import ( "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/internal/formats/common/spdxhelpers" "github.com/anchore/syft/internal/formats/spdx22json/model" "github.com/anchore/syft/syft/source" "github.com/stretchr/testify/assert" @@ -27,7 +28,7 @@ func Test_toFileTypes(t *testing.T) { MIMEType: "application/vnd.unknown", }, expected: []string{ - string(model.ApplicationFileType), + string(spdxhelpers.ApplicationFileType), }, }, { @@ -36,8 +37,8 @@ func Test_toFileTypes(t *testing.T) { MIMEType: "application/zip", }, expected: []string{ - string(model.ApplicationFileType), - string(model.ArchiveFileType), + string(spdxhelpers.ApplicationFileType), + string(spdxhelpers.ArchiveFileType), }, }, { @@ -46,7 +47,7 @@ func Test_toFileTypes(t *testing.T) { MIMEType: "audio/ogg", }, expected: []string{ - string(model.AudioFileType), + string(spdxhelpers.AudioFileType), }, }, { @@ -55,7 +56,7 @@ func Test_toFileTypes(t *testing.T) { MIMEType: "video/3gpp", }, expected: []string{ - string(model.VideoFileType), + string(spdxhelpers.VideoFileType), }, }, { @@ -64,7 +65,7 @@ func Test_toFileTypes(t *testing.T) { MIMEType: "text/html", }, expected: []string{ - string(model.TextFileType), + string(spdxhelpers.TextFileType), }, }, { @@ -73,7 +74,7 @@ func Test_toFileTypes(t *testing.T) { MIMEType: "image/png", }, expected: []string{ - string(model.ImageFileType), + string(spdxhelpers.ImageFileType), }, }, { @@ -82,8 +83,8 @@ func Test_toFileTypes(t *testing.T) { MIMEType: "application/x-sharedlib", }, expected: []string{ - string(model.ApplicationFileType), - string(model.BinaryFileType), + string(spdxhelpers.ApplicationFileType), + string(spdxhelpers.BinaryFileType), }, }, } @@ -99,18 +100,18 @@ func Test_lookupRelationship(t *testing.T) { tests := []struct { input artifact.RelationshipType exists bool - ty model.RelationshipType + ty spdxhelpers.RelationshipType comment string }{ { input: artifact.ContainsRelationship, exists: true, - ty: model.ContainsRelationship, + ty: spdxhelpers.ContainsRelationship, }, { input: artifact.OwnershipByFileOverlapRelationship, exists: true, - ty: model.OtherRelationship, + ty: spdxhelpers.OtherRelationship, comment: "ownership-by-file-overlap: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by", }, { diff --git a/internal/formats/spdx22json/validator.go b/internal/formats/spdx22json/validator.go new file mode 100644 index 00000000000..47366b1de5c --- /dev/null +++ b/internal/formats/spdx22json/validator.go @@ -0,0 +1,10 @@ +package spdx22json + +import ( + "io" +) + +func validator(reader io.Reader) error { + _, err := decoder(reader) + return err +} diff --git a/internal/formats/spdx22tagvalue/decoder.go b/internal/formats/spdx22tagvalue/decoder.go new file mode 100644 index 00000000000..adce71c3604 --- /dev/null +++ b/internal/formats/spdx22tagvalue/decoder.go @@ -0,0 +1,20 @@ +package spdx22tagvalue + +import ( + "fmt" + "io" + + "github.com/spdx/tools-golang/tvloader" + + "github.com/anchore/syft/internal/formats/common/spdxhelpers" + "github.com/anchore/syft/syft/sbom" +) + +func decoder(reader io.Reader) (*sbom.SBOM, error) { + doc, err := tvloader.Load2_2(reader) + if err != nil { + return nil, fmt.Errorf("unable to decode spdx-json: %w", err) + } + + return spdxhelpers.ToSyftModel(doc) +} diff --git a/internal/formats/spdx22tagvalue/format.go b/internal/formats/spdx22tagvalue/format.go index 33214b3998e..4e9a83bee01 100644 --- a/internal/formats/spdx22tagvalue/format.go +++ b/internal/formats/spdx22tagvalue/format.go @@ -7,7 +7,7 @@ func Format() format.Format { return format.NewFormat( format.SPDXTagValueOption, encoder, - nil, - nil, + decoder, + validator, ) } diff --git a/internal/formats/spdx22tagvalue/to_format_model.go b/internal/formats/spdx22tagvalue/to_format_model.go index f1dc9ff436c..c240eeaa762 100644 --- a/internal/formats/spdx22tagvalue/to_format_model.go +++ b/internal/formats/spdx22tagvalue/to_format_model.go @@ -183,9 +183,11 @@ func toFormatPackages(catalog *pkg.Catalog) map[spdx.ElementID]*spdx.Package2_2 // note: based on the purpose above no discovered checksums should be provided, but instead, only // tool-derived checksums. - PackageChecksumSHA1: "", - PackageChecksumSHA256: "", - PackageChecksumMD5: "", + //FIXME: this got removed between 0.1.0 and 0.2.0, is this right? it looks like + // it wasn't being used anyway + //PackageChecksumSHA1: "", + //PackageChecksumSHA256: "", + //PackageChecksumMD5: "", // 3.11: Package Home Page // Cardinality: optional, one diff --git a/internal/formats/spdx22tagvalue/validator.go b/internal/formats/spdx22tagvalue/validator.go new file mode 100644 index 00000000000..1a5c078bc33 --- /dev/null +++ b/internal/formats/spdx22tagvalue/validator.go @@ -0,0 +1,10 @@ +package spdx22tagvalue + +import ( + "io" +) + +func validator(reader io.Reader) error { + _, err := decoder(reader) + return err +} diff --git a/internal/formats/syftjson/to_syft_model.go b/internal/formats/syftjson/to_syft_model.go index 23a2af90863..9b6f0aa642e 100644 --- a/internal/formats/syftjson/to_syft_model.go +++ b/internal/formats/syftjson/to_syft_model.go @@ -3,6 +3,7 @@ package syftjson import ( "github.com/anchore/syft/internal/formats/syftjson/model" "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" @@ -11,13 +12,16 @@ import ( ) func toSyftModel(doc model.Document) (*sbom.SBOM, error) { + catalog := toSyftCatalog(doc.Artifacts) + return &sbom.SBOM{ Artifacts: sbom.Artifacts{ - PackageCatalog: toSyftCatalog(doc.Artifacts), + PackageCatalog: catalog, LinuxDistribution: toSyftLinuxRelease(doc.Distro), }, - Source: *toSyftSourceData(doc.Source), - Descriptor: toSyftDescriptor(doc.Descriptor), + Source: *toSyftSourceData(doc.Source), + Descriptor: toSyftDescriptor(doc.Descriptor), + Relationships: toSyftRelationships(&doc, catalog, doc.ArtifactRelationships), }, nil } @@ -42,6 +46,59 @@ func toSyftLinuxRelease(d model.LinuxRelease) *linux.Release { } } +func toSyftRelationships(doc *model.Document, catalog *pkg.Catalog, relationships []model.Relationship) []artifact.Relationship { + idMap := make(map[string]interface{}) + + for _, p := range catalog.Sorted() { + idMap[string(p.ID())] = p + for _, l := range p.Locations { + idMap[string(l.Coordinates.ID())] = l.Coordinates + } + } + + for _, f := range doc.Files { + idMap[f.ID] = f.Location + } + + var out []artifact.Relationship + for _, r := range relationships { + syftRelationship := toSyftRelationship(idMap, r) + if syftRelationship != nil { + out = append(out, *syftRelationship) + } + } + return out +} + +func toSyftRelationship(idMap map[string]interface{}, relationship model.Relationship) *artifact.Relationship { + from, ok := idMap[relationship.Parent].(artifact.Identifiable) + if !ok { + log.Warnf("relationship mapping from key %s is not a valid artifact.Identifiable type: %+v", relationship.Parent, idMap[relationship.Parent]) + return nil + } + to, ok := idMap[relationship.Child].(artifact.Identifiable) + if !ok { + log.Warnf("relationship mapping to key %s is not a valid artifact.Identifiable type: %+v", relationship.Child, idMap[relationship.Child]) + return nil + } + typ := artifact.RelationshipType(relationship.Type) + + switch typ { + case artifact.OwnershipByFileOverlapRelationship: + fallthrough + case artifact.ContainsRelationship: + default: + log.Warnf("unknown relationship type: %s", typ) + return nil + } + return &artifact.Relationship{ + From: from, + To: to, + Type: typ, + Data: relationship.Metadata, + } +} + func toSyftDescriptor(d model.Descriptor) sbom.Descriptor { return sbom.Descriptor{ Name: d.Name, diff --git a/syft/pkg/apk_metadata.go b/syft/pkg/apk_metadata.go index 866c080ee1d..843b5a77f78 100644 --- a/syft/pkg/apk_metadata.go +++ b/syft/pkg/apk_metadata.go @@ -50,11 +50,11 @@ type ApkFileRecord struct { // PackageURL returns the PURL for the specific Alpine package (see https://github.com/package-url/purl-spec) func (m ApkMetadata) PackageURL(distro *linux.Release) string { qualifiers := map[string]string{ - purlArchQualifier: m.Architecture, + PURLQualifierArch: m.Architecture, } if m.OriginPackage != "" { - qualifiers[purlUpstreamQualifier] = m.OriginPackage + qualifiers[PURLQualifierUpstream] = m.OriginPackage } return packageurl.NewPackageURL( diff --git a/syft/pkg/dpkg_metadata.go b/syft/pkg/dpkg_metadata.go index 93f6cab5e2b..0efa2287e12 100644 --- a/syft/pkg/dpkg_metadata.go +++ b/syft/pkg/dpkg_metadata.go @@ -46,14 +46,14 @@ func (m DpkgMetadata) PackageURL(distro *linux.Release) string { } qualifiers := map[string]string{ - purlArchQualifier: m.Architecture, + PURLQualifierArch: m.Architecture, } if m.Source != "" { if m.SourceVersion != "" { - qualifiers[purlUpstreamQualifier] = fmt.Sprintf("%s@%s", m.Source, m.SourceVersion) + qualifiers[PURLQualifierUpstream] = fmt.Sprintf("%s@%s", m.Source, m.SourceVersion) } else { - qualifiers[purlUpstreamQualifier] = m.Source + qualifiers[PURLQualifierUpstream] = m.Source } } diff --git a/syft/pkg/language.go b/syft/pkg/language.go index ce6f106231e..e0168f4759a 100644 --- a/syft/pkg/language.go +++ b/syft/pkg/language.go @@ -39,7 +39,11 @@ func LanguageFromPURL(p string) Language { return UnknownLanguage } - switch purl.Type { + return LanguageByName(purl.Type) +} + +func LanguageByName(name string) Language { + switch name { case packageurl.TypeMaven, purlGradlePkgType: return Java case packageurl.TypeComposer: diff --git a/syft/pkg/python_package_metadata.go b/syft/pkg/python_package_metadata.go index 125b9a4c28c..de5664d48c4 100644 --- a/syft/pkg/python_package_metadata.go +++ b/syft/pkg/python_package_metadata.go @@ -106,7 +106,7 @@ func (p PythonDirectURLOriginInfo) vcsURLQualifier() packageurl.Qualifiers { if p.VCS != "" { // Taken from https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#known-qualifiers-keyvalue-pairs // packageurl-go still doesn't support all qualifier names - return packageurl.Qualifiers{{Key: purlVCSURLQualifier, Value: fmt.Sprintf("%s+%s@%s", p.VCS, p.URL, p.CommitID)}} + return packageurl.Qualifiers{{Key: PURLQualifierVCSURL, Value: fmt.Sprintf("%s+%s@%s", p.VCS, p.URL, p.CommitID)}} } return nil } diff --git a/syft/pkg/rpmdb_metadata.go b/syft/pkg/rpmdb_metadata.go index 10e20433412..055dd21b394 100644 --- a/syft/pkg/rpmdb_metadata.go +++ b/syft/pkg/rpmdb_metadata.go @@ -56,15 +56,15 @@ func (m RpmdbMetadata) PackageURL(distro *linux.Release) string { } qualifiers := map[string]string{ - purlArchQualifier: m.Arch, + PURLQualifierArch: m.Arch, } if m.Epoch != nil { - qualifiers[purlEpochQualifier] = strconv.Itoa(*m.Epoch) + qualifiers[PURLQualifierEpoch] = strconv.Itoa(*m.Epoch) } if m.SourceRpm != "" { - qualifiers[purlUpstreamQualifier] = m.SourceRpm + qualifiers[PURLQualifierUpstream] = m.SourceRpm } return packageurl.NewPackageURL( diff --git a/syft/pkg/type.go b/syft/pkg/type.go index 4d4f8d4c71f..396faf522da 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -73,7 +73,11 @@ func TypeFromPURL(p string) Type { return UnknownPkg } - switch purl.Type { + return TypeByName(purl.Type) +} + +func TypeByName(name string) Type { + switch name { case packageurl.TypeDebian, "deb": return DebPkg case packageurl.TypeRPM: diff --git a/syft/pkg/url.go b/syft/pkg/url.go index 1bab84b45b9..7bce378554b 100644 --- a/syft/pkg/url.go +++ b/syft/pkg/url.go @@ -11,13 +11,13 @@ import ( ) const ( - purlArchQualifier = "arch" - purlDistroQualifier = "distro" - purlEpochQualifier = "epoch" - purlVCSURLQualifier = "vcs_url" + PURLQualifierArch = "arch" + PURLQualifierDistro = "distro" + PURLQualifierEpoch = "epoch" + PURLQualifierVCSURL = "vcs_url" - // this qualifier is not in the pURL spec, but is used by grype to perform indirect matching based on source information - purlUpstreamQualifier = "upstream" + // PURLQualifierUpstream this qualifier is not in the pURL spec, but is used by grype to perform indirect matching based on source information + PURLQualifierUpstream = "upstream" purlCargoPkgType = "cargo" purlGradlePkgType = "gradle" @@ -83,7 +83,7 @@ func purlQualifiers(vars map[string]string, release *linux.Release) (q packageur if release != nil && release.ID != "" && release.VersionID != "" { q = append(q, packageurl.Qualifier{ - Key: purlDistroQualifier, + Key: PURLQualifierDistro, Value: fmt.Sprintf("%s-%s", release.ID, release.VersionID), }) }