github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/purl/purl_test.go (about) 1 package purl_test 2 3 import ( 4 "testing" 5 6 v1 "github.com/google/go-containerregistry/pkg/v1" 7 "github.com/package-url/packageurl-go" 8 "github.com/stretchr/testify/assert" 9 "github.com/stretchr/testify/require" 10 11 "github.com/devseccon/trivy/pkg/fanal/analyzer" 12 ftypes "github.com/devseccon/trivy/pkg/fanal/types" 13 "github.com/devseccon/trivy/pkg/purl" 14 "github.com/devseccon/trivy/pkg/types" 15 ) 16 17 func TestNewPackageURL(t *testing.T) { 18 testCases := []struct { 19 name string 20 typ ftypes.TargetType 21 pkg ftypes.Package 22 metadata types.Metadata 23 want *purl.PackageURL 24 wantErr string 25 }{ 26 { 27 name: "maven package", 28 typ: ftypes.Jar, 29 pkg: ftypes.Package{ 30 Name: "org.springframework:spring-core", 31 Version: "5.3.14", 32 }, 33 want: &purl.PackageURL{ 34 PackageURL: packageurl.PackageURL{ 35 Type: packageurl.TypeMaven, 36 Namespace: "org.springframework", 37 Name: "spring-core", 38 Version: "5.3.14", 39 }, 40 }, 41 }, 42 { 43 name: "gradle package", 44 typ: ftypes.Gradle, 45 pkg: ftypes.Package{ 46 Name: "org.springframework:spring-core", 47 Version: "5.3.14", 48 }, 49 want: &purl.PackageURL{ 50 PackageURL: packageurl.PackageURL{ 51 Type: packageurl.TypeMaven, 52 Namespace: "org.springframework", 53 Name: "spring-core", 54 Version: "5.3.14", 55 }, 56 }, 57 }, 58 { 59 name: "yarn package", 60 typ: ftypes.Yarn, 61 pkg: ftypes.Package{ 62 Name: "@xtuc/ieee754", 63 Version: "1.2.0", 64 }, 65 want: &purl.PackageURL{ 66 PackageURL: packageurl.PackageURL{ 67 Type: packageurl.TypeNPM, 68 Namespace: "@xtuc", 69 Name: "ieee754", 70 Version: "1.2.0", 71 }, 72 }, 73 }, 74 { 75 name: "yarn package with non-namespace", 76 typ: ftypes.Yarn, 77 pkg: ftypes.Package{ 78 Name: "lodash", 79 Version: "4.17.21", 80 }, 81 want: &purl.PackageURL{ 82 PackageURL: packageurl.PackageURL{ 83 Type: packageurl.TypeNPM, 84 Name: "lodash", 85 Version: "4.17.21", 86 }, 87 }, 88 }, 89 { 90 name: "pnpm package", 91 typ: ftypes.Pnpm, 92 pkg: ftypes.Package{ 93 Name: "@xtuc/ieee754", 94 Version: "1.2.0", 95 }, 96 want: &purl.PackageURL{ 97 PackageURL: packageurl.PackageURL{ 98 Type: packageurl.TypeNPM, 99 Namespace: "@xtuc", 100 Name: "ieee754", 101 Version: "1.2.0", 102 }, 103 }, 104 }, 105 { 106 name: "pnpm package with non-namespace", 107 typ: ftypes.Pnpm, 108 pkg: ftypes.Package{ 109 Name: "lodash", 110 Version: "4.17.21", 111 }, 112 want: &purl.PackageURL{ 113 PackageURL: packageurl.PackageURL{ 114 Type: packageurl.TypeNPM, 115 Name: "lodash", 116 Version: "4.17.21", 117 }, 118 }, 119 }, 120 { 121 name: "pypi package", 122 typ: ftypes.PythonPkg, 123 pkg: ftypes.Package{ 124 Name: "Django_test", 125 Version: "1.2.0", 126 }, 127 want: &purl.PackageURL{ 128 PackageURL: packageurl.PackageURL{ 129 Type: packageurl.TypePyPi, 130 Name: "django-test", 131 Version: "1.2.0", 132 }, 133 }, 134 }, 135 { 136 name: "conda package", 137 typ: ftypes.CondaPkg, 138 pkg: ftypes.Package{ 139 Name: "absl-py", 140 Version: "0.4.1", 141 }, 142 want: &purl.PackageURL{ 143 PackageURL: packageurl.PackageURL{ 144 Type: packageurl.TypeConda, 145 Name: "absl-py", 146 Version: "0.4.1", 147 }, 148 }, 149 }, 150 { 151 name: "composer package", 152 typ: ftypes.Composer, 153 pkg: ftypes.Package{ 154 Name: "symfony/contracts", 155 Version: "v1.0.2", 156 }, 157 want: &purl.PackageURL{ 158 PackageURL: packageurl.PackageURL{ 159 Type: packageurl.TypeComposer, 160 Namespace: "symfony", 161 Name: "contracts", 162 Version: "v1.0.2", 163 }, 164 }, 165 }, 166 { 167 name: "golang package", 168 typ: ftypes.GoModule, 169 pkg: ftypes.Package{ 170 Name: "github.com/go-sql-driver/Mysql", 171 Version: "v1.5.0", 172 }, 173 want: &purl.PackageURL{ 174 PackageURL: packageurl.PackageURL{ 175 Type: packageurl.TypeGolang, 176 Namespace: "github.com/go-sql-driver", 177 Name: "mysql", 178 Version: "v1.5.0", 179 }, 180 }, 181 }, 182 { 183 name: "golang package with a local path", 184 typ: ftypes.GoModule, 185 pkg: ftypes.Package{ 186 Name: "./private_repos/cnrm.googlesource.com/cnrm/", 187 Version: "(devel)", 188 }, 189 want: nil, 190 }, 191 { 192 name: "hex package", 193 typ: ftypes.Hex, 194 pkg: ftypes.Package{ 195 ID: "bunt@0.2.0", 196 Name: "bunt", 197 Version: "0.2.0", 198 Locations: []ftypes.Location{ 199 { 200 StartLine: 2, 201 EndLine: 2, 202 }, 203 }, 204 }, 205 want: &purl.PackageURL{ 206 PackageURL: packageurl.PackageURL{ 207 Type: packageurl.TypeHex, 208 Name: "bunt", 209 Version: "0.2.0", 210 }, 211 }, 212 }, 213 { 214 name: "dart package", 215 typ: ftypes.Pub, 216 pkg: ftypes.Package{ 217 Name: "http", 218 Version: "0.13.2", 219 }, 220 want: &purl.PackageURL{ 221 PackageURL: packageurl.PackageURL{ 222 Type: purl.TypeDart, 223 Name: "http", 224 Version: "0.13.2", 225 }, 226 }, 227 }, 228 { 229 name: "swift package", 230 typ: ftypes.Swift, 231 pkg: ftypes.Package{ 232 ID: "github.com/apple/swift-atomics@1.1.0", 233 Name: "github.com/apple/swift-atomics", 234 Version: "1.1.0", 235 }, 236 want: &purl.PackageURL{ 237 PackageURL: packageurl.PackageURL{ 238 Type: packageurl.TypeSwift, 239 Namespace: "github.com/apple", 240 Name: "swift-atomics", 241 Version: "1.1.0", 242 }, 243 }, 244 }, 245 { 246 name: "cocoapods package", 247 typ: ftypes.Cocoapods, 248 pkg: ftypes.Package{ 249 ID: "GoogleUtilities/NSData+zlib@7.5.2", 250 Name: "GoogleUtilities/NSData+zlib", 251 Version: "7.5.2", 252 }, 253 want: &purl.PackageURL{ 254 PackageURL: packageurl.PackageURL{ 255 Type: packageurl.TypeCocoapods, 256 Name: "GoogleUtilities", 257 Version: "7.5.2", 258 Subpath: "NSData+zlib", 259 }, 260 }, 261 }, 262 { 263 name: "rust binary", 264 typ: ftypes.RustBinary, 265 pkg: ftypes.Package{ 266 ID: "abomination@0.7.3", 267 Name: "abomination", 268 Version: "0.7.3", 269 }, 270 want: &purl.PackageURL{ 271 PackageURL: packageurl.PackageURL{ 272 Type: packageurl.TypeCargo, 273 Name: "abomination", 274 Version: "0.7.3", 275 }, 276 }, 277 }, 278 { 279 name: "os package", 280 typ: ftypes.RedHat, 281 pkg: ftypes.Package{ 282 Name: "acl", 283 Version: "2.2.53", 284 Release: "1.el8", 285 Epoch: 1, 286 Arch: "aarch64", 287 SrcName: "acl", 288 SrcVersion: "2.2.53", 289 SrcRelease: "1.el8", 290 SrcEpoch: 1, 291 Modularitylabel: "", 292 }, 293 294 metadata: types.Metadata{ 295 OS: &ftypes.OS{ 296 Family: ftypes.RedHat, 297 Name: "8", 298 }, 299 }, 300 want: &purl.PackageURL{ 301 PackageURL: packageurl.PackageURL{ 302 Type: packageurl.TypeRPM, 303 Namespace: "redhat", 304 Name: "acl", 305 Version: "2.2.53-1.el8", 306 Qualifiers: packageurl.Qualifiers{ 307 { 308 Key: "arch", 309 Value: "aarch64", 310 }, 311 { 312 Key: "epoch", 313 Value: "1", 314 }, 315 { 316 Key: "distro", 317 Value: "redhat-8", 318 }, 319 }, 320 }, 321 }, 322 }, 323 { 324 name: "container", 325 typ: purl.TypeOCI, 326 metadata: types.Metadata{ 327 RepoTags: []string{ 328 "cblmariner2preview.azurecr.io/base/core:2.0.20220124-amd64", 329 }, 330 RepoDigests: []string{ 331 "cblmariner2preview.azurecr.io/base/core@sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", 332 "cblmariner2preview.azurecr.io/base/core@sha256:016bb1f5735e43b2738cd3fd1979b62608fe1727132b2506c17ba0e1f6a6ed8a", 333 }, 334 ImageConfig: v1.ConfigFile{ 335 Architecture: "amd64", 336 }, 337 }, 338 want: &purl.PackageURL{ 339 PackageURL: packageurl.PackageURL{ 340 Type: packageurl.TypeOCI, 341 Namespace: "", 342 Name: "core", 343 Version: "sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", 344 Qualifiers: packageurl.Qualifiers{ 345 { 346 Key: "repository_url", 347 Value: "cblmariner2preview.azurecr.io/base/core", 348 }, 349 { 350 Key: "arch", 351 Value: "amd64", 352 }, 353 }, 354 }, 355 }, 356 }, 357 { 358 name: "container local", 359 typ: purl.TypeOCI, 360 metadata: types.Metadata{ 361 RepoTags: []string{}, 362 RepoDigests: []string{}, 363 ImageConfig: v1.ConfigFile{ 364 Architecture: "amd64", 365 }, 366 ImageID: "sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", 367 }, 368 want: nil, 369 }, 370 { 371 name: "container with implicit registry", 372 typ: purl.TypeOCI, 373 metadata: types.Metadata{ 374 RepoTags: []string{ 375 "alpine:3.14", 376 "alpine:latest", 377 }, 378 RepoDigests: []string{ 379 "alpine:3.14@sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", 380 "alpine:latest@sha256:016bb1f5735e43b2738cd3fd1979b62608fe1727132b2506c17ba0e1f6a6ed8a", 381 }, 382 ImageConfig: v1.ConfigFile{ 383 Architecture: "amd64", 384 }, 385 }, 386 want: &purl.PackageURL{ 387 PackageURL: packageurl.PackageURL{ 388 Type: packageurl.TypeOCI, 389 Namespace: "", 390 Name: "alpine", 391 Version: "sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", 392 Qualifiers: packageurl.Qualifiers{ 393 { 394 Key: "repository_url", 395 Value: "index.docker.io/library/alpine", 396 }, 397 { 398 Key: "arch", 399 Value: "amd64", 400 }, 401 }, 402 }, 403 }, 404 }, 405 { 406 name: "sad path", 407 typ: purl.TypeOCI, 408 metadata: types.Metadata{ 409 RepoTags: []string{ 410 "cblmariner2preview.azurecr.io/base/core:2.0.20220124-amd64", 411 }, 412 RepoDigests: []string{ 413 "sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", 414 }, 415 }, 416 wantErr: "failed to parse digest", 417 }, 418 } 419 420 for _, tc := range testCases { 421 t.Run(tc.name, func(t *testing.T) { 422 packageURL, err := purl.NewPackageURL(tc.typ, tc.metadata, tc.pkg) 423 if tc.wantErr != "" { 424 require.Error(t, err) 425 assert.Contains(t, err.Error(), tc.wantErr) 426 return 427 } 428 assert.NoError(t, err) 429 assert.Equal(t, tc.want, packageURL, tc.name) 430 }) 431 } 432 } 433 434 func TestFromString(t *testing.T) { 435 testCases := []struct { 436 name string 437 purl string 438 want purl.PackageURL 439 wantErr string 440 }{ 441 { 442 name: "happy path for maven", 443 purl: "pkg:maven/org.springframework/spring-core@5.0.4.RELEASE", 444 want: purl.PackageURL{ 445 PackageURL: packageurl.PackageURL{ 446 Type: packageurl.TypeMaven, 447 Namespace: "org.springframework", 448 Version: "5.0.4.RELEASE", 449 Name: "spring-core", 450 Qualifiers: packageurl.Qualifiers{}, 451 }, 452 FilePath: "", 453 }, 454 }, 455 { 456 name: "happy path for npm", 457 purl: "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", 458 want: purl.PackageURL{ 459 PackageURL: packageurl.PackageURL{ 460 Type: packageurl.TypeNPM, 461 Name: "bootstrap", 462 Version: "5.0.2", 463 Qualifiers: packageurl.Qualifiers{ 464 { 465 Key: "file_path", 466 Value: "app/app/package.json", 467 }, 468 }, 469 }, 470 }, 471 }, 472 { 473 name: "happy path for coocapods", 474 purl: "pkg:cocoapods/GoogleUtilities@7.5.2#NSData+zlib", 475 want: purl.PackageURL{ 476 PackageURL: packageurl.PackageURL{ 477 Type: packageurl.TypeCocoapods, 478 Name: "GoogleUtilities", 479 Version: "7.5.2", 480 Subpath: "NSData+zlib", 481 Qualifiers: packageurl.Qualifiers{}, 482 }, 483 }, 484 }, 485 { 486 name: "happy path for hex", 487 purl: "pkg:hex/plug@1.14.0", 488 want: purl.PackageURL{ 489 PackageURL: packageurl.PackageURL{ 490 Type: packageurl.TypeHex, 491 Name: "plug", 492 Version: "1.14.0", 493 Qualifiers: packageurl.Qualifiers{}, 494 }, 495 }, 496 }, 497 { 498 name: "happy path for dart", 499 purl: "pkg:dart/http@0.13.2", 500 want: purl.PackageURL{ 501 PackageURL: packageurl.PackageURL{ 502 Type: purl.TypeDart, 503 Name: "http", 504 Version: "0.13.2", 505 Qualifiers: packageurl.Qualifiers{}, 506 }, 507 }, 508 }, 509 { 510 name: "happy path for apk", 511 purl: "pkg:apk/alpine/alpine-baselayout@3.2.0-r16?distro=3.14.2&epoch=1", 512 want: purl.PackageURL{ 513 PackageURL: packageurl.PackageURL{ 514 Type: string(analyzer.TypeApk), 515 Namespace: "alpine", 516 Name: "alpine-baselayout", 517 Version: "3.2.0-r16", 518 Qualifiers: packageurl.Qualifiers{ 519 { 520 Key: "distro", 521 Value: "3.14.2", 522 }, 523 { 524 Key: "epoch", 525 Value: "1", 526 }, 527 }, 528 }, 529 }, 530 }, 531 { 532 name: "happy path for rpm", 533 purl: "pkg:rpm/redhat/containers-common@0.1.14", 534 want: purl.PackageURL{ 535 PackageURL: packageurl.PackageURL{ 536 Type: packageurl.TypeRPM, 537 Namespace: "redhat", 538 Name: "containers-common", 539 Version: "0.1.14", 540 Qualifiers: packageurl.Qualifiers{}, 541 }, 542 }, 543 }, 544 { 545 name: "happy path for conda", 546 purl: "pkg:conda/absl-py@0.4.1", 547 want: purl.PackageURL{ 548 PackageURL: packageurl.PackageURL{ 549 Type: packageurl.TypeConda, 550 Name: "absl-py", 551 Version: "0.4.1", 552 Qualifiers: packageurl.Qualifiers{}, 553 }, 554 }, 555 }, 556 { 557 name: "bad rpm", 558 purl: "pkg:rpm/redhat/a--@1.0.0", 559 want: purl.PackageURL{ 560 PackageURL: packageurl.PackageURL{ 561 Type: packageurl.TypeRPM, 562 Namespace: "redhat", 563 Name: "a--", 564 Version: "1.0.0", 565 Qualifiers: packageurl.Qualifiers{}, 566 }, 567 }, 568 }, 569 } 570 571 for _, tc := range testCases { 572 t.Run(tc.name, func(t *testing.T) { 573 pkg, err := purl.FromString(tc.purl) 574 if tc.wantErr != "" { 575 require.Error(t, err) 576 assert.Contains(t, err.Error(), tc.wantErr) 577 return 578 } 579 assert.NoError(t, err) 580 assert.Equal(t, tc.want, *pkg, tc.name) 581 }) 582 } 583 } 584 585 func TestPackage(t *testing.T) { 586 tests := []struct { 587 name string 588 pkgURL *purl.PackageURL 589 wantPkg *ftypes.Package 590 }{ 591 { 592 name: "rpm + Qualifiers", 593 pkgURL: &purl.PackageURL{ 594 PackageURL: packageurl.PackageURL{ 595 Type: packageurl.TypeRPM, 596 Namespace: "redhat", 597 Name: "nodejs-full-i18n", 598 Version: "10.21.0-3.module_el8.2.0+391+8da3adc6", 599 Qualifiers: packageurl.Qualifiers{ 600 { 601 Key: "arch", 602 Value: "x86_64", 603 }, 604 { 605 Key: "epoch", 606 Value: "1", 607 }, 608 { 609 Key: "modularitylabel", 610 Value: "nodejs:10:8020020200707141642:6a468ee4", 611 }, 612 { 613 Key: "distro", 614 Value: "redhat-8", 615 }, 616 }, 617 }, 618 }, 619 wantPkg: &ftypes.Package{ 620 Name: "nodejs-full-i18n", 621 Version: "10.21.0", 622 Release: "3.module_el8.2.0+391+8da3adc6", 623 Arch: "x86_64", 624 Epoch: 1, 625 Modularitylabel: "nodejs:10:8020020200707141642:6a468ee4", 626 }, 627 }, 628 { 629 name: "composer with namespace", 630 pkgURL: &purl.PackageURL{ 631 PackageURL: packageurl.PackageURL{ 632 Type: packageurl.TypeComposer, 633 Namespace: "symfony", 634 Name: "contracts", 635 Version: "v1.0.2", 636 }, 637 }, 638 wantPkg: &ftypes.Package{ 639 Name: "symfony/contracts", 640 Version: "v1.0.2", 641 }, 642 }, 643 { 644 name: "maven with namespace", 645 pkgURL: &purl.PackageURL{ 646 PackageURL: packageurl.PackageURL{ 647 Type: packageurl.TypeMaven, 648 Namespace: "org.springframework", 649 Name: "spring-core", 650 Version: "5.0.4.RELEASE", 651 Qualifiers: packageurl.Qualifiers{}, 652 }, 653 }, 654 wantPkg: &ftypes.Package{ 655 Name: "org.springframework:spring-core", 656 Version: "5.0.4.RELEASE", 657 }, 658 }, 659 { 660 name: "cocoapods with subpath", 661 pkgURL: &purl.PackageURL{ 662 PackageURL: packageurl.PackageURL{ 663 Type: packageurl.TypeCocoapods, 664 Version: "4.2.0", 665 Name: "AppCenter", 666 Subpath: "Analytics", 667 Qualifiers: packageurl.Qualifiers{}, 668 }, 669 }, 670 wantPkg: &ftypes.Package{ 671 Name: "AppCenter/Analytics", 672 Version: "4.2.0", 673 }, 674 }, 675 { 676 name: "wrong epoch", 677 pkgURL: &purl.PackageURL{ 678 PackageURL: packageurl.PackageURL{ 679 Type: packageurl.TypeRPM, 680 Namespace: "redhat", 681 Name: "acl", 682 Version: "2.2.53-1.el8", 683 Qualifiers: packageurl.Qualifiers{ 684 { 685 Key: "epoch", 686 Value: "wrong", 687 }, 688 }, 689 }, 690 }, 691 wantPkg: &ftypes.Package{ 692 Name: "acl", 693 Version: "2.2.53", 694 Release: "1.el8", 695 }, 696 }, 697 } 698 699 for _, tt := range tests { 700 t.Run(tt.name, func(t *testing.T) { 701 got := tt.pkgURL.Package() 702 assert.Equal(t, tt.wantPkg, got) 703 }) 704 } 705 } 706 707 func TestPackageURL_LangType(t *testing.T) { 708 tests := []struct { 709 name string 710 purl packageurl.PackageURL 711 want ftypes.LangType 712 }{ 713 { 714 name: "maven", 715 purl: packageurl.PackageURL{ 716 Type: packageurl.TypeMaven, 717 Namespace: "org.springframework", 718 Name: "spring-core", 719 Version: "5.0.4.RELEASE", 720 }, 721 want: ftypes.Jar, 722 }, 723 { 724 name: "k8s", 725 purl: packageurl.PackageURL{ 726 Type: purl.TypeK8s, 727 Name: "kubelet", 728 Version: "1.21.1", 729 }, 730 want: ftypes.K8sUpstream, 731 }, 732 { 733 name: "eks", 734 purl: packageurl.PackageURL{ 735 Type: purl.TypeK8s, 736 Namespace: purl.NamespaceEKS, 737 Name: "kubelet", 738 Version: "1.21.1", 739 }, 740 want: ftypes.EKS, 741 }, 742 } 743 for _, tt := range tests { 744 t.Run(tt.name, func(t *testing.T) { 745 p := &purl.PackageURL{PackageURL: tt.purl} 746 assert.Equalf(t, tt.want, p.LangType(), "LangType()") 747 }) 748 } 749 }