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 }