github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/format/common/spdxhelpers/to_syft_model_test.go (about)

     1  package spdxhelpers
     2  
     3  import (
     4  	"reflect"
     5  	"testing"
     6  
     7  	"github.com/google/go-cmp/cmp"
     8  	"github.com/google/go-cmp/cmp/cmpopts"
     9  	"github.com/spdx/tools-golang/spdx"
    10  	"github.com/spdx/tools-golang/spdx/v2/common"
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  
    14  	"github.com/anchore/packageurl-go"
    15  	"github.com/anchore/syft/syft/artifact"
    16  	"github.com/anchore/syft/syft/file"
    17  	"github.com/anchore/syft/syft/pkg"
    18  	"github.com/anchore/syft/syft/sbom"
    19  	"github.com/anchore/syft/syft/source"
    20  )
    21  
    22  func TestToSyftModel(t *testing.T) {
    23  	sbom, err := ToSyftModel(&spdx.Document{
    24  		SPDXVersion:                "1",
    25  		DataLicense:                "GPL",
    26  		SPDXIdentifier:             "id-doc-1",
    27  		DocumentName:               "docName",
    28  		DocumentNamespace:          "docNamespace",
    29  		ExternalDocumentReferences: nil,
    30  		DocumentComment:            "",
    31  		CreationInfo: &spdx.CreationInfo{
    32  			LicenseListVersion: "",
    33  			Created:            "",
    34  			CreatorComment:     "",
    35  		},
    36  		Packages: []*spdx.Package{
    37  			{
    38  				PackageName:            "pkg-1",
    39  				PackageSPDXIdentifier:  "id-pkg-1",
    40  				PackageVersion:         "5.4.3",
    41  				PackageLicenseDeclared: "",
    42  				PackageDescription:     "",
    43  				PackageExternalReferences: []*spdx.PackageExternalReference{
    44  					{
    45  						Category: "SECURITY",
    46  						Locator:  "cpe:2.3:a:pkg-1:pkg-1:5.4.3:*:*:*:*:*:*:*",
    47  						RefType:  "cpe23Type",
    48  					},
    49  					{
    50  						Category: "SECURITY",
    51  						Locator:  "cpe:2.3:a:pkg_1:pkg_1:5.4.3:*:*:*:*:*:*:*",
    52  						RefType:  "cpe23Type",
    53  					},
    54  					{
    55  						Category: "PACKAGE-MANAGER",
    56  						Locator:  "pkg:apk/alpine/pkg-1@5.4.3?arch=x86_64&upstream=p1-origin&distro=alpine-3.10.9",
    57  						RefType:  "purl",
    58  					},
    59  				},
    60  				Files: nil,
    61  			},
    62  			{
    63  				PackageName:            "pkg-2",
    64  				PackageSPDXIdentifier:  "id-pkg-2",
    65  				PackageVersion:         "7.3.1",
    66  				PackageLicenseDeclared: "",
    67  				PackageDescription:     "",
    68  				PackageExternalReferences: []*spdx.PackageExternalReference{
    69  					{
    70  						Category: "SECURITY",
    71  						Locator:  "cpe:2.3:a:pkg-2:pkg-2:7.3.1:*:*:*:*:*:*:*",
    72  						RefType:  "cpe23Type",
    73  					},
    74  					{
    75  						Category: "SECURITY",
    76  						Locator:  "cpe:2.3:a:pkg_2:pkg_2:7.3.1:*:*:*:*:*:*:*",
    77  						RefType:  "cpe23Type",
    78  					},
    79  					{
    80  						Category: "SECURITY",
    81  						Locator:  "cpe:2.3:a:pkg-2:pkg_2:7.3.1:*:*:*:*:*:*:*",
    82  						RefType:  "cpe23Type",
    83  					},
    84  					{
    85  						Category: "PACKAGE-MANAGER",
    86  						Locator:  "pkg:deb/pkg-2@7.3.1?arch=x86_64&upstream=p2-origin@9.1.3&distro=debian-3.10.9",
    87  						RefType:  "purl",
    88  					},
    89  				},
    90  				Files: nil,
    91  			},
    92  		},
    93  		Relationships: []*spdx.Relationship{},
    94  	})
    95  
    96  	assert.NoError(t, err)
    97  
    98  	assert.NotNil(t, sbom)
    99  
   100  	pkgs := sbom.Artifacts.Packages.Sorted()
   101  
   102  	assert.Len(t, pkgs, 2)
   103  
   104  	p1 := pkgs[0]
   105  	assert.Equal(t, p1.Name, "pkg-1")
   106  	p1meta := p1.Metadata.(pkg.ApkDBEntry)
   107  	assert.Equal(t, p1meta.OriginPackage, "p1-origin")
   108  	assert.Len(t, p1.CPEs, 2)
   109  
   110  	p2 := pkgs[1]
   111  	assert.Equal(t, p2.Name, "pkg-2")
   112  	p2meta := p2.Metadata.(pkg.DpkgDBEntry)
   113  	assert.Equal(t, p2meta.Source, "p2-origin")
   114  	assert.Equal(t, p2meta.SourceVersion, "9.1.3")
   115  	assert.Len(t, p2.CPEs, 3)
   116  }
   117  
   118  func Test_extractMetadata(t *testing.T) {
   119  	oneTwoThreeFour := 1234
   120  	tests := []struct {
   121  		pkg  spdx.Package
   122  		meta interface{}
   123  	}{
   124  		{
   125  			pkg: spdx.Package{
   126  				PackageName:    "SomeDebPkg",
   127  				PackageVersion: "43.1.235",
   128  				PackageExternalReferences: []*spdx.PackageExternalReference{
   129  					{
   130  						Category: "PACKAGE-MANAGER",
   131  						Locator:  "pkg:deb/pkg-2@7.3.1?arch=x86_64&upstream=somedebpkg-origin@9.1.3&distro=debian-3.10.9",
   132  						RefType:  "purl",
   133  					},
   134  				},
   135  			},
   136  			meta: pkg.DpkgDBEntry{
   137  				Package:       "SomeDebPkg",
   138  				Source:        "somedebpkg-origin",
   139  				Version:       "43.1.235",
   140  				SourceVersion: "9.1.3",
   141  				Architecture:  "x86_64",
   142  			},
   143  		},
   144  		{
   145  			pkg: spdx.Package{
   146  				PackageName:    "SomeApkPkg",
   147  				PackageVersion: "3.2.9",
   148  				PackageExternalReferences: []*spdx.PackageExternalReference{
   149  					{
   150  						Category: "PACKAGE-MANAGER",
   151  						Locator:  "pkg:apk/alpine/pkg-2@7.3.1?arch=x86_64&upstream=apk-origin@9.1.3&distro=alpine-3.10.9",
   152  						RefType:  "purl",
   153  					},
   154  				},
   155  			},
   156  			meta: pkg.ApkDBEntry{
   157  				Package:       "SomeApkPkg",
   158  				OriginPackage: "apk-origin",
   159  				Version:       "3.2.9",
   160  				Architecture:  "x86_64",
   161  			},
   162  		},
   163  		{
   164  			pkg: spdx.Package{
   165  				PackageName:    "SomeRpmPkg",
   166  				PackageVersion: "13.2.79",
   167  				PackageExternalReferences: []*spdx.PackageExternalReference{
   168  					{
   169  						Category: "PACKAGE-MANAGER",
   170  						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",
   171  						RefType:  "purl",
   172  					},
   173  				},
   174  			},
   175  			meta: pkg.RpmDBEntry{
   176  				Name:      "SomeRpmPkg",
   177  				Version:   "13.2.79",
   178  				Epoch:     &oneTwoThreeFour,
   179  				Arch:      "x86_64",
   180  				Release:   "",
   181  				SourceRpm: "some-rpm-origin-1.16.3",
   182  			},
   183  		},
   184  	}
   185  
   186  	for _, test := range tests {
   187  		t.Run(test.pkg.PackageName, func(t *testing.T) {
   188  			info := extractPkgInfo(&test.pkg)
   189  			meta := extractMetadata(&test.pkg, info)
   190  			assert.EqualValues(t, test.meta, meta)
   191  		})
   192  	}
   193  }
   194  
   195  func TestExtractSourceFromNamespaces(t *testing.T) {
   196  	tests := []struct {
   197  		namespace string
   198  		expected  any
   199  	}{
   200  		{
   201  			namespace: "https://anchore.com/syft/file/d42b01d0-7325-409b-b03f-74082935c4d3",
   202  			expected:  source.FileSourceMetadata{},
   203  		},
   204  		{
   205  			namespace: "https://anchore.com/syft/image/d42b01d0-7325-409b-b03f-74082935c4d3",
   206  			expected:  source.StereoscopeImageSourceMetadata{},
   207  		},
   208  		{
   209  			namespace: "https://anchore.com/syft/dir/d42b01d0-7325-409b-b03f-74082935c4d3",
   210  			expected:  source.DirectorySourceMetadata{},
   211  		},
   212  		{
   213  			namespace: "https://another-host/blob/123",
   214  			expected:  nil,
   215  		},
   216  		{
   217  			namespace: "bla bla",
   218  			expected:  nil,
   219  		},
   220  		{
   221  			namespace: "",
   222  			expected:  nil,
   223  		},
   224  	}
   225  
   226  	for _, tt := range tests {
   227  		desc := extractSourceFromNamespace(tt.namespace)
   228  		if tt.expected == nil && desc.Metadata == nil {
   229  			return
   230  		}
   231  		if tt.expected != nil && desc.Metadata == nil {
   232  			t.Fatal("expected metadata but got nil")
   233  		}
   234  		if tt.expected == nil && desc.Metadata != nil {
   235  			t.Fatal("expected nil metadata but got something")
   236  		}
   237  		require.Equal(t, reflect.TypeOf(tt.expected), reflect.TypeOf(desc.Metadata))
   238  	}
   239  }
   240  
   241  func TestH1Digest(t *testing.T) {
   242  	tests := []struct {
   243  		name           string
   244  		pkg            spdx.Package
   245  		expectedDigest string
   246  	}{
   247  		{
   248  			name: "valid h1digest",
   249  			pkg: spdx.Package{
   250  				PackageName:    "github.com/googleapis/gnostic",
   251  				PackageVersion: "v0.5.5",
   252  				PackageExternalReferences: []*spdx.PackageExternalReference{
   253  					{
   254  						Category: "PACKAGE-MANAGER",
   255  						Locator:  "pkg:golang/github.com/googleapis/gnostic@v0.5.5",
   256  						RefType:  "purl",
   257  					},
   258  				},
   259  				PackageChecksums: []spdx.Checksum{
   260  					{
   261  						Algorithm: spdx.SHA256,
   262  						Value:     "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
   263  					},
   264  				},
   265  			},
   266  			expectedDigest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
   267  		},
   268  		{
   269  			name: "invalid h1digest algorithm",
   270  			pkg: spdx.Package{
   271  				PackageName:    "github.com/googleapis/gnostic",
   272  				PackageVersion: "v0.5.5",
   273  				PackageExternalReferences: []*spdx.PackageExternalReference{
   274  					{
   275  						Category: "PACKAGE-MANAGER",
   276  						Locator:  "pkg:golang/github.com/googleapis/gnostic@v0.5.5",
   277  						RefType:  "purl",
   278  					},
   279  				},
   280  				PackageChecksums: []spdx.Checksum{
   281  					{
   282  						Algorithm: spdx.SHA1,
   283  						Value:     "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
   284  					},
   285  				},
   286  			},
   287  			expectedDigest: "",
   288  		},
   289  		{
   290  			name: "invalid h1digest digest",
   291  			pkg: spdx.Package{
   292  				PackageName:    "github.com/googleapis/gnostic",
   293  				PackageVersion: "v0.5.5",
   294  				PackageExternalReferences: []*spdx.PackageExternalReference{
   295  					{
   296  						Category: "PACKAGE-MANAGER",
   297  						Locator:  "pkg:golang/github.com/googleapis/gnostic@v0.5.5",
   298  						RefType:  "purl",
   299  					},
   300  				},
   301  				PackageChecksums: []spdx.Checksum{
   302  					{
   303  						Algorithm: spdx.SHA256,
   304  						Value:     "",
   305  					},
   306  				},
   307  			},
   308  			expectedDigest: "",
   309  		},
   310  	}
   311  
   312  	for _, test := range tests {
   313  		t.Run(test.name, func(t *testing.T) {
   314  			p := toSyftPackage(&test.pkg)
   315  			meta := p.Metadata.(pkg.GolangBinaryBuildinfoEntry)
   316  			require.Equal(t, test.expectedDigest, meta.H1Digest)
   317  		})
   318  	}
   319  }
   320  
   321  func Test_toSyftRelationships(t *testing.T) {
   322  	type args struct {
   323  		spdxIDMap map[string]any
   324  		doc       *spdx.Document
   325  	}
   326  
   327  	pkg1 := pkg.Package{
   328  		Name:    "github.com/googleapis/gnostic",
   329  		Version: "v0.5.5",
   330  	}
   331  	pkg1.SetID()
   332  
   333  	pkg2 := pkg.Package{
   334  		Name:    "rfc3339",
   335  		Version: "1.2",
   336  		Type:    pkg.RpmPkg,
   337  	}
   338  	pkg2.SetID()
   339  
   340  	pkg3 := pkg.Package{
   341  		Name:    "rfc3339",
   342  		Version: "1.2",
   343  		Type:    pkg.PythonPkg,
   344  	}
   345  	pkg3.SetID()
   346  
   347  	loc1 := file.NewLocationFromCoordinates(file.Coordinates{
   348  		RealPath:     "/somewhere/real",
   349  		FileSystemID: "abc",
   350  	})
   351  
   352  	tests := []struct {
   353  		name string
   354  		args args
   355  		want []artifact.Relationship
   356  	}{
   357  		{
   358  			name: "evident-by relationship",
   359  			args: args{
   360  				spdxIDMap: map[string]any{
   361  					string(toSPDXID(pkg1)): pkg1,
   362  					string(toSPDXID(loc1)): loc1,
   363  				},
   364  				doc: &spdx.Document{
   365  					Relationships: []*spdx.Relationship{
   366  						{
   367  							RefA: common.DocElementID{
   368  								ElementRefID: toSPDXID(pkg1),
   369  							},
   370  							RefB: common.DocElementID{
   371  								ElementRefID: toSPDXID(loc1),
   372  							},
   373  							Relationship:        spdx.RelationshipOther,
   374  							RelationshipComment: "evident-by: indicates the package's existence is evident by the given file",
   375  						},
   376  					},
   377  				},
   378  			},
   379  			want: []artifact.Relationship{
   380  				{
   381  					From: pkg1,
   382  					To:   loc1,
   383  					Type: artifact.EvidentByRelationship,
   384  				},
   385  			},
   386  		},
   387  		{
   388  			name: "ownership-by-file-overlap relationship",
   389  			args: args{
   390  				spdxIDMap: map[string]any{
   391  					string(toSPDXID(pkg2)): pkg2,
   392  					string(toSPDXID(pkg3)): pkg3,
   393  				},
   394  				doc: &spdx.Document{
   395  					Relationships: []*spdx.Relationship{
   396  						{
   397  							RefA: common.DocElementID{
   398  								ElementRefID: toSPDXID(pkg2),
   399  							},
   400  							RefB: common.DocElementID{
   401  								ElementRefID: toSPDXID(pkg3),
   402  							},
   403  							Relationship:        spdx.RelationshipOther,
   404  							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",
   405  						},
   406  					},
   407  				},
   408  			},
   409  			want: []artifact.Relationship{
   410  				{
   411  					From: pkg2,
   412  					To:   pkg3,
   413  					Type: artifact.OwnershipByFileOverlapRelationship,
   414  				},
   415  			},
   416  		},
   417  	}
   418  	for _, tt := range tests {
   419  		t.Run(tt.name, func(t *testing.T) {
   420  			actual := toSyftRelationships(tt.args.spdxIDMap, tt.args.doc)
   421  			require.Len(t, actual, len(tt.want))
   422  			for i := range actual {
   423  				require.Equal(t, tt.want[i].From.ID(), actual[i].From.ID())
   424  				require.Equal(t, tt.want[i].To.ID(), actual[i].To.ID())
   425  				require.Equal(t, tt.want[i].Type, actual[i].Type)
   426  			}
   427  		})
   428  	}
   429  }
   430  
   431  func Test_convertToAndFromFormat(t *testing.T) {
   432  	packages := []pkg.Package{
   433  		{
   434  			Name: "pkg1",
   435  		},
   436  		{
   437  			Name: "pkg2",
   438  		},
   439  	}
   440  
   441  	for i := range packages {
   442  		(&packages[i]).SetID()
   443  	}
   444  
   445  	relationships := []artifact.Relationship{
   446  		{
   447  			From: packages[0],
   448  			To:   packages[1],
   449  			Type: artifact.ContainsRelationship,
   450  		},
   451  	}
   452  
   453  	tests := []struct {
   454  		name          string
   455  		source        source.Description
   456  		packages      []pkg.Package
   457  		relationships []artifact.Relationship
   458  	}{
   459  		{
   460  			name: "image source",
   461  			source: source.Description{
   462  				ID: "DocumentRoot-Image-some-image",
   463  				Metadata: source.StereoscopeImageSourceMetadata{
   464  					ID:             "DocumentRoot-Image-some-image",
   465  					UserInput:      "some-image:some-tag",
   466  					ManifestDigest: "sha256:ab8b83234bc28f28d8e",
   467  				},
   468  				Name:    "some-image",
   469  				Version: "some-tag",
   470  			},
   471  			packages:      packages,
   472  			relationships: relationships,
   473  		},
   474  		{
   475  			name: ". directory source",
   476  			source: source.Description{
   477  				ID:   "DocumentRoot-Directory-.",
   478  				Name: ".",
   479  				Metadata: source.DirectorySourceMetadata{
   480  					Path: ".",
   481  				},
   482  			},
   483  			packages:      packages,
   484  			relationships: relationships,
   485  		},
   486  		{
   487  			name: "directory source",
   488  			source: source.Description{
   489  				ID:   "DocumentRoot-Directory-my-app",
   490  				Name: "my-app",
   491  				Metadata: source.DirectorySourceMetadata{
   492  					Path: "my-app",
   493  				},
   494  			},
   495  			packages:      packages,
   496  			relationships: relationships,
   497  		},
   498  		{
   499  			name: "file source",
   500  			source: source.Description{
   501  				ID: "DocumentRoot-File-my-app.exe",
   502  				Metadata: source.FileSourceMetadata{
   503  					Path: "my-app.exe",
   504  					Digests: []file.Digest{
   505  						{
   506  							Algorithm: "sha256",
   507  							Value:     "3723cae0b8b83234bc28f28d8e",
   508  						},
   509  					},
   510  				},
   511  				Name: "my-app.exe",
   512  			},
   513  			packages:      packages,
   514  			relationships: relationships,
   515  		},
   516  	}
   517  
   518  	for _, test := range tests {
   519  		t.Run(test.name, func(t *testing.T) {
   520  			src := &test.source
   521  			s := sbom.SBOM{
   522  				Source: *src,
   523  				Artifacts: sbom.Artifacts{
   524  					Packages: pkg.NewCollection(test.packages...),
   525  				},
   526  				Relationships: test.relationships,
   527  			}
   528  			doc := ToFormatModel(s)
   529  			got, err := ToSyftModel(doc)
   530  			require.NoError(t, err)
   531  
   532  			if diff := cmp.Diff(&s, got,
   533  				cmpopts.IgnoreUnexported(artifact.Relationship{}),
   534  				cmpopts.IgnoreUnexported(file.LocationSet{}),
   535  				cmpopts.IgnoreUnexported(pkg.Collection{}),
   536  				cmpopts.IgnoreUnexported(pkg.Package{}),
   537  				cmpopts.IgnoreUnexported(pkg.LicenseSet{}),
   538  				cmpopts.IgnoreFields(sbom.Artifacts{}, "FileMetadata", "FileDigests"),
   539  			); diff != "" {
   540  				t.Fatalf("packages do not match:\n%s", diff)
   541  			}
   542  		})
   543  	}
   544  }
   545  
   546  func Test_purlValue(t *testing.T) {
   547  	tests := []struct {
   548  		purl     packageurl.PackageURL
   549  		expected string
   550  	}{
   551  		{
   552  			purl:     packageurl.PackageURL{},
   553  			expected: "",
   554  		},
   555  		{
   556  			purl: packageurl.PackageURL{
   557  				Name:    "name",
   558  				Version: "version",
   559  			},
   560  			expected: "",
   561  		},
   562  		{
   563  			purl: packageurl.PackageURL{
   564  				Type:    "typ",
   565  				Version: "version",
   566  			},
   567  			expected: "",
   568  		},
   569  		{
   570  			purl: packageurl.PackageURL{
   571  				Type:    "typ",
   572  				Name:    "name",
   573  				Version: "version",
   574  			},
   575  			expected: "pkg:typ/name@version",
   576  		},
   577  		{
   578  			purl: packageurl.PackageURL{
   579  				Type:    "typ",
   580  				Name:    "name",
   581  				Version: "version",
   582  				Qualifiers: packageurl.Qualifiers{
   583  					{
   584  						Key:   "q",
   585  						Value: "v",
   586  					},
   587  				},
   588  			},
   589  			expected: "pkg:typ/name@version?q=v",
   590  		},
   591  	}
   592  
   593  	for _, test := range tests {
   594  		t.Run(test.purl.String(), func(t *testing.T) {
   595  			got := purlValue(test.purl)
   596  			require.Equal(t, test.expected, got)
   597  		})
   598  	}
   599  }
   600  
   601  func Test_directPackageFiles(t *testing.T) {
   602  	doc := &spdx.Document{
   603  		SPDXVersion: "SPDX-2.3",
   604  		Packages: []*spdx.Package{
   605  			{
   606  				PackageName:           "some-package",
   607  				PackageSPDXIdentifier: "1",
   608  				PackageVersion:        "1.0.5",
   609  				Files: []*spdx.File{
   610  					{
   611  						FileName:           "some-file",
   612  						FileSPDXIdentifier: "2",
   613  						Checksums: []spdx.Checksum{
   614  							{
   615  								Algorithm: "SHA1",
   616  								Value:     "a8d733c64f9123",
   617  							},
   618  						},
   619  					},
   620  				},
   621  			},
   622  		},
   623  	}
   624  
   625  	got, err := ToSyftModel(doc)
   626  	require.NoError(t, err)
   627  
   628  	p := pkg.Package{
   629  		Name:    "some-package",
   630  		Version: "1.0.5",
   631  	}
   632  	p.SetID()
   633  	f := file.Location{
   634  		LocationData: file.LocationData{
   635  			Coordinates: file.Coordinates{
   636  				RealPath:     "some-file",
   637  				FileSystemID: "",
   638  			},
   639  			AccessPath: "some-file",
   640  		},
   641  		LocationMetadata: file.LocationMetadata{
   642  			Annotations: map[string]string{},
   643  		},
   644  	}
   645  	s := &sbom.SBOM{
   646  		Artifacts: sbom.Artifacts{
   647  			Packages: pkg.NewCollection(p),
   648  			FileMetadata: map[file.Coordinates]file.Metadata{
   649  				f.Coordinates: {},
   650  			},
   651  			FileDigests: map[file.Coordinates][]file.Digest{
   652  				f.Coordinates: {
   653  					{
   654  						Algorithm: "sha1",
   655  						Value:     "a8d733c64f9123",
   656  					},
   657  				},
   658  			},
   659  		},
   660  		Relationships: []artifact.Relationship{
   661  			{
   662  				From: p,
   663  				To:   f,
   664  				Type: artifact.ContainsRelationship,
   665  			},
   666  		},
   667  		Source:     source.Description{},
   668  		Descriptor: sbom.Descriptor{},
   669  	}
   670  
   671  	require.Equal(t, s, got)
   672  }