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 }