package spdxhelpers import ( "reflect" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/spdx/tools-golang/spdx" "github.com/spdx/tools-golang/spdx/v2/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) func TestToSyftModel(t *testing.T) { sbom, err := ToSyftModel(&spdx.Document{ SPDXVersion: "1", DataLicense: "GPL", SPDXIdentifier: "id-doc-1", DocumentName: "docName", DocumentNamespace: "docNamespace", ExternalDocumentReferences: nil, DocumentComment: "", CreationInfo: &spdx.CreationInfo{ LicenseListVersion: "", Created: "", CreatorComment: "", }, Packages: []*spdx.Package{ { PackageName: "pkg-1", PackageSPDXIdentifier: "id-pkg-1", PackageVersion: "5.4.3", PackageLicenseDeclared: "", PackageDescription: "", PackageExternalReferences: []*spdx.PackageExternalReference{ { 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:apk/alpine/pkg-1@5.4.3?arch=x86_64&upstream=p1-origin&distro=alpine-3.10.9", RefType: "purl", }, }, Files: nil, }, { PackageName: "pkg-2", PackageSPDXIdentifier: "id-pkg-2", PackageVersion: "7.3.1", PackageLicenseDeclared: "", PackageDescription: "", PackageExternalReferences: []*spdx.PackageExternalReference{ { 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, }, }, Relationships: []*spdx.Relationship{}, }) assert.NoError(t, err) assert.NotNil(t, sbom) pkgs := sbom.Artifacts.Packages.Sorted() assert.Len(t, pkgs, 2) p1 := pkgs[0] assert.Equal(t, p1.Name, "pkg-1") p1meta := p1.Metadata.(pkg.ApkDBEntry) assert.Equal(t, p1meta.OriginPackage, "p1-origin") assert.Len(t, p1.CPEs, 2) p2 := pkgs[1] assert.Equal(t, p2.Name, "pkg-2") p2meta := p2.Metadata.(pkg.DpkgDBEntry) 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.Package meta any }{ { pkg: spdx.Package{ PackageName: "SomeDebPkg", PackageVersion: "43.1.235", PackageExternalReferences: []*spdx.PackageExternalReference{ { 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", }, }, }, meta: pkg.DpkgDBEntry{ Package: "SomeDebPkg", Source: "somedebpkg-origin", Version: "43.1.235", SourceVersion: "9.1.3", Architecture: "x86_64", }, }, { pkg: spdx.Package{ PackageName: "SomeApkPkg", PackageVersion: "3.2.9", PackageExternalReferences: []*spdx.PackageExternalReference{ { Category: "PACKAGE-MANAGER", Locator: "pkg:apk/alpine/pkg-2@7.3.1?arch=x86_64&upstream=apk-origin@9.1.3&distro=alpine-3.10.9", RefType: "purl", }, }, }, meta: pkg.ApkDBEntry{ Package: "SomeApkPkg", OriginPackage: "apk-origin", Version: "3.2.9", Architecture: "x86_64", }, }, { pkg: spdx.Package{ PackageName: "SomeRpmPkg", PackageVersion: "13.2.79", PackageExternalReferences: []*spdx.PackageExternalReference{ { 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", }, }, }, meta: pkg.RpmDBEntry{ 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) meta := extractMetadata(&test.pkg, info) assert.EqualValues(t, test.meta, meta) }) } } func TestExtractSourceFromNamespaces(t *testing.T) { tests := []struct { namespace string expected any }{ { namespace: "https://anchore.com/syft/file/d42b01d0-7325-409b-b03f-74082935c4d3", expected: source.FileMetadata{}, }, { namespace: "https://anchore.com/syft/image/d42b01d0-7325-409b-b03f-74082935c4d3", expected: source.ImageMetadata{}, }, { namespace: "https://anchore.com/syft/dir/d42b01d0-7325-409b-b03f-74082935c4d3", expected: source.DirectoryMetadata{}, }, { namespace: "https://another-host/blob/123", expected: nil, }, { namespace: "bla bla", expected: nil, }, { namespace: "", expected: nil, }, } for _, tt := range tests { desc := extractSourceFromNamespace(tt.namespace) if tt.expected == nil && desc.Metadata == nil { return } if tt.expected != nil && desc.Metadata == nil { t.Fatal("expected metadata but got nil") } if tt.expected == nil && desc.Metadata != nil { t.Fatal("expected nil metadata but got something") } require.Equal(t, reflect.TypeOf(tt.expected), reflect.TypeOf(desc.Metadata)) } } func TestH1Digest(t *testing.T) { tests := []struct { name string pkg spdx.Package expectedDigest string }{ { name: "valid h1digest", pkg: spdx.Package{ PackageName: "github.com/googleapis/gnostic", PackageVersion: "v0.5.5", PackageExternalReferences: []*spdx.PackageExternalReference{ { Category: "PACKAGE-MANAGER", Locator: "pkg:golang/github.com/googleapis/gnostic@v0.5.5", RefType: "purl", }, }, PackageChecksums: []spdx.Checksum{ { Algorithm: spdx.SHA256, Value: "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c", }, }, }, expectedDigest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", }, { name: "invalid h1digest algorithm", pkg: spdx.Package{ PackageName: "github.com/googleapis/gnostic", PackageVersion: "v0.5.5", PackageExternalReferences: []*spdx.PackageExternalReference{ { Category: "PACKAGE-MANAGER", Locator: "pkg:golang/github.com/googleapis/gnostic@v0.5.5", RefType: "purl", }, }, PackageChecksums: []spdx.Checksum{ { Algorithm: spdx.SHA1, Value: "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c", }, }, }, expectedDigest: "", }, { name: "invalid h1digest digest", pkg: spdx.Package{ PackageName: "github.com/googleapis/gnostic", PackageVersion: "v0.5.5", PackageExternalReferences: []*spdx.PackageExternalReference{ { Category: "PACKAGE-MANAGER", Locator: "pkg:golang/github.com/googleapis/gnostic@v0.5.5", RefType: "purl", }, }, PackageChecksums: []spdx.Checksum{ { Algorithm: spdx.SHA256, Value: "", }, }, }, expectedDigest: "", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { p := toSyftPackage(&test.pkg) meta := p.Metadata.(pkg.GolangBinaryBuildinfoEntry) require.Equal(t, test.expectedDigest, meta.H1Digest) }) } } func Test_toSyftRelationships(t *testing.T) { type args struct { spdxIDMap map[string]any doc *spdx.Document } pkg1 := pkg.Package{ Name: "github.com/googleapis/gnostic", Version: "v0.5.5", } pkg1.SetID() pkg2 := pkg.Package{ Name: "rfc3339", Version: "1.2", Type: pkg.RpmPkg, } pkg2.SetID() pkg3 := pkg.Package{ Name: "rfc3339", Version: "1.2", Type: pkg.PythonPkg, } pkg3.SetID() loc1 := file.NewLocationFromCoordinates(file.NewCoordinates("/somewhere/real", "abc")) tests := []struct { name string args args want []artifact.Relationship }{ { name: "evident-by relationship", args: args{ spdxIDMap: map[string]any{ string(toSPDXID(pkg1)): pkg1, string(toSPDXID(loc1)): loc1, }, doc: &spdx.Document{ Relationships: []*spdx.Relationship{ { RefA: common.DocElementID{ ElementRefID: toSPDXID(pkg1), }, RefB: common.DocElementID{ ElementRefID: toSPDXID(loc1), }, Relationship: spdx.RelationshipOther, RelationshipComment: "evident-by: indicates the package's existence is evident by the given file", }, }, }, }, want: []artifact.Relationship{ { From: pkg1, To: loc1, Type: artifact.EvidentByRelationship, }, }, }, { name: "ownership-by-file-overlap relationship", args: args{ spdxIDMap: map[string]any{ string(toSPDXID(pkg2)): pkg2, string(toSPDXID(pkg3)): pkg3, }, doc: &spdx.Document{ Relationships: []*spdx.Relationship{ { RefA: common.DocElementID{ ElementRefID: toSPDXID(pkg2), }, RefB: common.DocElementID{ ElementRefID: toSPDXID(pkg3), }, Relationship: spdx.RelationshipOther, RelationshipComment: "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", }, }, }, }, want: []artifact.Relationship{ { From: pkg2, To: pkg3, Type: artifact.OwnershipByFileOverlapRelationship, }, }, }, { name: "dependency-of relationship", args: args{ spdxIDMap: map[string]any{ string(toSPDXID(pkg2)): pkg2, string(toSPDXID(pkg3)): pkg3, }, doc: &spdx.Document{ Relationships: []*spdx.Relationship{ { RefA: common.DocElementID{ ElementRefID: toSPDXID(pkg2), }, RefB: common.DocElementID{ ElementRefID: toSPDXID(pkg3), }, Relationship: spdx.RelationshipDependencyOf, RelationshipComment: "dependency-of: indicates that the package in RefA is a dependency of the package in RefB", }, }, }, }, want: []artifact.Relationship{ { From: pkg2, To: pkg3, Type: artifact.DependencyOfRelationship, }, }, }, { name: "dependends-on relationship", args: args{ spdxIDMap: map[string]any{ string(toSPDXID(pkg2)): pkg2, string(toSPDXID(pkg3)): pkg3, }, doc: &spdx.Document{ Relationships: []*spdx.Relationship{ { RefA: common.DocElementID{ ElementRefID: toSPDXID(pkg3), }, RefB: common.DocElementID{ ElementRefID: toSPDXID(pkg2), }, Relationship: spdx.RelationshipDependsOn, RelationshipComment: "dependends-on: indicates that the package in RefA depends on the package in RefB", }, }, }, }, want: []artifact.Relationship{ { From: pkg2, To: pkg3, Type: artifact.DependencyOfRelationship, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := toSyftRelationships(tt.args.spdxIDMap, tt.args.doc) require.Len(t, actual, len(tt.want)) for i := range actual { require.Equal(t, tt.want[i].From.ID(), actual[i].From.ID()) require.Equal(t, tt.want[i].To.ID(), actual[i].To.ID()) require.Equal(t, tt.want[i].Type, actual[i].Type) } }) } } func Test_convertToAndFromFormat(t *testing.T) { packages := []pkg.Package{ { Name: "pkg1", }, { Name: "pkg2", }, } for i := range packages { (&packages[i]).SetID() } relationships := []artifact.Relationship{ { From: packages[0], To: packages[1], Type: artifact.ContainsRelationship, }, } tests := []struct { name string source source.Description packages []pkg.Package relationships []artifact.Relationship }{ { name: "image source", source: source.Description{ ID: "DocumentRoot-Image-some-image", Metadata: source.ImageMetadata{ ID: "DocumentRoot-Image-some-image", UserInput: "some-image:some-tag", ManifestDigest: "sha256:ab8b83234bc28f28d8e", }, Name: "some-image", Version: "some-tag", Supplier: "some-supplier", }, packages: packages, relationships: relationships, }, { name: ". directory source with supplier", source: source.Description{ ID: "DocumentRoot-Directory-.", Name: ".", Supplier: "some-supplier", Metadata: source.DirectoryMetadata{ Path: ".", }, }, packages: packages, relationships: relationships, }, { name: "directory source without supplier", source: source.Description{ ID: "DocumentRoot-Directory-my-app", Name: "my-app", Metadata: source.DirectoryMetadata{ Path: "my-app", }, }, packages: packages, relationships: relationships, }, { name: "file source", source: source.Description{ ID: "DocumentRoot-File-my-app.exe", Metadata: source.FileMetadata{ Path: "my-app.exe", Digests: []file.Digest{ { Algorithm: "sha256", Value: "3723cae0b8b83234bc28f28d8e", }, }, }, Name: "my-app.exe", }, packages: packages, relationships: relationships, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { src := &test.source s := sbom.SBOM{ Source: *src, Artifacts: sbom.Artifacts{ Packages: pkg.NewCollection(test.packages...), }, Relationships: test.relationships, } doc := ToFormatModel(s) got, err := ToSyftModel(doc) require.NoError(t, err) if diff := cmp.Diff(&s, got, cmpopts.IgnoreUnexported(artifact.Relationship{}), cmpopts.IgnoreUnexported(file.LocationSet{}), cmpopts.IgnoreUnexported(pkg.Collection{}), cmpopts.IgnoreUnexported(pkg.Package{}), cmpopts.IgnoreUnexported(pkg.LicenseSet{}), cmpopts.IgnoreFields(sbom.Artifacts{}, "FileMetadata", "FileDigests"), ); diff != "" { t.Fatalf("packages do not match:\n%s", diff) } }) } } func Test_purlValue(t *testing.T) { tests := []struct { purl packageurl.PackageURL expected string }{ { purl: packageurl.PackageURL{}, expected: "", }, { purl: packageurl.PackageURL{ Name: "name", Version: "version", }, expected: "", }, { purl: packageurl.PackageURL{ Type: "typ", Version: "version", }, expected: "", }, { purl: packageurl.PackageURL{ Type: "typ", Name: "name", Version: "version", }, expected: "pkg:typ/name@version", }, { purl: packageurl.PackageURL{ Type: "typ", Name: "name", Version: "version", Qualifiers: packageurl.Qualifiers{ { Key: "q", Value: "v", }, }, }, expected: "pkg:typ/name@version?q=v", }, } for _, test := range tests { t.Run(test.purl.String(), func(t *testing.T) { got := purlValue(test.purl) require.Equal(t, test.expected, got) }) } } func Test_directPackageFiles(t *testing.T) { doc := &spdx.Document{ SPDXVersion: "SPDX-2.3", Packages: []*spdx.Package{ { PackageName: "some-package", PackageSPDXIdentifier: "1", // important! PackageVersion: "1.0.5", Files: []*spdx.File{ { FileName: "some-file", FileSPDXIdentifier: "2", Checksums: []spdx.Checksum{ { Algorithm: "SHA1", Value: "a8d733c64f9123", }, }, }, }, }, }, } got, err := ToSyftModel(doc) require.NoError(t, err) p := pkg.Package{ Name: "some-package", Version: "1.0.5", } p.OverrideID("1") // the same as the spdxID on the package element f := file.Location{ LocationData: file.LocationData{ Coordinates: file.NewCoordinates("some-file", ""), AccessPath: "some-file", }, LocationMetadata: file.LocationMetadata{ Annotations: map[string]string{}, }, } s := &sbom.SBOM{ Artifacts: sbom.Artifacts{ Packages: pkg.NewCollection(p), FileMetadata: map[file.Coordinates]file.Metadata{ f.Coordinates: {}, }, FileDigests: map[file.Coordinates][]file.Digest{ f.Coordinates: { { Algorithm: "sha1", Value: "a8d733c64f9123", }, }, }, }, Relationships: []artifact.Relationship{ { From: p, To: f, Type: artifact.ContainsRelationship, }, }, Source: source.Description{}, Descriptor: sbom.Descriptor{}, } require.Equal(t, s, got) } func Test_useSPDXIdentifierOverDerivedSyftArtifactID(t *testing.T) { doc := &spdx.Document{ SPDXVersion: "SPDX-2.3", Packages: []*spdx.Package{ { PackageName: "some-package", PackageSPDXIdentifier: "1", // important! PackageVersion: "1.0.5", Files: []*spdx.File{ { FileName: "some-file", FileSPDXIdentifier: "2", Checksums: []spdx.Checksum{ { Algorithm: "SHA1", Value: "a8d733c64f9123", }, }, }, }, }, }, } s, err := ToSyftModel(doc) assert.Nil(t, err) assert.NotNil(t, s.Artifacts.Packages.Package("1")) } func Test_skipsPackagesWithGeneratedFromRelationship(t *testing.T) { doc := &spdx.Document{ SPDXVersion: "SPDX-2.3", Packages: []*spdx.Package{ { PackageName: "package-1", PackageSPDXIdentifier: "1", PackageVersion: "1.0.5", }, { PackageName: "package-1-src", PackageSPDXIdentifier: "1-src", PackageVersion: "1.0.5-src", }, }, Relationships: []*spdx.Relationship{ { Relationship: spdx.RelationshipGeneratedFrom, RefA: common.DocElementID{ // package 1 ElementRefID: spdx.ElementID("1"), }, RefB: common.DocElementID{ // generated from package 1-src ElementRefID: spdx.ElementID("1-src"), }, }, }, } s, err := ToSyftModel(doc) assert.Nil(t, err) assert.NotNil(t, s.Artifacts.Packages.Package("1")) assert.Nil(t, s.Artifacts.Packages.Package("1-src")) } func Test_coordinatesFromFile_nonCanonicalChainIsDropped(t *testing.T) { // a chain the name doesn't end with leaves FullPath equal to RealPath, which must decode as a plain file got := coordinatesFromFile("/app.jar", "archivePath: other.jar!/x", "") assert.Equal(t, file.NewCoordinates("/app.jar", ""), got) nested := coordinatesFromFile("/app.war!/lib/a.jar", "archivePath: lib/a.jar", "") assert.Equal(t, file.Coordinates{RealPath: "/app.war", FullPath: "/app.war!/lib/a.jar"}, nested) }