github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/format/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/pkg" 18 "github.com/anchore/syft/syft/sbom" 19 "github.com/anchore/syft/syft/source" 20 "github.com/lineaje-labs/syft/syft/internal/sourcemetadata" 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.JavaArchive{ 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.JavaArchive{ 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 Metadata: pkg.GolangBinaryBuildinfoEntry{ 325 H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", 326 }, 327 }, 328 expected: []spdx.Checksum{ 329 { 330 Algorithm: "SHA256", 331 Value: "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c", 332 }, 333 }, 334 filesAnalyzed: false, 335 }, 336 { 337 name: "Package with no metadata type", 338 pkg: pkg.Package{ 339 Name: "test", 340 Version: "1.0.0", 341 Language: pkg.Java, 342 Metadata: struct{}{}, 343 }, 344 expected: []spdx.Checksum{}, 345 filesAnalyzed: false, 346 }, 347 } 348 349 for _, test := range tests { 350 t.Run(test.name, func(t *testing.T) { 351 commonSum, filesAnalyzed := toPackageChecksums(test.pkg) 352 assert.ElementsMatch(t, test.expected, commonSum) 353 assert.Equal(t, test.filesAnalyzed, filesAnalyzed) 354 }) 355 } 356 } 357 358 func Test_toFileTypes(t *testing.T) { 359 360 tests := []struct { 361 name string 362 metadata file.Metadata 363 expected []string 364 }{ 365 { 366 name: "application", 367 metadata: file.Metadata{ 368 MIMEType: "application/vnd.unknown", 369 }, 370 expected: []string{ 371 string(ApplicationFileType), 372 }, 373 }, 374 { 375 name: "archive", 376 metadata: file.Metadata{ 377 MIMEType: "application/zip", 378 }, 379 expected: []string{ 380 string(ApplicationFileType), 381 string(ArchiveFileType), 382 }, 383 }, 384 { 385 name: "audio", 386 metadata: file.Metadata{ 387 MIMEType: "audio/ogg", 388 }, 389 expected: []string{ 390 string(AudioFileType), 391 }, 392 }, 393 { 394 name: "video", 395 metadata: file.Metadata{ 396 MIMEType: "video/3gpp", 397 }, 398 expected: []string{ 399 string(VideoFileType), 400 }, 401 }, 402 { 403 name: "text", 404 metadata: file.Metadata{ 405 MIMEType: "text/html", 406 }, 407 expected: []string{ 408 string(TextFileType), 409 }, 410 }, 411 { 412 name: "image", 413 metadata: file.Metadata{ 414 MIMEType: "image/png", 415 }, 416 expected: []string{ 417 string(ImageFileType), 418 }, 419 }, 420 { 421 name: "binary", 422 metadata: file.Metadata{ 423 MIMEType: "application/x-sharedlib", 424 }, 425 expected: []string{ 426 string(ApplicationFileType), 427 string(BinaryFileType), 428 }, 429 }, 430 } 431 for _, test := range tests { 432 t.Run(test.name, func(t *testing.T) { 433 assert.ElementsMatch(t, test.expected, toFileTypes(&test.metadata)) 434 }) 435 } 436 } 437 438 func Test_lookupRelationship(t *testing.T) { 439 440 tests := []struct { 441 input artifact.RelationshipType 442 exists bool 443 ty RelationshipType 444 comment string 445 }{ 446 { 447 input: artifact.ContainsRelationship, 448 exists: true, 449 ty: ContainsRelationship, 450 }, 451 { 452 input: artifact.OwnershipByFileOverlapRelationship, 453 exists: true, 454 ty: OtherRelationship, 455 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", 456 }, 457 { 458 input: artifact.EvidentByRelationship, 459 exists: true, 460 ty: OtherRelationship, 461 comment: "evident-by: indicates the package's existence is evident by the given file", 462 }, 463 { 464 input: "made-up", 465 exists: false, 466 }, 467 } 468 for _, test := range tests { 469 t.Run(string(test.input), func(t *testing.T) { 470 exists, ty, comment := lookupRelationship(test.input) 471 assert.Equal(t, exists, test.exists) 472 assert.Equal(t, ty, test.ty) 473 assert.Equal(t, comment, test.comment) 474 }) 475 } 476 } 477 478 func Test_toFileChecksums(t *testing.T) { 479 tests := []struct { 480 name string 481 digests []file.Digest 482 expected []spdx.Checksum 483 }{ 484 { 485 name: "empty", 486 }, 487 { 488 name: "has digests", 489 digests: []file.Digest{ 490 { 491 Algorithm: "SHA256", 492 Value: "deadbeefcafe", 493 }, 494 { 495 Algorithm: "md5", 496 Value: "meh", 497 }, 498 }, 499 expected: []spdx.Checksum{ 500 { 501 Algorithm: "SHA256", 502 Value: "deadbeefcafe", 503 }, 504 { 505 Algorithm: "MD5", 506 Value: "meh", 507 }, 508 }, 509 }, 510 } 511 for _, test := range tests { 512 t.Run(test.name, func(t *testing.T) { 513 assert.ElementsMatch(t, test.expected, toFileChecksums(test.digests)) 514 }) 515 } 516 } 517 518 func Test_fileIDsForPackage(t *testing.T) { 519 p := pkg.Package{ 520 Name: "bogus", 521 } 522 523 c := file.Coordinates{ 524 RealPath: "/path", 525 FileSystemID: "nowhere", 526 } 527 528 docElementId := func(identifiable artifact.Identifiable) spdx.DocElementID { 529 return spdx.DocElementID{ 530 ElementRefID: toSPDXID(identifiable), 531 } 532 } 533 534 tests := []struct { 535 name string 536 relationships []artifact.Relationship 537 expected []*spdx.Relationship 538 }{ 539 { 540 name: "package-to-file contains relationships", 541 relationships: []artifact.Relationship{ 542 { 543 From: p, 544 To: c, 545 Type: artifact.ContainsRelationship, 546 }, 547 }, 548 expected: []*spdx.Relationship{ 549 { 550 Relationship: "CONTAINS", 551 RefA: docElementId(p), 552 RefB: docElementId(c), 553 }, 554 }, 555 }, 556 { 557 name: "package-to-package", 558 relationships: []artifact.Relationship{ 559 { 560 From: p, 561 To: p, 562 Type: artifact.ContainsRelationship, 563 }, 564 }, 565 expected: []*spdx.Relationship{ 566 { 567 Relationship: "CONTAINS", 568 RefA: docElementId(p), 569 RefB: docElementId(p), 570 }, 571 }, 572 }, 573 { 574 name: "ignore file-to-file", 575 relationships: []artifact.Relationship{ 576 { 577 From: c, 578 To: c, 579 Type: artifact.ContainsRelationship, 580 }, 581 }, 582 expected: nil, 583 }, 584 { 585 name: "ignore file-to-package", 586 relationships: []artifact.Relationship{ 587 { 588 From: c, 589 To: p, 590 Type: artifact.ContainsRelationship, 591 }, 592 }, 593 expected: nil, 594 }, 595 { 596 name: "include package-to-file overlap relationships", 597 relationships: []artifact.Relationship{ 598 { 599 From: p, 600 To: c, 601 Type: artifact.OwnershipByFileOverlapRelationship, 602 }, 603 }, 604 expected: []*spdx.Relationship{ 605 { 606 Relationship: "OTHER", 607 RefA: docElementId(p), 608 RefB: docElementId(c), 609 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", 610 }, 611 }, 612 }, 613 } 614 for _, test := range tests { 615 t.Run(test.name, func(t *testing.T) { 616 relationships := toRelationships(test.relationships) 617 assert.Equal(t, test.expected, relationships) 618 }) 619 } 620 } 621 622 func Test_H1Digest(t *testing.T) { 623 s := sbom.SBOM{} 624 tests := []struct { 625 name string 626 pkg pkg.Package 627 expectedDigest string 628 }{ 629 { 630 name: "valid h1digest", 631 pkg: pkg.Package{ 632 Name: "github.com/googleapis/gnostic", 633 Version: "v0.5.5", 634 Metadata: pkg.GolangBinaryBuildinfoEntry{ 635 H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", 636 }, 637 }, 638 expectedDigest: "SHA256:f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c", 639 }, 640 { 641 name: "invalid h1digest", 642 pkg: pkg.Package{ 643 Name: "github.com/googleapis/gnostic", 644 Version: "v0.5.5", 645 Metadata: pkg.GolangBinaryBuildinfoEntry{ 646 H1Digest: "h1:9fHAtK0uzzz", 647 }, 648 }, 649 expectedDigest: "", 650 }, 651 { 652 name: "unsupported h-digest", 653 pkg: pkg.Package{ 654 Name: "github.com/googleapis/gnostic", 655 Version: "v0.5.5", 656 Metadata: pkg.GolangBinaryBuildinfoEntry{ 657 H1Digest: "h12:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", 658 }, 659 }, 660 expectedDigest: "", 661 }, 662 } 663 664 for _, test := range tests { 665 t.Run(test.name, func(t *testing.T) { 666 catalog := pkg.NewCollection(test.pkg) 667 pkgs := toPackages(catalog, s) 668 require.Len(t, pkgs, 1) 669 for _, p := range pkgs { 670 if test.expectedDigest == "" { 671 require.Len(t, p.PackageChecksums, 0) 672 } else { 673 require.Len(t, p.PackageChecksums, 1) 674 for _, c := range p.PackageChecksums { 675 require.Equal(t, test.expectedDigest, fmt.Sprintf("%s:%s", c.Algorithm, c.Value)) 676 } 677 } 678 } 679 }) 680 } 681 } 682 683 func Test_OtherLicenses(t *testing.T) { 684 tests := []struct { 685 name string 686 pkg pkg.Package 687 expected []*spdx.OtherLicense 688 }{ 689 { 690 name: "no licenseRef", 691 pkg: pkg.Package{ 692 Licenses: pkg.NewLicenseSet(), 693 }, 694 expected: nil, 695 }, 696 { 697 name: "single licenseRef", 698 pkg: pkg.Package{ 699 Licenses: pkg.NewLicenseSet( 700 pkg.NewLicense("foobar"), 701 ), 702 }, 703 expected: []*spdx.OtherLicense{ 704 { 705 LicenseIdentifier: "LicenseRef-foobar", 706 ExtractedText: "foobar", 707 }, 708 }, 709 }, 710 { 711 name: "multiple licenseRef", 712 pkg: pkg.Package{ 713 Licenses: pkg.NewLicenseSet( 714 pkg.NewLicense("internal made up license name"), 715 pkg.NewLicense("new apple license 2.0"), 716 ), 717 }, 718 expected: []*spdx.OtherLicense{ 719 { 720 LicenseIdentifier: "LicenseRef-internal-made-up-license-name", 721 ExtractedText: "internal made up license name", 722 }, 723 { 724 LicenseIdentifier: "LicenseRef-new-apple-license-2.0", 725 ExtractedText: "new apple license 2.0", 726 }, 727 }, 728 }, 729 } 730 731 for _, test := range tests { 732 t.Run(test.name, func(t *testing.T) { 733 catalog := pkg.NewCollection(test.pkg) 734 otherLicenses := toOtherLicenses(catalog) 735 require.Len(t, otherLicenses, len(test.expected)) 736 require.Equal(t, test.expected, otherLicenses) 737 }) 738 } 739 } 740 741 func Test_toSPDXID(t *testing.T) { 742 tests := []struct { 743 name string 744 it artifact.Identifiable 745 expected string 746 }{ 747 { 748 name: "short filename", 749 it: file.Coordinates{ 750 RealPath: "/short/path/file.txt", 751 }, 752 expected: "File-short-path-file.txt", 753 }, 754 { 755 name: "long filename", 756 it: file.Coordinates{ 757 RealPath: "/some/long/path/with/a/lot/of-text/that-contains-a/file.txt", 758 }, 759 expected: "File-...a-lot-of-text-that-contains-a-file.txt", 760 }, 761 { 762 name: "package", 763 it: pkg.Package{ 764 Type: pkg.NpmPkg, 765 Name: "some-package", 766 }, 767 expected: "Package-npm-some-package", 768 }, 769 } 770 771 for _, test := range tests { 772 t.Run(test.name, func(t *testing.T) { 773 got := string(toSPDXID(test.it)) 774 // trim the hash 775 got = regexp.MustCompile(`-[a-z0-9]*$`).ReplaceAllString(got, "") 776 require.Equal(t, test.expected, got) 777 }) 778 } 779 }