github.com/anchore/syft@v1.38.2/syft/format/common/spdxhelpers/to_format_model_test.go (about)

     1  package spdxhelpers
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"regexp"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/google/go-cmp/cmp"
    11  	"github.com/google/go-cmp/cmp/cmpopts"
    12  	"github.com/spdx/tools-golang/spdx"
    13  	"github.com/spdx/tools-golang/spdx/v2/v2_3"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  
    17  	"github.com/anchore/syft/internal/relationship"
    18  	"github.com/anchore/syft/internal/sourcemetadata"
    19  	"github.com/anchore/syft/syft/artifact"
    20  	"github.com/anchore/syft/syft/file"
    21  	"github.com/anchore/syft/syft/format/internal/spdxutil/helpers"
    22  	"github.com/anchore/syft/syft/pkg"
    23  	"github.com/anchore/syft/syft/sbom"
    24  	"github.com/anchore/syft/syft/source"
    25  )
    26  
    27  func Test_toFormatModel(t *testing.T) {
    28  	tracker := sourcemetadata.NewCompletionTester(t)
    29  
    30  	tests := []struct {
    31  		name     string
    32  		in       sbom.SBOM
    33  		expected *spdx.Document
    34  	}{
    35  		{
    36  			name: "container",
    37  			in: sbom.SBOM{
    38  				Source: source.Description{
    39  					Name:     "alpine",
    40  					Version:  "sha256:d34db33f",
    41  					Supplier: "Alpine Linux",
    42  					Metadata: source.ImageMetadata{
    43  						UserInput:      "alpine:latest",
    44  						ManifestDigest: "sha256:d34db33f",
    45  					},
    46  				},
    47  				Artifacts: sbom.Artifacts{
    48  					Packages: pkg.NewCollection(pkg.Package{
    49  						Name:    "pkg-1",
    50  						Version: "version-1",
    51  					}),
    52  				},
    53  			},
    54  			expected: &spdx.Document{
    55  				SPDXIdentifier: "DOCUMENT",
    56  				SPDXVersion:    spdx.Version,
    57  				DataLicense:    spdx.DataLicense,
    58  				DocumentName:   "alpine",
    59  				Packages: []*spdx.Package{
    60  					{
    61  						PackageSPDXIdentifier: "Package-pkg-1-pkg-1",
    62  						PackageName:           "pkg-1",
    63  						PackageVersion:        "version-1",
    64  						PackageSupplier: &spdx.Supplier{
    65  							Supplier:     "Alpine Linux",
    66  							SupplierType: "Organization",
    67  						},
    68  					},
    69  					{
    70  						PackageSPDXIdentifier: "DocumentRoot-Image-alpine",
    71  						PackageName:           "alpine",
    72  						PackageVersion:        "sha256:d34db33f",
    73  						PrimaryPackagePurpose: "CONTAINER",
    74  						PackageChecksums:      []spdx.Checksum{{Algorithm: "SHA256", Value: "d34db33f"}},
    75  						PackageExternalReferences: []*v2_3.PackageExternalReference{
    76  							{
    77  								Category: "PACKAGE-MANAGER",
    78  								RefType:  "purl",
    79  								Locator:  "pkg:oci/alpine@sha256%3Ad34db33f?arch=&tag=latest",
    80  							},
    81  						},
    82  						PackageSupplier: &spdx.Supplier{
    83  							Supplier:     "Alpine Linux",
    84  							SupplierType: "Organization",
    85  						},
    86  					},
    87  				},
    88  				Relationships: []*spdx.Relationship{
    89  					{
    90  						RefA: spdx.DocElementID{
    91  							ElementRefID: "DocumentRoot-Image-alpine",
    92  						},
    93  						RefB: spdx.DocElementID{
    94  							ElementRefID: "Package-pkg-1-pkg-1",
    95  						},
    96  						Relationship: spdx.RelationshipContains,
    97  					},
    98  					{
    99  						RefA: spdx.DocElementID{
   100  							ElementRefID: "DOCUMENT",
   101  						},
   102  						RefB: spdx.DocElementID{
   103  							ElementRefID: "DocumentRoot-Image-alpine",
   104  						},
   105  						Relationship: spdx.RelationshipDescribes,
   106  					},
   107  				},
   108  			},
   109  		},
   110  		{
   111  			name: "directory",
   112  			in: sbom.SBOM{
   113  				Source: source.Description{
   114  					Name: "some/directory",
   115  					Metadata: source.DirectoryMetadata{
   116  						Path: "some/directory",
   117  					},
   118  				},
   119  				Artifacts: sbom.Artifacts{
   120  					Packages: pkg.NewCollection(pkg.Package{
   121  						Name:    "pkg-1",
   122  						Version: "version-1",
   123  					}),
   124  				},
   125  			},
   126  			expected: &spdx.Document{
   127  				SPDXIdentifier: "DOCUMENT",
   128  				SPDXVersion:    spdx.Version,
   129  				DataLicense:    spdx.DataLicense,
   130  				DocumentName:   "some/directory",
   131  
   132  				Packages: []*spdx.Package{
   133  					{
   134  						PackageSPDXIdentifier: "Package-pkg-1-pkg-1",
   135  						PackageName:           "pkg-1",
   136  						PackageVersion:        "version-1",
   137  						PackageSupplier: &spdx.Supplier{
   138  							Supplier: "NOASSERTION",
   139  						},
   140  					},
   141  					{
   142  						PackageSPDXIdentifier: "DocumentRoot-Directory-some-directory",
   143  						PackageName:           "some/directory",
   144  						PackageVersion:        "",
   145  						PrimaryPackagePurpose: "FILE",
   146  						PackageSupplier: &spdx.Supplier{
   147  							Supplier: "NOASSERTION",
   148  						},
   149  					},
   150  				},
   151  				Relationships: []*spdx.Relationship{
   152  					{
   153  						RefA: spdx.DocElementID{
   154  							ElementRefID: "DocumentRoot-Directory-some-directory",
   155  						},
   156  						RefB: spdx.DocElementID{
   157  							ElementRefID: "Package-pkg-1-pkg-1",
   158  						},
   159  						Relationship: spdx.RelationshipContains,
   160  					},
   161  					{
   162  						RefA: spdx.DocElementID{
   163  							ElementRefID: "DOCUMENT",
   164  						},
   165  						RefB: spdx.DocElementID{
   166  							ElementRefID: "DocumentRoot-Directory-some-directory",
   167  						},
   168  						Relationship: spdx.RelationshipDescribes,
   169  					},
   170  				},
   171  			},
   172  		},
   173  		{
   174  			name: "file",
   175  			in: sbom.SBOM{
   176  				Source: source.Description{
   177  					Name:    "path/to/some.file",
   178  					Version: "sha256:d34db33f",
   179  					Metadata: source.FileMetadata{
   180  						Path: "path/to/some.file",
   181  						Digests: []file.Digest{
   182  							{
   183  								Algorithm: "sha256",
   184  								Value:     "d34db33f",
   185  							},
   186  						},
   187  					},
   188  				},
   189  				Artifacts: sbom.Artifacts{
   190  					Packages: pkg.NewCollection(pkg.Package{
   191  						Name:    "pkg-1",
   192  						Version: "version-1",
   193  					}),
   194  				},
   195  			},
   196  			expected: &spdx.Document{
   197  				SPDXIdentifier: "DOCUMENT",
   198  				SPDXVersion:    spdx.Version,
   199  				DataLicense:    spdx.DataLicense,
   200  				DocumentName:   "path/to/some.file",
   201  				Packages: []*spdx.Package{
   202  					{
   203  						PackageSPDXIdentifier: "Package-pkg-1-pkg-1",
   204  						PackageName:           "pkg-1",
   205  						PackageVersion:        "version-1",
   206  						PackageSupplier: &spdx.Supplier{
   207  							Supplier: "NOASSERTION",
   208  						},
   209  					},
   210  					{
   211  						PackageSPDXIdentifier: "DocumentRoot-File-path-to-some.file",
   212  						PackageName:           "path/to/some.file",
   213  						PackageVersion:        "sha256:d34db33f",
   214  						PrimaryPackagePurpose: "FILE",
   215  						PackageChecksums:      []spdx.Checksum{{Algorithm: "SHA256", Value: "d34db33f"}},
   216  						PackageSupplier: &spdx.Supplier{
   217  							Supplier: "NOASSERTION",
   218  						},
   219  					},
   220  				},
   221  				Relationships: []*spdx.Relationship{
   222  					{
   223  						RefA: spdx.DocElementID{
   224  							ElementRefID: "DocumentRoot-File-path-to-some.file",
   225  						},
   226  						RefB: spdx.DocElementID{
   227  							ElementRefID: "Package-pkg-1-pkg-1",
   228  						},
   229  						Relationship: spdx.RelationshipContains,
   230  					},
   231  					{
   232  						RefA: spdx.DocElementID{
   233  							ElementRefID: "DOCUMENT",
   234  						},
   235  						RefB: spdx.DocElementID{
   236  							ElementRefID: "DocumentRoot-File-path-to-some.file",
   237  						},
   238  						Relationship: spdx.RelationshipDescribes,
   239  					},
   240  				},
   241  			},
   242  		},
   243  		{
   244  			name: "snap",
   245  			in: sbom.SBOM{
   246  				Source: source.Description{
   247  					Name:    "etcd",
   248  					Version: "3.4.36",
   249  					Metadata: source.SnapMetadata{
   250  						Summary:     "Distributed reliable key-value store",
   251  						Base:        "core18",
   252  						Grade:       "stable",
   253  						Confinement: "strict",
   254  						Architectures: []string{
   255  							"amd64",
   256  						},
   257  						Digests: []file.Digest{
   258  							{
   259  								Algorithm: "sha256",
   260  								Value:     "d34db33f",
   261  							},
   262  						},
   263  					},
   264  				},
   265  				Artifacts: sbom.Artifacts{
   266  					Packages: pkg.NewCollection(pkg.Package{
   267  						Name:    "pkg-1",
   268  						Version: "version-1",
   269  					}),
   270  				},
   271  			},
   272  			expected: &spdx.Document{
   273  				SPDXIdentifier: "DOCUMENT",
   274  				SPDXVersion:    spdx.Version,
   275  				DataLicense:    spdx.DataLicense,
   276  				DocumentName:   "etcd",
   277  				Packages: []*spdx.Package{
   278  					{
   279  						PackageSPDXIdentifier: "Package-pkg-1-pkg-1",
   280  						PackageName:           "pkg-1",
   281  						PackageVersion:        "version-1",
   282  						PackageSupplier: &spdx.Supplier{
   283  							Supplier: "NOASSERTION",
   284  						},
   285  					},
   286  					{
   287  						PackageSPDXIdentifier: "DocumentRoot-Snap-etcd",
   288  						PackageName:           "etcd",
   289  						PackageVersion:        "3.4.36",
   290  						PrimaryPackagePurpose: "CONTAINER",
   291  						PackageChecksums:      []spdx.Checksum{{Algorithm: "SHA256", Value: "d34db33f"}},
   292  						PackageSupplier: &spdx.Supplier{
   293  							Supplier: "NOASSERTION",
   294  						},
   295  					},
   296  				},
   297  				Relationships: []*spdx.Relationship{
   298  					{
   299  						RefA: spdx.DocElementID{
   300  							ElementRefID: "DocumentRoot-Snap-etcd",
   301  						},
   302  						RefB: spdx.DocElementID{
   303  							ElementRefID: "Package-pkg-1-pkg-1",
   304  						},
   305  						Relationship: spdx.RelationshipContains,
   306  					},
   307  					{
   308  						RefA: spdx.DocElementID{
   309  							ElementRefID: "DOCUMENT",
   310  						},
   311  						RefB: spdx.DocElementID{
   312  							ElementRefID: "DocumentRoot-Snap-etcd",
   313  						},
   314  						Relationship: spdx.RelationshipDescribes,
   315  					},
   316  				},
   317  			},
   318  		},
   319  	}
   320  
   321  	for _, test := range tests {
   322  		t.Run(test.name, func(t *testing.T) {
   323  			tracker.Tested(t, test.in.Source.Metadata)
   324  
   325  			// replace IDs with package names
   326  			var pkgs []pkg.Package
   327  			for p := range test.in.Artifacts.Packages.Enumerate() {
   328  				p.OverrideID(artifact.ID(p.Name))
   329  				pkgs = append(pkgs, p)
   330  			}
   331  			test.in.Artifacts.Packages = pkg.NewCollection(pkgs...)
   332  
   333  			// convert
   334  			got := ToFormatModel(test.in)
   335  
   336  			// check differences
   337  			if diff := cmp.Diff(test.expected, got,
   338  				cmpopts.IgnoreUnexported(spdx.Document{}, spdx.Package{}),
   339  				cmpopts.IgnoreFields(spdx.Document{}, "CreationInfo", "DocumentNamespace"),
   340  				cmpopts.IgnoreFields(spdx.Package{}, "PackageDownloadLocation", "IsFilesAnalyzedTagPresent", "PackageSourceInfo", "PackageLicenseConcluded", "PackageLicenseDeclared", "PackageCopyrightText"),
   341  			); diff != "" {
   342  				t.Error(diff)
   343  			}
   344  		})
   345  	}
   346  }
   347  
   348  func Test_toPackageChecksums(t *testing.T) {
   349  	tests := []struct {
   350  		name          string
   351  		pkg           pkg.Package
   352  		expected      []spdx.Checksum
   353  		filesAnalyzed bool
   354  	}{
   355  		{
   356  			name: "Java Package",
   357  			pkg: pkg.Package{
   358  				Name:     "test",
   359  				Version:  "1.0.0",
   360  				Language: pkg.Java,
   361  				Metadata: pkg.JavaArchive{
   362  					ArchiveDigests: []file.Digest{
   363  						{
   364  							Algorithm: "sha1", // SPDX expects these to be uppercase
   365  							Value:     "1234",
   366  						},
   367  					},
   368  				},
   369  			},
   370  			expected: []spdx.Checksum{
   371  				{
   372  					Algorithm: "SHA1",
   373  					Value:     "1234",
   374  				},
   375  			},
   376  			filesAnalyzed: true,
   377  		},
   378  		{
   379  			name: "Java Package with no archive digests",
   380  			pkg: pkg.Package{
   381  				Name:     "test",
   382  				Version:  "1.0.0",
   383  				Language: pkg.Java,
   384  				Metadata: pkg.JavaArchive{
   385  					ArchiveDigests: []file.Digest{},
   386  				},
   387  			},
   388  			expected:      []spdx.Checksum{},
   389  			filesAnalyzed: false,
   390  		},
   391  		{
   392  			name: "Java Package with no metadata",
   393  			pkg: pkg.Package{
   394  				Name:     "test",
   395  				Version:  "1.0.0",
   396  				Language: pkg.Java,
   397  			},
   398  			expected:      []spdx.Checksum{},
   399  			filesAnalyzed: false,
   400  		},
   401  		{
   402  			name: "Go Binary Package",
   403  			pkg: pkg.Package{
   404  				Name:     "test",
   405  				Version:  "1.0.0",
   406  				Language: pkg.Go,
   407  				Metadata: pkg.GolangBinaryBuildinfoEntry{
   408  					H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
   409  				},
   410  			},
   411  			expected: []spdx.Checksum{
   412  				{
   413  					Algorithm: "SHA256",
   414  					Value:     "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
   415  				},
   416  			},
   417  			filesAnalyzed: false,
   418  		},
   419  		{
   420  			name: "Opam Package",
   421  			pkg: pkg.Package{
   422  				Name:     "test",
   423  				Version:  "1.0.0",
   424  				Language: pkg.Go,
   425  				Metadata: pkg.OpamPackage{
   426  					Checksums: []string{
   427  						"sha256=f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
   428  						"sha512=05a359dc8400d4ca200ff255dbd030acd33d2c4acb5020838f772c02cdb5f243f3dbafbc43a8cd51e6b5923a140f84c9e7ea25b2c0fa277bb68b996190d36e3b",
   429  					},
   430  				},
   431  			},
   432  			expected: []spdx.Checksum{
   433  				{
   434  					Algorithm: "SHA256",
   435  					Value:     "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
   436  				},
   437  				{
   438  					Algorithm: "SHA512",
   439  					Value:     "05a359dc8400d4ca200ff255dbd030acd33d2c4acb5020838f772c02cdb5f243f3dbafbc43a8cd51e6b5923a140f84c9e7ea25b2c0fa277bb68b996190d36e3b",
   440  				},
   441  			},
   442  			filesAnalyzed: false,
   443  		},
   444  		{
   445  			name: "Package with no metadata type",
   446  			pkg: pkg.Package{
   447  				Name:     "test",
   448  				Version:  "1.0.0",
   449  				Language: pkg.Java,
   450  				Metadata: struct{}{},
   451  			},
   452  			expected:      []spdx.Checksum{},
   453  			filesAnalyzed: false,
   454  		},
   455  	}
   456  
   457  	for _, test := range tests {
   458  		t.Run(test.name, func(t *testing.T) {
   459  			commonSum, filesAnalyzed := toPackageChecksums(test.pkg)
   460  			assert.ElementsMatch(t, test.expected, commonSum)
   461  			assert.Equal(t, test.filesAnalyzed, filesAnalyzed)
   462  		})
   463  	}
   464  }
   465  
   466  func Test_toFiles(t *testing.T) {
   467  	tests := []struct {
   468  		name string
   469  		in   sbom.SBOM
   470  		want spdx.File
   471  	}{
   472  		{
   473  			name: "File paths are converted to relative in final SPDX collection",
   474  			in: sbom.SBOM{
   475  				Source: source.Description{
   476  					Name:    "alpine",
   477  					Version: "sha256:d34db33f",
   478  					Metadata: source.ImageMetadata{
   479  						UserInput:      "alpine:latest",
   480  						ManifestDigest: "sha256:d34db33f",
   481  					},
   482  				},
   483  				Artifacts: sbom.Artifacts{
   484  					Packages: pkg.NewCollection(pkg.Package{
   485  						Name:    "pkg-1",
   486  						Version: "version-1",
   487  					}),
   488  					FileMetadata: map[file.Coordinates]file.Metadata{
   489  						{
   490  							RealPath:     "/some/path",
   491  							FileSystemID: "",
   492  						}: {
   493  							Path: "/some/path",
   494  						},
   495  					},
   496  				},
   497  			},
   498  			want: spdx.File{
   499  				FileName: "some/path",
   500  			},
   501  		},
   502  	}
   503  
   504  	for _, test := range tests {
   505  		files := toFiles(test.in)
   506  		got := files[0]
   507  		assert.Equal(t, test.want.FileName, got.FileName)
   508  	}
   509  }
   510  
   511  func Test_toFileTypes(t *testing.T) {
   512  
   513  	tests := []struct {
   514  		name     string
   515  		metadata file.Metadata
   516  		expected []string
   517  	}{
   518  		{
   519  			name: "application",
   520  			metadata: file.Metadata{
   521  				MIMEType: "application/vnd.unknown",
   522  			},
   523  			expected: []string{
   524  				string(helpers.ApplicationFileType),
   525  			},
   526  		},
   527  		{
   528  			name: "archive",
   529  			metadata: file.Metadata{
   530  				MIMEType: "application/zip",
   531  			},
   532  			expected: []string{
   533  				string(helpers.ApplicationFileType),
   534  				string(helpers.ArchiveFileType),
   535  			},
   536  		},
   537  		{
   538  			name: "audio",
   539  			metadata: file.Metadata{
   540  				MIMEType: "audio/ogg",
   541  			},
   542  			expected: []string{
   543  				string(helpers.AudioFileType),
   544  			},
   545  		},
   546  		{
   547  			name: "video",
   548  			metadata: file.Metadata{
   549  				MIMEType: "video/3gpp",
   550  			},
   551  			expected: []string{
   552  				string(helpers.VideoFileType),
   553  			},
   554  		},
   555  		{
   556  			name: "text",
   557  			metadata: file.Metadata{
   558  				MIMEType: "text/html",
   559  			},
   560  			expected: []string{
   561  				string(helpers.TextFileType),
   562  			},
   563  		},
   564  		{
   565  			name: "image",
   566  			metadata: file.Metadata{
   567  				MIMEType: "image/png",
   568  			},
   569  			expected: []string{
   570  				string(helpers.ImageFileType),
   571  			},
   572  		},
   573  		{
   574  			name: "binary",
   575  			metadata: file.Metadata{
   576  				MIMEType: "application/x-sharedlib",
   577  			},
   578  			expected: []string{
   579  				string(helpers.ApplicationFileType),
   580  				string(helpers.BinaryFileType),
   581  			},
   582  		},
   583  	}
   584  	for _, test := range tests {
   585  		t.Run(test.name, func(t *testing.T) {
   586  			assert.ElementsMatch(t, test.expected, toFileTypes(&test.metadata))
   587  		})
   588  	}
   589  }
   590  
   591  func Test_lookupRelationship(t *testing.T) {
   592  
   593  	tests := []struct {
   594  		input   artifact.RelationshipType
   595  		exists  bool
   596  		ty      helpers.RelationshipType
   597  		comment string
   598  	}{
   599  		{
   600  			input:  artifact.ContainsRelationship,
   601  			exists: true,
   602  			ty:     helpers.ContainsRelationship,
   603  		},
   604  		{
   605  			input:   artifact.OwnershipByFileOverlapRelationship,
   606  			exists:  true,
   607  			ty:      helpers.OtherRelationship,
   608  			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",
   609  		},
   610  		{
   611  			input:   artifact.EvidentByRelationship,
   612  			exists:  true,
   613  			ty:      helpers.OtherRelationship,
   614  			comment: "evident-by: indicates the package's existence is evident by the given file",
   615  		},
   616  		{
   617  			input:  "made-up",
   618  			exists: false,
   619  		},
   620  	}
   621  	for _, test := range tests {
   622  		t.Run(string(test.input), func(t *testing.T) {
   623  			exists, ty, comment := lookupRelationship(test.input)
   624  			assert.Equal(t, exists, test.exists)
   625  			assert.Equal(t, ty, test.ty)
   626  			assert.Equal(t, comment, test.comment)
   627  		})
   628  	}
   629  }
   630  
   631  func Test_toFileChecksums(t *testing.T) {
   632  	tests := []struct {
   633  		name     string
   634  		digests  []file.Digest
   635  		expected []spdx.Checksum
   636  	}{
   637  		{
   638  			name: "empty",
   639  		},
   640  		{
   641  			name: "has digests",
   642  			digests: []file.Digest{
   643  				{
   644  					Algorithm: "SHA256",
   645  					Value:     "deadbeefcafe",
   646  				},
   647  				{
   648  					Algorithm: "md5",
   649  					Value:     "meh",
   650  				},
   651  			},
   652  			expected: []spdx.Checksum{
   653  				{
   654  					Algorithm: "SHA256",
   655  					Value:     "deadbeefcafe",
   656  				},
   657  				{
   658  					Algorithm: "MD5",
   659  					Value:     "meh",
   660  				},
   661  			},
   662  		},
   663  	}
   664  	for _, test := range tests {
   665  		t.Run(test.name, func(t *testing.T) {
   666  			assert.ElementsMatch(t, test.expected, toFileChecksums(test.digests))
   667  		})
   668  	}
   669  }
   670  
   671  func Test_fileIDsForPackage(t *testing.T) {
   672  	p := pkg.Package{
   673  		Name: "bogus",
   674  	}
   675  
   676  	c := file.Coordinates{
   677  		RealPath:     "/path",
   678  		FileSystemID: "nowhere",
   679  	}
   680  
   681  	docElementId := func(identifiable artifact.Identifiable) spdx.DocElementID {
   682  		return spdx.DocElementID{
   683  			ElementRefID: toSPDXID(identifiable),
   684  		}
   685  	}
   686  
   687  	tests := []struct {
   688  		name          string
   689  		relationships []artifact.Relationship
   690  		expected      []*spdx.Relationship
   691  	}{
   692  		{
   693  			name: "package-to-file contains relationships",
   694  			relationships: []artifact.Relationship{
   695  				{
   696  					From: p,
   697  					To:   c,
   698  					Type: artifact.ContainsRelationship,
   699  				},
   700  			},
   701  			expected: []*spdx.Relationship{
   702  				{
   703  					Relationship: "CONTAINS",
   704  					RefA:         docElementId(p),
   705  					RefB:         docElementId(c),
   706  				},
   707  			},
   708  		},
   709  		{
   710  			name: "package-to-package",
   711  			relationships: []artifact.Relationship{
   712  				{
   713  					From: p,
   714  					To:   p,
   715  					Type: artifact.ContainsRelationship,
   716  				},
   717  			},
   718  			expected: []*spdx.Relationship{
   719  				{
   720  					Relationship: "CONTAINS",
   721  					RefA:         docElementId(p),
   722  					RefB:         docElementId(p),
   723  				},
   724  			},
   725  		},
   726  		{
   727  			name: "ignore file-to-file",
   728  			relationships: []artifact.Relationship{
   729  				{
   730  					From: c,
   731  					To:   c,
   732  					Type: artifact.ContainsRelationship,
   733  				},
   734  			},
   735  			expected: nil,
   736  		},
   737  		{
   738  			name: "ignore file-to-package",
   739  			relationships: []artifact.Relationship{
   740  				{
   741  					From: c,
   742  					To:   p,
   743  					Type: artifact.ContainsRelationship,
   744  				},
   745  			},
   746  			expected: nil,
   747  		},
   748  		{
   749  			name: "include package-to-file overlap relationships",
   750  			relationships: []artifact.Relationship{
   751  				{
   752  					From: p,
   753  					To:   c,
   754  					Type: artifact.OwnershipByFileOverlapRelationship,
   755  				},
   756  			},
   757  			expected: []*spdx.Relationship{
   758  				{
   759  					Relationship:        "OTHER",
   760  					RefA:                docElementId(p),
   761  					RefB:                docElementId(c),
   762  					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",
   763  				},
   764  			},
   765  		},
   766  	}
   767  	for _, test := range tests {
   768  		t.Run(test.name, func(t *testing.T) {
   769  			relationships := toRelationships(test.relationships)
   770  			assert.Equal(t, test.expected, relationships)
   771  		})
   772  	}
   773  }
   774  
   775  func Test_H1Digest(t *testing.T) {
   776  	s := sbom.SBOM{}
   777  	tests := []struct {
   778  		name           string
   779  		pkg            pkg.Package
   780  		expectedDigest string
   781  	}{
   782  		{
   783  			name: "valid h1digest",
   784  			pkg: pkg.Package{
   785  				Name:    "github.com/googleapis/gnostic",
   786  				Version: "v0.5.5",
   787  				Metadata: pkg.GolangBinaryBuildinfoEntry{
   788  					H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
   789  				},
   790  			},
   791  			expectedDigest: "SHA256:f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
   792  		},
   793  		{
   794  			name: "invalid h1digest",
   795  			pkg: pkg.Package{
   796  				Name:    "github.com/googleapis/gnostic",
   797  				Version: "v0.5.5",
   798  				Metadata: pkg.GolangBinaryBuildinfoEntry{
   799  					H1Digest: "h1:9fHAtK0uzzz",
   800  				},
   801  			},
   802  			expectedDigest: "",
   803  		},
   804  		{
   805  			name: "unsupported h-digest",
   806  			pkg: pkg.Package{
   807  				Name:    "github.com/googleapis/gnostic",
   808  				Version: "v0.5.5",
   809  				Metadata: pkg.GolangBinaryBuildinfoEntry{
   810  					H1Digest: "h12:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
   811  				},
   812  			},
   813  			expectedDigest: "",
   814  		},
   815  	}
   816  
   817  	for _, test := range tests {
   818  		t.Run(test.name, func(t *testing.T) {
   819  			catalog := pkg.NewCollection(test.pkg)
   820  			pkgs, _ := toPackages(relationship.NewIndex(), catalog, s)
   821  			require.Len(t, pkgs, 1)
   822  			for _, p := range pkgs {
   823  				if test.expectedDigest == "" {
   824  					require.Len(t, p.PackageChecksums, 0)
   825  				} else {
   826  					require.Len(t, p.PackageChecksums, 1)
   827  					for _, c := range p.PackageChecksums {
   828  						require.Equal(t, test.expectedDigest, fmt.Sprintf("%s:%s", c.Algorithm, c.Value))
   829  					}
   830  				}
   831  			}
   832  		})
   833  	}
   834  }
   835  
   836  func Test_OtherLicenses(t *testing.T) {
   837  	ctx := context.Background()
   838  	tests := []struct {
   839  		name     string
   840  		pkg      pkg.Package
   841  		expected []spdx.OtherLicense
   842  	}{
   843  		{
   844  			name: "no licenseRef",
   845  			pkg: pkg.Package{
   846  				Licenses: pkg.NewLicenseSet(),
   847  			},
   848  			expected: []spdx.OtherLicense{},
   849  		},
   850  		{
   851  			name: "single licenseRef",
   852  			pkg: pkg.Package{
   853  				Licenses: pkg.NewLicenseSet(
   854  					pkg.NewLicenseWithContext(ctx, "foobar"),
   855  				),
   856  			},
   857  			expected: []spdx.OtherLicense{
   858  				{
   859  					LicenseIdentifier: "LicenseRef-foobar",
   860  					LicenseName:       "foobar",
   861  					ExtractedText:     "NOASSERTION",
   862  				},
   863  			},
   864  		},
   865  		{
   866  			name: "multiple licenseRef",
   867  			pkg: pkg.Package{
   868  				Licenses: pkg.NewLicenseSet(
   869  					pkg.NewLicenseWithContext(ctx, "internal made up license name"),
   870  					pkg.NewLicenseWithContext(ctx, "new apple license 2.0"),
   871  				),
   872  			},
   873  			expected: []spdx.OtherLicense{
   874  				{
   875  					LicenseIdentifier: "LicenseRef-internal-made-up-license-name",
   876  					ExtractedText:     "NOASSERTION",
   877  					LicenseName:       "internal made up license name",
   878  				},
   879  				{
   880  					LicenseIdentifier: "LicenseRef-new-apple-license-2.0",
   881  					ExtractedText:     "NOASSERTION",
   882  					LicenseName:       "new apple license 2.0",
   883  				},
   884  			},
   885  		},
   886  		{
   887  			name: "LicenseRef as a valid spdx expression",
   888  			pkg: pkg.Package{
   889  				Licenses: pkg.NewLicenseSet(
   890  					pkg.NewLicenseWithContext(ctx, "LicenseRef-Fedora-Public-Domain"),
   891  				),
   892  			},
   893  			expected: []spdx.OtherLicense{},
   894  		},
   895  		{
   896  			name: "LicenseRef as a valid spdx expression does not otherize compound spdx expressions",
   897  			pkg: pkg.Package{
   898  				Licenses: pkg.NewLicenseSet(
   899  					pkg.NewLicenseWithContext(ctx, "(MIT AND LicenseRef-Fedora-Public-Domain)"),
   900  				),
   901  			},
   902  			expected: []spdx.OtherLicense{},
   903  		},
   904  	}
   905  
   906  	for _, test := range tests {
   907  		t.Run(test.name, func(t *testing.T) {
   908  			catalog := pkg.NewCollection(test.pkg)
   909  			rels := relationship.NewIndex()
   910  			_, otherLicenses := toPackages(rels, catalog, sbom.SBOM{})
   911  			require.Len(t, otherLicenses, len(test.expected))
   912  			require.Equal(t, test.expected, otherLicenses)
   913  		})
   914  	}
   915  }
   916  
   917  func Test_toSPDXID(t *testing.T) {
   918  	tests := []struct {
   919  		name     string
   920  		it       artifact.Identifiable
   921  		expected string
   922  	}{
   923  		{
   924  			name: "short filename",
   925  			it: file.Coordinates{
   926  				RealPath: "/short/path/file.txt",
   927  			},
   928  			expected: "File-short-path-file.txt",
   929  		},
   930  		{
   931  			name: "long filename",
   932  			it: file.Coordinates{
   933  				RealPath: "/some/long/path/with/a/lot/of-text/that-contains-a/file.txt",
   934  			},
   935  			expected: "File-...a-lot-of-text-that-contains-a-file.txt",
   936  		},
   937  		{
   938  			name: "package",
   939  			it: pkg.Package{
   940  				Type: pkg.NpmPkg,
   941  				Name: "some-package",
   942  			},
   943  			expected: "Package-npm-some-package",
   944  		},
   945  		{
   946  			name: "package with existing SPDX ID",
   947  			it: func() pkg.Package {
   948  				p := pkg.Package{
   949  					Type: pkg.NpmPkg,
   950  					Name: "some-package",
   951  				}
   952  				// SPDXRef- prefix is removed on decode (when everything is working as it should)
   953  				p.OverrideID("Package-npm-some-package-extra!")
   954  				return p
   955  			}(),
   956  			// note: we still sanitize out the "!" which is not allowed in SPDX IDs
   957  			expected: "Package-npm-some-package-extra",
   958  		},
   959  		{
   960  			name: "package with existing SPDX Ref",
   961  			it: func() pkg.Package {
   962  				p := pkg.Package{
   963  					Type: pkg.NpmPkg,
   964  					Name: "some-package",
   965  				}
   966  				// someone incorrectly added SPDXRef- prefix
   967  				p.OverrideID("SPDXRef-Package-npm-some-package-extra!")
   968  				return p
   969  			}(),
   970  			// note: we still sanitize out the "!" which is not allowed in SPDX IDs
   971  			expected: "Package-npm-some-package-extra",
   972  		},
   973  	}
   974  
   975  	for _, test := range tests {
   976  		t.Run(test.name, func(t *testing.T) {
   977  			got := string(toSPDXID(test.it))
   978  			// trim the hash
   979  			got = regexp.MustCompile(`-[a-z0-9]*$`).ReplaceAllString(got, "")
   980  			require.Equal(t, test.expected, got)
   981  		})
   982  	}
   983  }
   984  
   985  func Test_otherLicenses(t *testing.T) {
   986  	ctx := context.TODO()
   987  	pkg1 := pkg.Package{
   988  		Name:    "first-pkg",
   989  		Version: "1.1",
   990  		Licenses: pkg.NewLicenseSet(
   991  			pkg.NewLicenseWithContext(ctx, "MIT"),
   992  		),
   993  	}
   994  	pkg2 := pkg.Package{
   995  		Name:    "second-pkg",
   996  		Version: "2.2",
   997  		Licenses: pkg.NewLicenseSet(
   998  			pkg.NewLicenseWithContext(ctx, "non spdx license"),
   999  		),
  1000  	}
  1001  	bigText := `
  1002                                   Apache License
  1003                             Version 2.0, January 2004`
  1004  	pkg3 := pkg.Package{
  1005  		Name:    "third-pkg",
  1006  		Version: "3.3",
  1007  		Licenses: pkg.NewLicenseSet(
  1008  			pkg.NewLicenseWithContext(ctx, bigText),
  1009  		),
  1010  	}
  1011  
  1012  	tests := []struct {
  1013  		name     string
  1014  		packages []pkg.Package
  1015  		expected []*spdx.OtherLicense
  1016  	}{
  1017  		{
  1018  			name:     "no other licenses when all valid spdx expressions",
  1019  			packages: []pkg.Package{pkg1},
  1020  			expected: nil,
  1021  		},
  1022  		{
  1023  			name:     "other licenses must include some original text",
  1024  			packages: []pkg.Package{pkg2},
  1025  			expected: []*spdx.OtherLicense{
  1026  				{
  1027  					LicenseIdentifier: "LicenseRef-non-spdx-license",
  1028  					LicenseName:       "non spdx license",
  1029  					ExtractedText:     "NOASSERTION",
  1030  				},
  1031  			},
  1032  		},
  1033  		{
  1034  			name:     "big licenses get hashed and space is trimmed",
  1035  			packages: []pkg.Package{pkg3},
  1036  			expected: []*spdx.OtherLicense{
  1037  				{
  1038  					LicenseIdentifier: "LicenseRef-3f17782eef51ae86f18fdd6832f5918e2b40f688b52c9adc07ba6ec1024ef408",
  1039  					// Carries through the syft-json license value when we shasum large texts
  1040  					LicenseName:   "sha256:3f17782eef51ae86f18fdd6832f5918e2b40f688b52c9adc07ba6ec1024ef408",
  1041  					ExtractedText: strings.TrimSpace(bigText),
  1042  				},
  1043  			},
  1044  		},
  1045  	}
  1046  
  1047  	for _, test := range tests {
  1048  		t.Run(test.name, func(t *testing.T) {
  1049  			s := sbom.SBOM{
  1050  				Artifacts: sbom.Artifacts{
  1051  					Packages: pkg.NewCollection(test.packages...),
  1052  				},
  1053  			}
  1054  			got := ToFormatModel(s)
  1055  			require.Equal(t, test.expected, got.OtherLicenses)
  1056  		})
  1057  	}
  1058  }