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