github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/formats/common/spdxhelpers/to_format_model_test.go (about) 1 package spdxhelpers 2 3 import ( 4 "fmt" 5 "regexp" 6 "testing" 7 8 "github.com/google/go-cmp/cmp" 9 "github.com/google/go-cmp/cmp/cmpopts" 10 "github.com/spdx/tools-golang/spdx" 11 "github.com/spdx/tools-golang/spdx/v2/v2_3" 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 15 "github.com/anchore/syft/syft/artifact" 16 "github.com/anchore/syft/syft/file" 17 "github.com/anchore/syft/syft/internal/sourcemetadata" 18 "github.com/anchore/syft/syft/pkg" 19 "github.com/anchore/syft/syft/sbom" 20 "github.com/anchore/syft/syft/source" 21 ) 22 23 func Test_toFormatModel(t *testing.T) { 24 tracker := sourcemetadata.NewCompletionTester(t) 25 26 tests := []struct { 27 name string 28 in sbom.SBOM 29 expected *spdx.Document 30 }{ 31 { 32 name: "container", 33 in: sbom.SBOM{ 34 Source: source.Description{ 35 Name: "alpine", 36 Version: "sha256:d34db33f", 37 Metadata: source.StereoscopeImageSourceMetadata{ 38 UserInput: "alpine:latest", 39 ManifestDigest: "sha256:d34db33f", 40 }, 41 }, 42 Artifacts: sbom.Artifacts{ 43 Packages: pkg.NewCollection(pkg.Package{ 44 Name: "pkg-1", 45 Version: "version-1", 46 }), 47 }, 48 }, 49 expected: &spdx.Document{ 50 SPDXIdentifier: "DOCUMENT", 51 SPDXVersion: spdx.Version, 52 DataLicense: spdx.DataLicense, 53 DocumentName: "alpine", 54 Packages: []*spdx.Package{ 55 { 56 PackageSPDXIdentifier: "Package-pkg-1-pkg-1", 57 PackageName: "pkg-1", 58 PackageVersion: "version-1", 59 PackageSupplier: &spdx.Supplier{ 60 Supplier: "NOASSERTION", 61 }, 62 }, 63 { 64 PackageSPDXIdentifier: "DocumentRoot-Image-alpine", 65 PackageName: "alpine", 66 PackageVersion: "sha256:d34db33f", 67 PrimaryPackagePurpose: "CONTAINER", 68 PackageChecksums: []spdx.Checksum{{Algorithm: "SHA256", Value: "d34db33f"}}, 69 PackageExternalReferences: []*v2_3.PackageExternalReference{ 70 { 71 Category: "PACKAGE-MANAGER", 72 RefType: "purl", 73 Locator: "pkg:oci/alpine@sha256:d34db33f?arch=&tag=latest", 74 }, 75 }, 76 PackageSupplier: &spdx.Supplier{ 77 Supplier: "NOASSERTION", 78 }, 79 }, 80 }, 81 Relationships: []*spdx.Relationship{ 82 { 83 RefA: spdx.DocElementID{ 84 ElementRefID: "DocumentRoot-Image-alpine", 85 }, 86 RefB: spdx.DocElementID{ 87 ElementRefID: "Package-pkg-1-pkg-1", 88 }, 89 Relationship: spdx.RelationshipContains, 90 }, 91 { 92 RefA: spdx.DocElementID{ 93 ElementRefID: "DOCUMENT", 94 }, 95 RefB: spdx.DocElementID{ 96 ElementRefID: "DocumentRoot-Image-alpine", 97 }, 98 Relationship: spdx.RelationshipDescribes, 99 }, 100 }, 101 }, 102 }, 103 { 104 name: "directory", 105 in: sbom.SBOM{ 106 Source: source.Description{ 107 Name: "some/directory", 108 Metadata: source.DirectorySourceMetadata{ 109 Path: "some/directory", 110 }, 111 }, 112 Artifacts: sbom.Artifacts{ 113 Packages: pkg.NewCollection(pkg.Package{ 114 Name: "pkg-1", 115 Version: "version-1", 116 }), 117 }, 118 }, 119 expected: &spdx.Document{ 120 SPDXIdentifier: "DOCUMENT", 121 SPDXVersion: spdx.Version, 122 DataLicense: spdx.DataLicense, 123 DocumentName: "some/directory", 124 125 Packages: []*spdx.Package{ 126 { 127 PackageSPDXIdentifier: "Package-pkg-1-pkg-1", 128 PackageName: "pkg-1", 129 PackageVersion: "version-1", 130 PackageSupplier: &spdx.Supplier{ 131 Supplier: "NOASSERTION", 132 }, 133 }, 134 { 135 PackageSPDXIdentifier: "DocumentRoot-Directory-some-directory", 136 PackageName: "some/directory", 137 PackageVersion: "", 138 PrimaryPackagePurpose: "FILE", 139 PackageSupplier: &spdx.Supplier{ 140 Supplier: "NOASSERTION", 141 }, 142 }, 143 }, 144 Relationships: []*spdx.Relationship{ 145 { 146 RefA: spdx.DocElementID{ 147 ElementRefID: "DocumentRoot-Directory-some-directory", 148 }, 149 RefB: spdx.DocElementID{ 150 ElementRefID: "Package-pkg-1-pkg-1", 151 }, 152 Relationship: spdx.RelationshipContains, 153 }, 154 { 155 RefA: spdx.DocElementID{ 156 ElementRefID: "DOCUMENT", 157 }, 158 RefB: spdx.DocElementID{ 159 ElementRefID: "DocumentRoot-Directory-some-directory", 160 }, 161 Relationship: spdx.RelationshipDescribes, 162 }, 163 }, 164 }, 165 }, 166 { 167 name: "file", 168 in: sbom.SBOM{ 169 Source: source.Description{ 170 Name: "path/to/some.file", 171 Version: "sha256:d34db33f", 172 Metadata: source.FileSourceMetadata{ 173 Path: "path/to/some.file", 174 Digests: []file.Digest{ 175 { 176 Algorithm: "sha256", 177 Value: "d34db33f", 178 }, 179 }, 180 }, 181 }, 182 Artifacts: sbom.Artifacts{ 183 Packages: pkg.NewCollection(pkg.Package{ 184 Name: "pkg-1", 185 Version: "version-1", 186 }), 187 }, 188 }, 189 expected: &spdx.Document{ 190 SPDXIdentifier: "DOCUMENT", 191 SPDXVersion: spdx.Version, 192 DataLicense: spdx.DataLicense, 193 DocumentName: "path/to/some.file", 194 Packages: []*spdx.Package{ 195 { 196 PackageSPDXIdentifier: "Package-pkg-1-pkg-1", 197 PackageName: "pkg-1", 198 PackageVersion: "version-1", 199 PackageSupplier: &spdx.Supplier{ 200 Supplier: "NOASSERTION", 201 }, 202 }, 203 { 204 PackageSPDXIdentifier: "DocumentRoot-File-path-to-some.file", 205 PackageName: "path/to/some.file", 206 PackageVersion: "sha256:d34db33f", 207 PrimaryPackagePurpose: "FILE", 208 PackageChecksums: []spdx.Checksum{{Algorithm: "SHA256", Value: "d34db33f"}}, 209 PackageSupplier: &spdx.Supplier{ 210 Supplier: "NOASSERTION", 211 }, 212 }, 213 }, 214 Relationships: []*spdx.Relationship{ 215 { 216 RefA: spdx.DocElementID{ 217 ElementRefID: "DocumentRoot-File-path-to-some.file", 218 }, 219 RefB: spdx.DocElementID{ 220 ElementRefID: "Package-pkg-1-pkg-1", 221 }, 222 Relationship: spdx.RelationshipContains, 223 }, 224 { 225 RefA: spdx.DocElementID{ 226 ElementRefID: "DOCUMENT", 227 }, 228 RefB: spdx.DocElementID{ 229 ElementRefID: "DocumentRoot-File-path-to-some.file", 230 }, 231 Relationship: spdx.RelationshipDescribes, 232 }, 233 }, 234 }, 235 }, 236 } 237 238 for _, test := range tests { 239 t.Run(test.name, func(t *testing.T) { 240 tracker.Tested(t, test.in.Source.Metadata) 241 242 // replace IDs with package names 243 var pkgs []pkg.Package 244 for p := range test.in.Artifacts.Packages.Enumerate() { 245 p.OverrideID(artifact.ID(p.Name)) 246 pkgs = append(pkgs, p) 247 } 248 test.in.Artifacts.Packages = pkg.NewCollection(pkgs...) 249 250 // convert 251 got := ToFormatModel(test.in) 252 253 // check differences 254 if diff := cmp.Diff(test.expected, got, 255 cmpopts.IgnoreUnexported(spdx.Document{}, spdx.Package{}), 256 cmpopts.IgnoreFields(spdx.Document{}, "CreationInfo", "DocumentNamespace"), 257 cmpopts.IgnoreFields(spdx.Package{}, "PackageDownloadLocation", "IsFilesAnalyzedTagPresent", "PackageSourceInfo", "PackageLicenseConcluded", "PackageLicenseDeclared", "PackageCopyrightText"), 258 ); diff != "" { 259 t.Error(diff) 260 } 261 }) 262 } 263 } 264 265 func Test_toPackageChecksums(t *testing.T) { 266 tests := []struct { 267 name string 268 pkg pkg.Package 269 expected []spdx.Checksum 270 filesAnalyzed bool 271 }{ 272 { 273 name: "Java Package", 274 pkg: pkg.Package{ 275 Name: "test", 276 Version: "1.0.0", 277 Language: pkg.Java, 278 Metadata: pkg.JavaMetadata{ 279 ArchiveDigests: []file.Digest{ 280 { 281 Algorithm: "sha1", // SPDX expects these to be uppercase 282 Value: "1234", 283 }, 284 }, 285 }, 286 }, 287 expected: []spdx.Checksum{ 288 { 289 Algorithm: "SHA1", 290 Value: "1234", 291 }, 292 }, 293 filesAnalyzed: true, 294 }, 295 { 296 name: "Java Package with no archive digests", 297 pkg: pkg.Package{ 298 Name: "test", 299 Version: "1.0.0", 300 Language: pkg.Java, 301 Metadata: pkg.JavaMetadata{ 302 ArchiveDigests: []file.Digest{}, 303 }, 304 }, 305 expected: []spdx.Checksum{}, 306 filesAnalyzed: false, 307 }, 308 { 309 name: "Java Package with no metadata", 310 pkg: pkg.Package{ 311 Name: "test", 312 Version: "1.0.0", 313 Language: pkg.Java, 314 }, 315 expected: []spdx.Checksum{}, 316 filesAnalyzed: false, 317 }, 318 { 319 name: "Go Binary Package", 320 pkg: pkg.Package{ 321 Name: "test", 322 Version: "1.0.0", 323 Language: pkg.Go, 324 MetadataType: pkg.GolangBinMetadataType, 325 Metadata: pkg.GolangBinMetadata{ 326 H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", 327 }, 328 }, 329 expected: []spdx.Checksum{ 330 { 331 Algorithm: "SHA256", 332 Value: "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c", 333 }, 334 }, 335 filesAnalyzed: false, 336 }, 337 { 338 name: "Package with no metadata type", 339 pkg: pkg.Package{ 340 Name: "test", 341 Version: "1.0.0", 342 Language: pkg.Java, 343 Metadata: struct{}{}, 344 }, 345 expected: []spdx.Checksum{}, 346 filesAnalyzed: false, 347 }, 348 } 349 350 for _, test := range tests { 351 t.Run(test.name, func(t *testing.T) { 352 commonSum, filesAnalyzed := toPackageChecksums(test.pkg) 353 assert.ElementsMatch(t, test.expected, commonSum) 354 assert.Equal(t, test.filesAnalyzed, filesAnalyzed) 355 }) 356 } 357 } 358 359 func Test_toFileTypes(t *testing.T) { 360 361 tests := []struct { 362 name string 363 metadata file.Metadata 364 expected []string 365 }{ 366 { 367 name: "application", 368 metadata: file.Metadata{ 369 MIMEType: "application/vnd.unknown", 370 }, 371 expected: []string{ 372 string(ApplicationFileType), 373 }, 374 }, 375 { 376 name: "archive", 377 metadata: file.Metadata{ 378 MIMEType: "application/zip", 379 }, 380 expected: []string{ 381 string(ApplicationFileType), 382 string(ArchiveFileType), 383 }, 384 }, 385 { 386 name: "audio", 387 metadata: file.Metadata{ 388 MIMEType: "audio/ogg", 389 }, 390 expected: []string{ 391 string(AudioFileType), 392 }, 393 }, 394 { 395 name: "video", 396 metadata: file.Metadata{ 397 MIMEType: "video/3gpp", 398 }, 399 expected: []string{ 400 string(VideoFileType), 401 }, 402 }, 403 { 404 name: "text", 405 metadata: file.Metadata{ 406 MIMEType: "text/html", 407 }, 408 expected: []string{ 409 string(TextFileType), 410 }, 411 }, 412 { 413 name: "image", 414 metadata: file.Metadata{ 415 MIMEType: "image/png", 416 }, 417 expected: []string{ 418 string(ImageFileType), 419 }, 420 }, 421 { 422 name: "binary", 423 metadata: file.Metadata{ 424 MIMEType: "application/x-sharedlib", 425 }, 426 expected: []string{ 427 string(ApplicationFileType), 428 string(BinaryFileType), 429 }, 430 }, 431 } 432 for _, test := range tests { 433 t.Run(test.name, func(t *testing.T) { 434 assert.ElementsMatch(t, test.expected, toFileTypes(&test.metadata)) 435 }) 436 } 437 } 438 439 func Test_lookupRelationship(t *testing.T) { 440 441 tests := []struct { 442 input artifact.RelationshipType 443 exists bool 444 ty RelationshipType 445 comment string 446 }{ 447 { 448 input: artifact.ContainsRelationship, 449 exists: true, 450 ty: ContainsRelationship, 451 }, 452 { 453 input: artifact.OwnershipByFileOverlapRelationship, 454 exists: true, 455 ty: OtherRelationship, 456 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", 457 }, 458 { 459 input: artifact.EvidentByRelationship, 460 exists: true, 461 ty: OtherRelationship, 462 comment: "evident-by: indicates the package's existence is evident by the given file", 463 }, 464 { 465 input: "made-up", 466 exists: false, 467 }, 468 } 469 for _, test := range tests { 470 t.Run(string(test.input), func(t *testing.T) { 471 exists, ty, comment := lookupRelationship(test.input) 472 assert.Equal(t, exists, test.exists) 473 assert.Equal(t, ty, test.ty) 474 assert.Equal(t, comment, test.comment) 475 }) 476 } 477 } 478 479 func Test_toFileChecksums(t *testing.T) { 480 tests := []struct { 481 name string 482 digests []file.Digest 483 expected []spdx.Checksum 484 }{ 485 { 486 name: "empty", 487 }, 488 { 489 name: "has digests", 490 digests: []file.Digest{ 491 { 492 Algorithm: "SHA256", 493 Value: "deadbeefcafe", 494 }, 495 { 496 Algorithm: "md5", 497 Value: "meh", 498 }, 499 }, 500 expected: []spdx.Checksum{ 501 { 502 Algorithm: "SHA256", 503 Value: "deadbeefcafe", 504 }, 505 { 506 Algorithm: "MD5", 507 Value: "meh", 508 }, 509 }, 510 }, 511 } 512 for _, test := range tests { 513 t.Run(test.name, func(t *testing.T) { 514 assert.ElementsMatch(t, test.expected, toFileChecksums(test.digests)) 515 }) 516 } 517 } 518 519 func Test_fileIDsForPackage(t *testing.T) { 520 p := pkg.Package{ 521 Name: "bogus", 522 } 523 524 c := file.Coordinates{ 525 RealPath: "/path", 526 FileSystemID: "nowhere", 527 } 528 529 docElementId := func(identifiable artifact.Identifiable) spdx.DocElementID { 530 return spdx.DocElementID{ 531 ElementRefID: toSPDXID(identifiable), 532 } 533 } 534 535 tests := []struct { 536 name string 537 relationships []artifact.Relationship 538 expected []*spdx.Relationship 539 }{ 540 { 541 name: "package-to-file contains relationships", 542 relationships: []artifact.Relationship{ 543 { 544 From: p, 545 To: c, 546 Type: artifact.ContainsRelationship, 547 }, 548 }, 549 expected: []*spdx.Relationship{ 550 { 551 Relationship: "CONTAINS", 552 RefA: docElementId(p), 553 RefB: docElementId(c), 554 }, 555 }, 556 }, 557 { 558 name: "package-to-package", 559 relationships: []artifact.Relationship{ 560 { 561 From: p, 562 To: p, 563 Type: artifact.ContainsRelationship, 564 }, 565 }, 566 expected: []*spdx.Relationship{ 567 { 568 Relationship: "CONTAINS", 569 RefA: docElementId(p), 570 RefB: docElementId(p), 571 }, 572 }, 573 }, 574 { 575 name: "ignore file-to-file", 576 relationships: []artifact.Relationship{ 577 { 578 From: c, 579 To: c, 580 Type: artifact.ContainsRelationship, 581 }, 582 }, 583 expected: nil, 584 }, 585 { 586 name: "ignore file-to-package", 587 relationships: []artifact.Relationship{ 588 { 589 From: c, 590 To: p, 591 Type: artifact.ContainsRelationship, 592 }, 593 }, 594 expected: nil, 595 }, 596 { 597 name: "include package-to-file overlap relationships", 598 relationships: []artifact.Relationship{ 599 { 600 From: p, 601 To: c, 602 Type: artifact.OwnershipByFileOverlapRelationship, 603 }, 604 }, 605 expected: []*spdx.Relationship{ 606 { 607 Relationship: "OTHER", 608 RefA: docElementId(p), 609 RefB: docElementId(c), 610 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", 611 }, 612 }, 613 }, 614 } 615 for _, test := range tests { 616 t.Run(test.name, func(t *testing.T) { 617 relationships := toRelationships(test.relationships) 618 assert.Equal(t, test.expected, relationships) 619 }) 620 } 621 } 622 623 func Test_H1Digest(t *testing.T) { 624 s := sbom.SBOM{} 625 tests := []struct { 626 name string 627 pkg pkg.Package 628 expectedDigest string 629 }{ 630 { 631 name: "valid h1digest", 632 pkg: pkg.Package{ 633 Name: "github.com/googleapis/gnostic", 634 Version: "v0.5.5", 635 MetadataType: pkg.GolangBinMetadataType, 636 Metadata: pkg.GolangBinMetadata{ 637 H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", 638 }, 639 }, 640 expectedDigest: "SHA256:f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c", 641 }, 642 { 643 name: "invalid h1digest", 644 pkg: pkg.Package{ 645 Name: "github.com/googleapis/gnostic", 646 Version: "v0.5.5", 647 MetadataType: pkg.GolangBinMetadataType, 648 Metadata: pkg.GolangBinMetadata{ 649 H1Digest: "h1:9fHAtK0uzzz", 650 }, 651 }, 652 expectedDigest: "", 653 }, 654 { 655 name: "unsupported h-digest", 656 pkg: pkg.Package{ 657 Name: "github.com/googleapis/gnostic", 658 Version: "v0.5.5", 659 MetadataType: pkg.GolangBinMetadataType, 660 Metadata: pkg.GolangBinMetadata{ 661 H1Digest: "h12:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", 662 }, 663 }, 664 expectedDigest: "", 665 }, 666 } 667 668 for _, test := range tests { 669 t.Run(test.name, func(t *testing.T) { 670 catalog := pkg.NewCollection(test.pkg) 671 pkgs := toPackages(catalog, s) 672 require.Len(t, pkgs, 1) 673 for _, p := range pkgs { 674 if test.expectedDigest == "" { 675 require.Len(t, p.PackageChecksums, 0) 676 } else { 677 require.Len(t, p.PackageChecksums, 1) 678 for _, c := range p.PackageChecksums { 679 require.Equal(t, test.expectedDigest, fmt.Sprintf("%s:%s", c.Algorithm, c.Value)) 680 } 681 } 682 } 683 }) 684 } 685 } 686 687 func Test_OtherLicenses(t *testing.T) { 688 tests := []struct { 689 name string 690 pkg pkg.Package 691 expected []*spdx.OtherLicense 692 }{ 693 { 694 name: "no licenseRef", 695 pkg: pkg.Package{ 696 Licenses: pkg.NewLicenseSet(), 697 }, 698 expected: nil, 699 }, 700 { 701 name: "single licenseRef", 702 pkg: pkg.Package{ 703 Licenses: pkg.NewLicenseSet( 704 pkg.NewLicense("foobar"), 705 ), 706 }, 707 expected: []*spdx.OtherLicense{ 708 { 709 LicenseIdentifier: "LicenseRef-foobar", 710 ExtractedText: "foobar", 711 }, 712 }, 713 }, 714 { 715 name: "multiple licenseRef", 716 pkg: pkg.Package{ 717 Licenses: pkg.NewLicenseSet( 718 pkg.NewLicense("internal made up license name"), 719 pkg.NewLicense("new apple license 2.0"), 720 ), 721 }, 722 expected: []*spdx.OtherLicense{ 723 { 724 LicenseIdentifier: "LicenseRef-internal-made-up-license-name", 725 ExtractedText: "internal made up license name", 726 }, 727 { 728 LicenseIdentifier: "LicenseRef-new-apple-license-2.0", 729 ExtractedText: "new apple license 2.0", 730 }, 731 }, 732 }, 733 } 734 735 for _, test := range tests { 736 t.Run(test.name, func(t *testing.T) { 737 catalog := pkg.NewCollection(test.pkg) 738 otherLicenses := toOtherLicenses(catalog) 739 require.Len(t, otherLicenses, len(test.expected)) 740 require.Equal(t, test.expected, otherLicenses) 741 }) 742 } 743 } 744 745 func Test_toSPDXID(t *testing.T) { 746 tests := []struct { 747 name string 748 it artifact.Identifiable 749 expected string 750 }{ 751 { 752 name: "short filename", 753 it: file.Coordinates{ 754 RealPath: "/short/path/file.txt", 755 }, 756 expected: "File-short-path-file.txt", 757 }, 758 { 759 name: "long filename", 760 it: file.Coordinates{ 761 RealPath: "/some/long/path/with/a/lot/of-text/that-contains-a/file.txt", 762 }, 763 expected: "File-...a-lot-of-text-that-contains-a-file.txt", 764 }, 765 { 766 name: "package", 767 it: pkg.Package{ 768 Type: pkg.NpmPkg, 769 Name: "some-package", 770 }, 771 expected: "Package-npm-some-package", 772 }, 773 } 774 775 for _, test := range tests { 776 t.Run(test.name, func(t *testing.T) { 777 got := string(toSPDXID(test.it)) 778 // trim the hash 779 got = regexp.MustCompile(`-[a-z0-9]*$`).ReplaceAllString(got, "") 780 require.Equal(t, test.expected, got) 781 }) 782 } 783 }