github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/pkg/cataloger/binary/classifier_cataloger_test.go (about) 1 package binary 2 3 import ( 4 "context" 5 "errors" 6 "flag" 7 "fmt" 8 "io" 9 "strings" 10 "testing" 11 12 "github.com/google/go-cmp/cmp" 13 "github.com/google/go-cmp/cmp/cmpopts" 14 "github.com/stretchr/testify/assert" 15 "github.com/stretchr/testify/require" 16 17 "github.com/anchore/packageurl-go" 18 "github.com/anchore/stereoscope/pkg/imagetest" 19 "github.com/anchore/syft/syft/cpe" 20 "github.com/anchore/syft/syft/file" 21 "github.com/anchore/syft/syft/pkg" 22 "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/testutil" 23 "github.com/anchore/syft/syft/source" 24 "github.com/anchore/syft/syft/source/directorysource" 25 "github.com/anchore/syft/syft/source/stereoscopesource" 26 ) 27 28 var mustUseOriginalBinaries = flag.Bool("must-use-original-binaries", false, "force the use of binaries for testing (instead of snippets)") 29 30 func Test_Cataloger_PositiveCases(t *testing.T) { 31 tests := []struct { 32 name string 33 // logicalFixture is the logical path to the full binary or snippet. This is relative to the test-fixtures/classifiers/snippets 34 // or test-fixtures/classifiers/bin directory . Snippets are searched for first, and if not found, then existing binaries are 35 // used. If no binary or snippet is found the test will fail. If '-must-use-original-binaries' is used the only 36 // full binaries are tested (no snippets), and if no binary is found the test will be skipped. 37 logicalFixture string 38 expected pkg.Package 39 }{ 40 { 41 logicalFixture: "arangodb/3.11.8/linux-amd64", 42 expected: pkg.Package{ 43 Name: "arangodb", 44 Version: "3.11.8", 45 Type: "binary", 46 PURL: "pkg:generic/arangodb@3.11.8", 47 Locations: locations("arangosh"), 48 Metadata: metadata("arangodb-binary"), 49 }, 50 }, 51 { 52 logicalFixture: "postgres/15beta4/linux-amd64", 53 expected: pkg.Package{ 54 Name: "postgresql", 55 Version: "15beta4", 56 Type: "binary", 57 PURL: "pkg:generic/postgresql@15beta4", 58 Locations: locations("postgres"), 59 Metadata: metadata("postgresql-binary"), 60 }, 61 }, 62 { 63 logicalFixture: "postgres/15.1/linux-amd64", 64 expected: pkg.Package{ 65 Name: "postgresql", 66 Version: "15.1", 67 Type: "binary", 68 PURL: "pkg:generic/postgresql@15.1", 69 Locations: locations("postgres"), 70 Metadata: metadata("postgresql-binary"), 71 }, 72 }, 73 { 74 logicalFixture: "postgres/9.6.24/linux-amd64", 75 expected: pkg.Package{ 76 Name: "postgresql", 77 Version: "9.6.24", 78 Type: "binary", 79 PURL: "pkg:generic/postgresql@9.6.24", 80 Locations: locations("postgres"), 81 Metadata: metadata("postgresql-binary"), 82 }, 83 }, 84 { 85 // TODO: find original binary... 86 // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo 87 logicalFixture: "postgres/9.5alpha1/linux-amd64", 88 expected: pkg.Package{ 89 Name: "postgresql", 90 Version: "9.5alpha1", 91 Type: "binary", 92 PURL: "pkg:generic/postgresql@9.5alpha1", 93 Locations: locations("postgres"), 94 Metadata: metadata("postgresql-binary"), 95 }, 96 }, 97 { 98 logicalFixture: "mysql/8.0.34/linux-amd64", 99 expected: pkg.Package{ 100 Name: "mysql", 101 Version: "8.0.34", 102 Type: "binary", 103 PURL: "pkg:generic/mysql@8.0.34", 104 Locations: locations("mysql"), 105 Metadata: metadata("mysql-binary"), 106 }, 107 }, 108 { 109 logicalFixture: "percona-server/8.0.35/linux-amd64", 110 expected: pkg.Package{ 111 Name: "percona-server", 112 Version: "8.0.35", 113 Type: "binary", 114 PURL: "pkg:generic/percona-server@8.0.35", 115 Locations: locations("mysql"), 116 Metadata: metadata("mysql-binary"), 117 }, 118 }, 119 { 120 logicalFixture: "percona-xtradb-cluster/8.0.34/linux-amd64", 121 expected: pkg.Package{ 122 Name: "percona-xtradb-cluster", 123 Version: "8.0.34", 124 Type: "binary", 125 PURL: "pkg:generic/percona-xtradb-cluster@8.0.34", 126 Locations: locations("mysql"), 127 Metadata: metadata("mysql-binary"), 128 }, 129 }, 130 { 131 logicalFixture: "percona-xtrabackup/8.0.35/linux-amd64", 132 expected: pkg.Package{ 133 Name: "percona-xtrabackup", 134 Version: "8.0.35", 135 Type: "binary", 136 PURL: "pkg:generic/percona-xtrabackup@8.0.35", 137 Locations: locations("xtrabackup"), 138 Metadata: metadata("xtrabackup-binary"), 139 }, 140 }, 141 { 142 logicalFixture: "mysql/5.6.51/linux-amd64", 143 expected: pkg.Package{ 144 Name: "mysql", 145 Version: "5.6.51", 146 Type: "binary", 147 PURL: "pkg:generic/mysql@5.6.51", 148 Locations: locations("mysql"), 149 Metadata: metadata("mysql-binary"), 150 }, 151 }, 152 { 153 logicalFixture: "mariadb/10.6.15/linux-amd64", 154 expected: pkg.Package{ 155 Name: "mariadb", 156 Version: "10.6.15", 157 Type: "binary", 158 PURL: "pkg:generic/mariadb@10.6.15", 159 Locations: locations("mariadb"), 160 Metadata: metadata("mariadb-binary"), 161 }, 162 }, 163 { 164 logicalFixture: "traefik/1.7.34/linux-amd64", 165 expected: pkg.Package{ 166 Name: "traefik", 167 Version: "1.7.34", 168 Type: "binary", 169 PURL: "pkg:generic/traefik@1.7.34", 170 Locations: locations("traefik"), 171 Metadata: metadata("traefik-binary"), 172 }, 173 }, 174 { 175 logicalFixture: "traefik/2.9.6/linux-amd64", 176 expected: pkg.Package{ 177 Name: "traefik", 178 Version: "2.9.6", 179 Type: "binary", 180 PURL: "pkg:generic/traefik@2.9.6", 181 Locations: locations("traefik"), 182 Metadata: metadata("traefik-binary"), 183 }, 184 }, 185 { 186 logicalFixture: "traefik/2.10.7/linux-amd64", 187 expected: pkg.Package{ 188 Name: "traefik", 189 Version: "2.10.7", 190 Type: "binary", 191 PURL: "pkg:generic/traefik@2.10.7", 192 Locations: locations("traefik"), 193 Metadata: metadata("traefik-binary"), 194 }, 195 }, 196 { 197 logicalFixture: "memcached/1.6.18/linux-amd64", 198 expected: pkg.Package{ 199 Name: "memcached", 200 Version: "1.6.18", 201 Type: "binary", 202 PURL: "pkg:generic/memcached@1.6.18", 203 Locations: locations("memcached"), 204 Metadata: metadata("memcached-binary"), 205 }, 206 }, 207 { 208 logicalFixture: "httpd/2.4.54/linux-amd64", 209 expected: pkg.Package{ 210 Name: "httpd", 211 Version: "2.4.54", 212 Type: "binary", 213 PURL: "pkg:generic/httpd@2.4.54", 214 Locations: locations("httpd"), 215 Metadata: metadata("httpd-binary"), 216 }, 217 }, 218 { 219 // TODO: find original binary... 220 // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo 221 logicalFixture: "php-cli/8.2.1/linux-amd64", 222 expected: pkg.Package{ 223 Name: "php-cli", 224 Version: "8.2.1", 225 Type: "binary", 226 PURL: "pkg:generic/php-cli@8.2.1", 227 Locations: locations("php"), 228 Metadata: metadata("php-cli-binary"), 229 }, 230 }, 231 { 232 // TODO: find original binary... 233 // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo 234 logicalFixture: "php-fpm/8.2.1/linux-amd64", 235 expected: pkg.Package{ 236 Name: "php-fpm", 237 Version: "8.2.1", 238 Type: "binary", 239 PURL: "pkg:generic/php-fpm@8.2.1", 240 Locations: locations("php-fpm"), 241 Metadata: metadata("php-fpm-binary"), 242 }, 243 }, 244 { 245 // TODO: find original binary... 246 // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo 247 logicalFixture: "php-apache/8.2.1/linux-amd64", 248 expected: pkg.Package{ 249 Name: "libphp", 250 Version: "8.2.1", 251 Type: "binary", 252 PURL: "pkg:generic/php@8.2.1", 253 Locations: locations("libphp.so"), 254 Metadata: metadata("php-apache-binary"), 255 }, 256 }, 257 { 258 // TODO: original binary is different than whats in config.yaml 259 // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo 260 logicalFixture: "perl/5.12.5/linux-amd64", 261 expected: pkg.Package{ 262 Name: "perl", 263 Version: "5.12.5", 264 Type: "binary", 265 PURL: "pkg:generic/perl@5.12.5", 266 Locations: locations("perl"), 267 Metadata: metadata("perl-binary"), 268 }, 269 }, 270 { 271 // TODO: original binary is different than whats in config.yaml 272 // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo 273 logicalFixture: "perl/5.20.0/linux-amd64", 274 expected: pkg.Package{ 275 Name: "perl", 276 Version: "5.20.0", 277 Type: "binary", 278 PURL: "pkg:generic/perl@5.20.0", 279 Locations: locations("perl"), 280 Metadata: metadata("perl-binary"), 281 }, 282 }, 283 { 284 // TODO: original binary is different than whats in config.yaml 285 // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo 286 logicalFixture: "perl/5.37.8/linux-amd64", 287 expected: pkg.Package{ 288 Name: "perl", 289 Version: "5.37.8", 290 Type: "binary", 291 PURL: "pkg:generic/perl@5.37.8", 292 Locations: locations("perl"), 293 Metadata: metadata("perl-binary"), 294 }, 295 }, 296 { 297 logicalFixture: "haproxy/1.5.14/linux-amd64", 298 expected: pkg.Package{ 299 Name: "haproxy", 300 Version: "1.5.14", 301 Type: "binary", 302 PURL: "pkg:generic/haproxy@1.5.14", 303 Locations: locations("haproxy"), 304 Metadata: metadata("haproxy-binary"), 305 }, 306 }, 307 { 308 logicalFixture: "haproxy/1.8.22/linux-amd64", 309 expected: pkg.Package{ 310 Name: "haproxy", 311 Version: "1.8.22", 312 Type: "binary", 313 PURL: "pkg:generic/haproxy@1.8.22", 314 Locations: locations("haproxy"), 315 Metadata: metadata("haproxy-binary"), 316 }, 317 }, 318 { 319 logicalFixture: "haproxy/2.7.3/linux-amd64", 320 expected: pkg.Package{ 321 Name: "haproxy", 322 Version: "2.7.3", 323 Type: "binary", 324 PURL: "pkg:generic/haproxy@2.7.3", 325 Locations: locations("haproxy"), 326 Metadata: metadata("haproxy-binary"), 327 }, 328 }, 329 330 { 331 logicalFixture: "helm/3.11.1/linux-amd64", 332 expected: pkg.Package{ 333 Name: "helm", 334 Version: "3.11.1", 335 Type: "binary", 336 PURL: "pkg:golang/helm.sh/helm@3.11.1", 337 Locations: locations("helm"), 338 Metadata: metadata("helm"), 339 }, 340 }, 341 { 342 logicalFixture: "helm/3.10.3/linux-amd64", 343 expected: pkg.Package{ 344 Name: "helm", 345 Version: "3.10.3", 346 Type: "binary", 347 PURL: "pkg:golang/helm.sh/helm@3.10.3", 348 Locations: locations("helm"), 349 Metadata: metadata("helm"), 350 }, 351 }, 352 353 { 354 // note: dynamic (non-snippet) test case 355 logicalFixture: "redis-server/2.8.23/linux-amd64", 356 expected: pkg.Package{ 357 Name: "redis", 358 Version: "2.8.23", 359 Type: "binary", 360 PURL: "pkg:generic/redis@2.8.23", 361 Locations: locations("redis-server"), 362 Metadata: metadata("redis-binary"), 363 }, 364 }, 365 { 366 // note: dynamic (non-snippet) test case 367 logicalFixture: "redis-server/4.0.11/linux-amd64", 368 expected: pkg.Package{ 369 Name: "redis", 370 Version: "4.0.11", 371 Type: "binary", 372 PURL: "pkg:generic/redis@4.0.11", 373 Locations: locations("redis-server"), 374 Metadata: metadata("redis-binary"), 375 }, 376 }, 377 { 378 logicalFixture: "redis-server/5.0.0/linux-amd64", 379 expected: pkg.Package{ 380 Name: "redis", 381 Version: "5.0.0", 382 Type: "binary", 383 PURL: "pkg:generic/redis@5.0.0", 384 Locations: locations("redis-server"), 385 Metadata: metadata("redis-binary"), 386 }, 387 }, 388 { 389 logicalFixture: "redis-server/6.0.16/linux-amd64", 390 expected: pkg.Package{ 391 Name: "redis", 392 Version: "6.0.16", 393 Type: "binary", 394 PURL: "pkg:generic/redis@6.0.16", 395 Locations: locations("redis-server"), 396 Metadata: metadata("redis-binary"), 397 }, 398 }, 399 { 400 logicalFixture: "redis-server/7.0.0/linux-amd64", 401 expected: pkg.Package{ 402 Name: "redis", 403 Version: "7.0.0", 404 Type: "binary", 405 PURL: "pkg:generic/redis@7.0.0", 406 Locations: locations("redis-server"), 407 Metadata: metadata("redis-binary"), 408 }, 409 }, 410 { 411 logicalFixture: "redis-server/7.0.14/linux-amd64", 412 expected: pkg.Package{ 413 Name: "redis", 414 Version: "7.0.14", 415 Type: "binary", 416 PURL: "pkg:generic/redis@7.0.14", 417 Locations: locations("redis-server"), 418 Metadata: metadata("redis-binary"), 419 }, 420 }, 421 { 422 // note: dynamic (non-snippet) test case 423 logicalFixture: "redis-server/7.2.3/linux-amd64", 424 expected: pkg.Package{ 425 Name: "redis", 426 Version: "7.2.3", 427 Type: "binary", 428 PURL: "pkg:generic/redis@7.2.3", 429 Locations: locations("redis-server"), 430 Metadata: metadata("redis-binary"), 431 }, 432 }, 433 { 434 // note: dynamic (non-snippet) test case 435 logicalFixture: "redis-server/7.2.3/linux-arm64", 436 expected: pkg.Package{ 437 Name: "redis", 438 Version: "7.2.3", 439 Type: "binary", 440 PURL: "pkg:generic/redis@7.2.3", 441 Locations: locations("redis-server"), 442 Metadata: metadata("redis-binary"), 443 }, 444 }, 445 { 446 logicalFixture: "python-shared-lib/3.7.4/linux-amd64", 447 expected: pkg.Package{ 448 Name: "python", 449 Version: "3.7.4", 450 PURL: "pkg:generic/python@3.7.4", 451 Locations: locations("libpython3.7m.so.1.0"), 452 Metadata: metadata("python-binary-lib"), 453 }, 454 }, 455 456 { 457 // note: dynamic (non-snippet) test case 458 logicalFixture: "python-slim-shared-libs/3.11/linux-amd64", 459 expected: pkg.Package{ 460 Name: "python", 461 Version: "3.11.2", 462 PURL: "pkg:generic/python@3.11.2", 463 Locations: locations("python3.11", "libpython3.11.so.1.0"), 464 Metadata: pkg.BinarySignature{ 465 Matches: []pkg.ClassifierMatch{ 466 match("python-binary", "python3.11"), 467 match("python-binary", "libpython3.11.so.1.0"), 468 match("python-binary-lib", "libpython3.11.so.1.0"), 469 }, 470 }, 471 }, 472 }, 473 { 474 // note: dynamic (non-snippet) test case 475 logicalFixture: "python-rhel-shared-libs/3.9/linux-amd64", 476 expected: pkg.Package{ 477 Name: "python", 478 Version: "3.9.13", 479 PURL: "pkg:generic/python@3.9.13", 480 Locations: locations("python3.9", "libpython3.9.so.1.0"), 481 Metadata: pkg.BinarySignature{ 482 Matches: []pkg.ClassifierMatch{ 483 match("python-binary", "python3.9"), 484 match("python-binary", "libpython3.9.so.1.0"), 485 match("python-binary-lib", "libpython3.9.so.1.0"), 486 }, 487 }, 488 }, 489 }, 490 { 491 // note: dynamic (non-snippet) test case 492 logicalFixture: "python3.9/3.9.16/linux-amd64", 493 expected: pkg.Package{ 494 Name: "python", 495 Version: "3.9.2", 496 PURL: "pkg:generic/python@3.9.2", 497 Locations: locations("python3.9"), 498 Metadata: pkg.BinarySignature{ 499 Matches: []pkg.ClassifierMatch{ 500 match("python-binary", "python3.9"), 501 }, 502 }, 503 }, 504 }, 505 { 506 // note: dynamic (non-snippet) test case 507 logicalFixture: "python-alpine-shared-libs/3.4/linux-amd64", 508 expected: pkg.Package{ 509 Name: "python", 510 Version: "3.4.10", 511 PURL: "pkg:generic/python@3.4.10", 512 Locations: locations("python3.4", "libpython3.4m.so.1.0"), 513 Metadata: pkg.BinarySignature{ 514 Matches: []pkg.ClassifierMatch{ 515 match("python-binary", "python3.4"), 516 match("python-binary", "libpython3.4m.so.1.0"), 517 match("python-binary-lib", "libpython3.4m.so.1.0"), 518 }, 519 }, 520 }, 521 }, 522 { 523 // TODO: find original binary... 524 // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo 525 logicalFixture: "python-with-incorrect-match/3.5.3/linux-amd64", 526 expected: pkg.Package{ 527 Name: "python", 528 Version: "3.5.3", 529 PURL: "pkg:generic/python@3.5.3", 530 Locations: locations("python3.5"), 531 Metadata: metadata("python-binary"), 532 }, 533 }, 534 { 535 // TODO: find original binary... 536 // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo 537 logicalFixture: "python/3.6.3/linux-amd64", 538 expected: pkg.Package{ 539 Name: "python", 540 Version: "3.6.3", 541 PURL: "pkg:generic/python@3.6.3", 542 Locations: locations("python3.6"), 543 Metadata: metadata("python-binary"), 544 }, 545 }, 546 { 547 // TODO: find original binary... 548 // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo 549 logicalFixture: "python-duplicates/3.8.16/linux-amd64", 550 expected: pkg.Package{ 551 Name: "python", 552 Version: "3.8.16", 553 Type: "binary", 554 PURL: "pkg:generic/python@3.8.16", 555 Locations: locations("dir/python3.8", "python3.8", "libpython3.8.so"), 556 Metadata: pkg.BinarySignature{ 557 Matches: []pkg.ClassifierMatch{ 558 match("python-binary", "dir/python3.8"), 559 match("python-binary", "python3.8"), 560 match("python-binary-lib", "libpython3.8.so"), 561 }, 562 }, 563 }, 564 }, 565 { 566 logicalFixture: "pypy-shared-lib/7.3.14/linux-amd64", 567 expected: pkg.Package{ 568 Name: "pypy", 569 Version: "7.3.14", 570 PURL: "pkg:generic/pypy@7.3.14", 571 Locations: locations("libpypy3.9-c.so"), 572 Metadata: metadata("pypy-binary-lib"), 573 }, 574 }, 575 { 576 logicalFixture: "go/1.21.3/linux-amd64", 577 expected: pkg.Package{ 578 Name: "go", 579 Version: "1.21.3", 580 PURL: "pkg:generic/go@1.21.3", 581 Locations: locations("go"), 582 Metadata: metadata("go-binary"), 583 }, 584 }, 585 { 586 logicalFixture: "node/19.2.0/linux-amd64", 587 expected: pkg.Package{ 588 Name: "node", 589 Version: "19.2.0", 590 PURL: "pkg:generic/node@19.2.0", 591 Locations: locations("node"), 592 Metadata: metadata("nodejs-binary"), 593 }, 594 }, 595 { 596 // TODO: find original binary... 597 // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo 598 logicalFixture: "go-version-hint/1.15/any", 599 expected: pkg.Package{ 600 Name: "go", 601 Version: "1.15", 602 PURL: "pkg:generic/go@1.15", 603 Locations: locations("VERSION"), 604 Metadata: metadata("go-binary-hint"), 605 }, 606 }, 607 { 608 // note: this is testing BUSYBOX which is typically through a link to "[" (in this case a symlink but in 609 // practice this is often a hard link). 610 logicalFixture: `busybox/1.36.1/linux-amd64`, 611 expected: pkg.Package{ 612 Name: "busybox", 613 Version: "1.36.1", 614 PURL: "pkg:generic/busybox@1.36.1", 615 Locations: locations("["), // note: busybox is a link to [ 616 Metadata: metadata("busybox-binary", "[", "busybox"), 617 }, 618 }, 619 { 620 logicalFixture: "java-jre-openjdk/1.8.0_352-b08/linux-amd64", 621 expected: pkg.Package{ 622 Name: "java/jre", 623 Version: "1.8.0_352-b08", 624 Type: "binary", 625 PURL: "pkg:generic/java/jre@1.8.0_352-b08", 626 Locations: locations("java"), 627 Metadata: metadata("java-binary-openjdk", "java"), 628 }, 629 }, 630 { 631 logicalFixture: "java-jre-openjdk/11.0.17/linux-amd64", 632 expected: pkg.Package{ 633 Name: "java/jre", 634 Version: "11.0.17+8-LTS", 635 Type: "binary", 636 PURL: "pkg:generic/java/jre@11.0.17%2B8-LTS", 637 Locations: locations("java"), 638 Metadata: metadata("java-binary-openjdk", "java"), 639 }, 640 }, 641 { 642 logicalFixture: "java-jre-openjdk-eclipse/11.0.22/linux-amd64", 643 expected: pkg.Package{ 644 Name: "java/jre", 645 Version: "11.0.22+7", 646 Type: "binary", 647 PURL: "pkg:generic/java/jre@11.0.22%2B7", 648 Locations: locations("java"), 649 Metadata: metadata("java-binary-openjdk", "java"), 650 }, 651 }, 652 { 653 logicalFixture: "java-jre-openjdk-arm64-eclipse/11.0.22/linux-arm64", 654 expected: pkg.Package{ 655 Name: "java/jre", 656 Version: "11.0.22+7", 657 Type: "binary", 658 PURL: "pkg:generic/java/jre@11.0.22%2B7", 659 Locations: locations("java"), 660 Metadata: metadata("java-binary-openjdk", "java"), 661 }, 662 }, 663 { 664 logicalFixture: "java-graal-openjdk/17.0.3+7-jvmci-22.1-b06/linux-amd64", 665 expected: pkg.Package{ 666 Name: "java/graalvm", 667 Version: "17.0.3+7-jvmci-22.1-b06", 668 Type: "binary", 669 PURL: "pkg:generic/java/graalvm@17.0.3%2B7-jvmci-22.1-b06", 670 Locations: locations("java"), 671 Metadata: metadata("java-binary-graalvm", "java"), 672 }, 673 }, 674 { 675 // TODO: find original binary... 676 // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo 677 logicalFixture: "java-jre-oracle/19.0.1/linux-amd64", 678 expected: pkg.Package{ 679 Name: "java/jre", 680 Version: "19.0.1+10-21", 681 Type: "binary", 682 PURL: "pkg:generic/java/jre@19.0.1%2B10-21", 683 Locations: locations("java"), 684 Metadata: metadata("java-binary-oracle", "java"), 685 }, 686 }, 687 { 688 // TODO: find original binary... 689 // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo 690 logicalFixture: "java-jre-oracle/19.0.1/darwin", 691 expected: pkg.Package{ 692 Name: "java/jre", 693 Version: "19.0.1+10-21", 694 Type: "binary", 695 PURL: "pkg:generic/java/jre@19.0.1%2B10-21", 696 Locations: locations("java"), 697 Metadata: metadata("java-binary-oracle", "java"), 698 }, 699 }, 700 { 701 logicalFixture: "java-jre-ibm/1.8.0_391/linux-amd64", 702 expected: pkg.Package{ 703 Name: "java/jre", 704 Version: "1.8.0-foreman_2023_10_12_13_27-b00", 705 Type: "binary", 706 PURL: "pkg:generic/java/jre@1.8.0-foreman_2023_10_12_13_27-b00", 707 Locations: locations("java"), 708 Metadata: metadata("java-binary-ibm", "java"), 709 }, 710 }, 711 { 712 logicalFixture: "java-jdk-openjdk/21.0.2+13-LTS/linux-amd64", 713 expected: pkg.Package{ 714 Name: "java/jdk", 715 Version: "21.0.2+13-LTS", 716 Type: "binary", 717 PURL: "pkg:generic/java/jdk@21.0.2%2B13-LTS", 718 Locations: locations("jdb"), 719 Metadata: metadata("java-binary-jdk", "java"), 720 }, 721 }, 722 { 723 logicalFixture: "rust-libstd/1.50.0/linux-amd64", 724 expected: pkg.Package{ 725 Name: "rust", 726 Version: "1.50.0", 727 Type: "binary", 728 PURL: "pkg:generic/rust@1.50.0", 729 Locations: locations("libstd-6f77337c1826707d.so"), 730 Metadata: metadata("rust-standard-library-linux"), 731 }, 732 }, 733 { 734 // TODO: find original binary... 735 // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo 736 logicalFixture: "rust-libstd/1.50.0/darwin", 737 expected: pkg.Package{ 738 Name: "rust", 739 Version: "1.50.0", 740 Type: "binary", 741 PURL: "pkg:generic/rust@1.50.0", 742 Locations: locations("libstd-f6f9eec1635e636a.dylib"), 743 Metadata: metadata("rust-standard-library-macos"), 744 }, 745 }, 746 { 747 // TODO: find original binary... 748 // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo 749 logicalFixture: "rust-libstd/1.67.1/darwin", 750 expected: pkg.Package{ 751 Name: "rust", 752 Version: "1.67.1", 753 Type: "binary", 754 PURL: "pkg:generic/rust@1.67.1", 755 Locations: locations("libstd-16f2b65e77054c42.dylib"), 756 Metadata: metadata("rust-standard-library-macos"), 757 }, 758 }, 759 { 760 logicalFixture: "rust-libstd-musl/1.67.1/linux-amd64", 761 expected: pkg.Package{ 762 Name: "rust", 763 Version: "1.67.1", 764 Type: "binary", 765 PURL: "pkg:generic/rust@1.67.1", 766 Locations: locations("libstd-86aefecbddda356d.so"), 767 Metadata: metadata("rust-standard-library-linux"), 768 }, 769 }, 770 { 771 logicalFixture: "rust-libstd/1.67.1/linux-amd64", 772 expected: pkg.Package{ 773 Name: "rust", 774 Version: "1.67.1", 775 Type: "binary", 776 PURL: "pkg:generic/rust@1.67.1", 777 Locations: locations("libstd-c6192dd4c4d410ac.so"), 778 Metadata: metadata("rust-standard-library-linux"), 779 }, 780 }, 781 { 782 // note: dynamic (non-snippet) test case 783 784 name: "positive-ruby-3.2.1", 785 logicalFixture: "ruby-bullseye-shared-libs/3.2.1/linux-amd64", 786 expected: pkg.Package{ 787 Name: "ruby", 788 Version: "3.2.1", 789 Type: "binary", 790 PURL: "pkg:generic/ruby@3.2.1", 791 Locations: locations("ruby", "libruby.so.3.2.1"), 792 Metadata: pkg.BinarySignature{ 793 Matches: []pkg.ClassifierMatch{ 794 match("ruby-binary", "ruby"), 795 match("ruby-binary", "libruby.so.3.2.1"), 796 }, 797 }, 798 }, 799 }, 800 { 801 // note: dynamic (non-snippet) test case 802 logicalFixture: "ruby-bullseye-shared-libs/2.7.7/linux-amd64", 803 expected: pkg.Package{ 804 Name: "ruby", 805 Version: "2.7.7p221", 806 Type: "binary", 807 PURL: "pkg:generic/ruby@2.7.7p221", 808 Locations: locations("ruby", "libruby.so.2.7.7"), 809 Metadata: pkg.BinarySignature{ 810 Matches: []pkg.ClassifierMatch{ 811 match("ruby-binary", "ruby"), 812 match("ruby-binary", "libruby.so.2.7.7"), 813 }, 814 }, 815 }, 816 }, 817 { 818 // note: dynamic (non-snippet) test case 819 logicalFixture: "ruby-shared-libs/2.6.10/linux-amd64", 820 expected: pkg.Package{ 821 Name: "ruby", 822 Version: "2.6.10p210", 823 Type: "binary", 824 PURL: "pkg:generic/ruby@2.6.10p210", 825 Locations: locations("ruby", "libruby.so.2.6.10"), 826 Metadata: pkg.BinarySignature{ 827 Matches: []pkg.ClassifierMatch{ 828 match("ruby-binary", "ruby"), 829 match("ruby-binary", "libruby.so.2.6.10"), 830 }, 831 }, 832 }, 833 }, 834 { 835 logicalFixture: "ruby/1.9.3p551/linux-amd64", 836 expected: pkg.Package{ 837 Name: "ruby", 838 Version: "1.9.3p551", 839 Type: "binary", 840 PURL: "pkg:generic/ruby@1.9.3p551", 841 Locations: locations("ruby"), 842 Metadata: metadata("ruby-binary"), 843 }, 844 }, 845 { 846 logicalFixture: "consul/1.15.2/linux-amd64", 847 expected: pkg.Package{ 848 Name: "consul", 849 Version: "1.15.2", 850 Type: "binary", 851 PURL: "pkg:golang/github.com/hashicorp/consul@1.15.2", 852 Locations: locations("consul"), 853 Metadata: metadata("consul-binary"), 854 }, 855 }, 856 { 857 logicalFixture: "erlang/25.3.2.6/linux-amd64", 858 expected: pkg.Package{ 859 Name: "erlang", 860 Version: "25.3.2.6", 861 Type: "binary", 862 PURL: "pkg:generic/erlang@25.3.2.6", 863 Locations: locations("erlexec"), 864 Metadata: metadata("erlang-binary"), 865 }, 866 }, 867 { 868 logicalFixture: "erlang/26.2.0.0/linux-amd64", 869 expected: pkg.Package{ 870 Name: "erlang", 871 Version: "26.2", 872 Type: "binary", 873 PURL: "pkg:generic/erlang@26.2", 874 Locations: locations("erlexec"), 875 Metadata: metadata("erlang-binary"), 876 }, 877 }, 878 { 879 logicalFixture: "erlang/26.2.4/linux-amd64", 880 expected: pkg.Package{ 881 Name: "erlang", 882 Version: "26.2.4", 883 Type: "binary", 884 PURL: "pkg:generic/erlang@26.2.4", 885 Locations: locations("liberts_internal.a"), 886 Metadata: metadata("erlang-library"), 887 }, 888 }, 889 { 890 logicalFixture: "nginx/1.25.1/linux-amd64", 891 expected: pkg.Package{ 892 Name: "nginx", 893 Version: "1.25.1", 894 Type: "binary", 895 PURL: "pkg:generic/nginx@1.25.1", 896 Locations: locations("nginx"), 897 Metadata: metadata("nginx-binary"), 898 }, 899 }, 900 { 901 logicalFixture: "nginx-openresty/1.21.4.3/linux-amd64", 902 expected: pkg.Package{ 903 Name: "nginx", 904 Version: "1.21.4", 905 Type: "binary", 906 PURL: "pkg:generic/nginx@1.21.4", 907 Locations: locations("nginx"), 908 Metadata: metadata("nginx-binary"), 909 }, 910 }, 911 { 912 logicalFixture: "bash/5.1.16/linux-amd64", 913 expected: pkg.Package{ 914 Name: "bash", 915 Version: "5.1.16", 916 Type: "binary", 917 PURL: "pkg:generic/bash@5.1.16", 918 Locations: locations("bash"), 919 Metadata: metadata("bash-binary"), 920 }, 921 }, 922 { 923 logicalFixture: "openssl/3.1.4/linux-amd64", 924 expected: pkg.Package{ 925 Name: "openssl", 926 Version: "3.1.4", 927 Type: "binary", 928 PURL: "pkg:generic/openssl@3.1.4", 929 Locations: locations("openssl"), 930 Metadata: metadata("openssl-binary"), 931 }, 932 }, 933 { 934 logicalFixture: "openssl/1.1.1w/linux-arm64", 935 expected: pkg.Package{ 936 Name: "openssl", 937 Version: "1.1.1w", 938 Type: "binary", 939 PURL: "pkg:generic/openssl@1.1.1w", 940 Locations: locations("openssl"), 941 Metadata: metadata("openssl-binary"), 942 }, 943 }, 944 { 945 logicalFixture: "gcc/12.3.0/linux-amd64", 946 expected: pkg.Package{ 947 Name: "gcc", 948 Version: "12.3.0", 949 Type: "binary", 950 PURL: "pkg:generic/gcc@12.3.0", 951 Locations: locations("gcc"), 952 Metadata: metadata("gcc-binary"), 953 }, 954 }, 955 { 956 logicalFixture: "wp/2.9.0/linux-amd64", 957 expected: pkg.Package{ 958 Name: "wp-cli", 959 Version: "2.9.0", 960 Type: "binary", 961 PURL: "pkg:generic/wp-cli@2.9.0", 962 Locations: locations("wp"), 963 Metadata: metadata("wordpress-cli-binary"), 964 }, 965 }, 966 } 967 968 for _, test := range tests { 969 t.Run(test.logicalFixture, func(t *testing.T) { 970 c := NewClassifierCataloger(DefaultClassifierCatalogerConfig()) 971 972 // logicalFixture is the logical path to the full binary or snippet. This is relative to the test-fixtures/classifiers/snippets 973 // or test-fixtures/classifiers/bin directory . Snippets are searched for first, and if not found, then existing binaries are 974 // used. If no binary or snippet is found the test will fail. If '-must-use-original-binaries' is used the only 975 // full binaries are tested (no snippets), and if no binary is found the test will be skipped. 976 path := testutil.SnippetOrBinary(t, test.logicalFixture, *mustUseOriginalBinaries) 977 978 src, err := directorysource.NewFromPath(path) 979 require.NoError(t, err) 980 981 resolver, err := src.FileResolver(source.SquashedScope) 982 require.NoError(t, err) 983 984 packages, _, err := c.Catalog(context.Background(), resolver) 985 require.NoError(t, err) 986 987 require.Len(t, packages, 1, "mismatched package count") 988 989 assertPackagesAreEqual(t, test.expected, packages[0]) 990 }) 991 } 992 } 993 994 func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) { 995 tests := []struct { 996 name string 997 fixtureImage string 998 expected pkg.Package 999 }{ 1000 { 1001 name: "busybox-regression", 1002 fixtureImage: "image-busybox", 1003 expected: pkg.Package{ 1004 Name: "busybox", 1005 Version: "1.35.0", 1006 PURL: "pkg:generic/busybox@1.35.0", 1007 Locations: locations("/bin/["), 1008 Metadata: metadata("busybox-binary", "/bin/[", "/bin/busybox"), 1009 }, 1010 }, 1011 } 1012 1013 for _, test := range tests { 1014 t.Run(test.name, func(t *testing.T) { 1015 c := NewClassifierCataloger(DefaultClassifierCatalogerConfig()) 1016 1017 img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage) 1018 src := stereoscopesource.New(img, stereoscopesource.ImageConfig{ 1019 Reference: test.fixtureImage, 1020 }) 1021 1022 resolver, err := src.FileResolver(source.SquashedScope) 1023 require.NoError(t, err) 1024 1025 packages, _, err := c.Catalog(context.Background(), resolver) 1026 require.NoError(t, err) 1027 1028 for _, p := range packages { 1029 expectedLocations := test.expected.Locations.ToSlice() 1030 gotLocations := p.Locations.ToSlice() 1031 require.Len(t, gotLocations, len(expectedLocations)) 1032 1033 for i, expectedLocation := range expectedLocations { 1034 gotLocation := gotLocations[i] 1035 if expectedLocation.RealPath != gotLocation.RealPath { 1036 t.Fatalf("locations do not match; expected: %v got: %v", expectedLocations, gotLocations) 1037 } 1038 } 1039 1040 assertPackagesAreEqual(t, test.expected, p) 1041 } 1042 }) 1043 } 1044 } 1045 1046 func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) { 1047 c := NewClassifierCataloger(DefaultClassifierCatalogerConfig()) 1048 1049 src, err := directorysource.NewFromPath("test-fixtures/classifiers/negative") 1050 assert.NoError(t, err) 1051 1052 resolver, err := src.FileResolver(source.SquashedScope) 1053 assert.NoError(t, err) 1054 1055 actualResults, _, err := c.Catalog(context.Background(), resolver) 1056 assert.NoError(t, err) 1057 assert.Equal(t, 0, len(actualResults)) 1058 } 1059 1060 func Test_Cataloger_CustomClassifiers(t *testing.T) { 1061 defaultClassifers := DefaultClassifiers() 1062 1063 golangExpected := pkg.Package{ 1064 Name: "go", 1065 Version: "1.14", 1066 PURL: "pkg:generic/go@1.14", 1067 Locations: locations("go"), 1068 Metadata: metadata("go-binary"), 1069 } 1070 customExpected := pkg.Package{ 1071 Name: "foo", 1072 Version: "1.2.3", 1073 PURL: "pkg:generic/foo@1.2.3", 1074 Locations: locations("foo"), 1075 Metadata: metadata("foo-binary"), 1076 } 1077 fooClassifier := Classifier{ 1078 Class: "foo-binary", 1079 FileGlob: "**/foo", 1080 EvidenceMatcher: FileContentsVersionMatcher( 1081 `(?m)foobar\s(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`), 1082 Package: "foo", 1083 PURL: mustPURL("pkg:generic/foo@version"), 1084 CPEs: singleCPE("cpe:2.3:a:foo:foo:*:*:*:*:*:*:*:*"), 1085 } 1086 1087 tests := []struct { 1088 name string 1089 config ClassifierCatalogerConfig 1090 fixtureDir string 1091 expected *pkg.Package 1092 }{ 1093 { 1094 name: "empty-negative", 1095 config: ClassifierCatalogerConfig{ 1096 Classifiers: []Classifier{}, 1097 }, 1098 fixtureDir: "test-fixtures/custom/go-1.14", 1099 expected: nil, 1100 }, 1101 { 1102 name: "default-positive", 1103 config: ClassifierCatalogerConfig{ 1104 Classifiers: defaultClassifers, 1105 }, 1106 fixtureDir: "test-fixtures/custom/go-1.14", 1107 expected: &golangExpected, 1108 }, 1109 { 1110 name: "nodefault-negative", 1111 config: ClassifierCatalogerConfig{ 1112 Classifiers: []Classifier{fooClassifier}, 1113 }, 1114 fixtureDir: "test-fixtures/custom/go-1.14", 1115 expected: nil, 1116 }, 1117 { 1118 name: "default-extended-positive", 1119 config: ClassifierCatalogerConfig{ 1120 Classifiers: append( 1121 append([]Classifier{}, defaultClassifers...), 1122 fooClassifier, 1123 ), 1124 }, 1125 fixtureDir: "test-fixtures/custom/go-1.14", 1126 expected: &golangExpected, 1127 }, 1128 { 1129 name: "default-custom-negative", 1130 config: ClassifierCatalogerConfig{ 1131 1132 Classifiers: append( 1133 append([]Classifier{}, defaultClassifers...), 1134 Classifier{ 1135 Class: "foo-binary", 1136 FileGlob: "**/foo", 1137 EvidenceMatcher: FileContentsVersionMatcher(`(?m)not there`), 1138 Package: "foo", 1139 PURL: mustPURL("pkg:generic/foo@version"), 1140 CPEs: singleCPE("cpe:2.3:a:foo:foo:*:*:*:*:*:*:*:*"), 1141 }, 1142 ), 1143 }, 1144 fixtureDir: "test-fixtures/custom/extra", 1145 expected: nil, 1146 }, 1147 { 1148 name: "default-cutsom-positive", 1149 config: ClassifierCatalogerConfig{ 1150 Classifiers: append( 1151 append([]Classifier{}, defaultClassifers...), 1152 fooClassifier, 1153 ), 1154 }, 1155 fixtureDir: "test-fixtures/custom/extra", 1156 expected: &customExpected, 1157 }, 1158 } 1159 for _, test := range tests { 1160 t.Run(test.name, func(t *testing.T) { 1161 c := NewClassifierCataloger(test.config) 1162 1163 src, err := directorysource.NewFromPath(test.fixtureDir) 1164 require.NoError(t, err) 1165 1166 resolver, err := src.FileResolver(source.SquashedScope) 1167 require.NoError(t, err) 1168 1169 packages, _, err := c.Catalog(context.Background(), resolver) 1170 require.NoError(t, err) 1171 1172 if test.expected == nil { 1173 assert.Equal(t, 0, len(packages)) 1174 } else { 1175 require.Len(t, packages, 1) 1176 1177 assertPackagesAreEqual(t, *test.expected, packages[0]) 1178 } 1179 }) 1180 } 1181 } 1182 1183 func locations(locations ...string) file.LocationSet { 1184 var locs []file.Location 1185 for _, s := range locations { 1186 locs = append(locs, file.NewLocation(s)) 1187 } 1188 return file.NewLocationSet(locs...) 1189 } 1190 1191 // metadata paths are: realPath, virtualPath 1192 func metadata(classifier string, paths ...string) pkg.BinarySignature { 1193 return pkg.BinarySignature{ 1194 Matches: []pkg.ClassifierMatch{ 1195 match(classifier, paths...), 1196 }, 1197 } 1198 } 1199 1200 // match paths are: realPath, virtualPath 1201 func match(classifier string, paths ...string) pkg.ClassifierMatch { 1202 realPath := "" 1203 if len(paths) > 0 { 1204 realPath = paths[0] 1205 } 1206 virtualPath := "" 1207 if len(paths) > 1 { 1208 virtualPath = paths[1] 1209 } 1210 return pkg.ClassifierMatch{ 1211 Classifier: classifier, 1212 Location: file.NewVirtualLocationFromCoordinates( 1213 file.Coordinates{ 1214 RealPath: realPath, 1215 }, 1216 virtualPath, 1217 ), 1218 } 1219 } 1220 1221 func assertPackagesAreEqual(t *testing.T, expected pkg.Package, p pkg.Package) { 1222 var failMessages []string 1223 expectedLocations := expected.Locations.ToSlice() 1224 gotLocations := p.Locations.ToSlice() 1225 1226 if len(expectedLocations) != len(gotLocations) { 1227 failMessages = append(failMessages, "locations are not equal length") 1228 } else { 1229 for i, expectedLocation := range expectedLocations { 1230 gotLocation := gotLocations[i] 1231 if expectedLocation.RealPath != gotLocation.RealPath { 1232 failMessages = append(failMessages, fmt.Sprintf("locations do not match; expected: %v got: %v", expectedLocation.RealPath, gotLocation.RealPath)) 1233 } 1234 } 1235 } 1236 1237 m1 := expected.Metadata.(pkg.BinarySignature).Matches 1238 m2 := p.Metadata.(pkg.BinarySignature).Matches 1239 matches := true 1240 if len(m1) == len(m2) { 1241 for i, m1 := range m1 { 1242 m2 := m2[i] 1243 if m1.Classifier != m2.Classifier { 1244 matches = false 1245 break 1246 } 1247 } 1248 } else { 1249 matches = false 1250 } 1251 1252 if !matches { 1253 failMessages = append(failMessages, "classifier matches not equal") 1254 } 1255 if expected.Name != p.Name || 1256 expected.Version != p.Version || 1257 expected.PURL != p.PURL { 1258 failMessages = append(failMessages, "packages do not match") 1259 } 1260 1261 if len(failMessages) > 0 { 1262 assert.Failf(t, strings.Join(failMessages, "; "), "diff: %s", 1263 cmp.Diff(expected, p, 1264 cmp.Transformer("Locations", func(l file.LocationSet) []file.Location { 1265 return l.ToSlice() 1266 }), 1267 cmpopts.IgnoreUnexported(pkg.Package{}, file.LocationData{}), 1268 cmpopts.IgnoreFields(pkg.Package{}, "CPEs", "FoundBy", "Type", "Locations", "Licenses"), 1269 ), 1270 ) 1271 } 1272 } 1273 1274 type panicyResolver struct { 1275 searchCalled bool 1276 } 1277 1278 func (p *panicyResolver) FilesByExtension(_ ...string) ([]file.Location, error) { 1279 p.searchCalled = true 1280 return nil, errors.New("not implemented") 1281 } 1282 1283 func (p *panicyResolver) FilesByBasename(_ ...string) ([]file.Location, error) { 1284 p.searchCalled = true 1285 return nil, errors.New("not implemented") 1286 } 1287 1288 func (p *panicyResolver) FilesByBasenameGlob(_ ...string) ([]file.Location, error) { 1289 p.searchCalled = true 1290 return nil, errors.New("not implemented") 1291 } 1292 1293 func (p *panicyResolver) FileContentsByLocation(_ file.Location) (io.ReadCloser, error) { 1294 p.searchCalled = true 1295 return nil, errors.New("not implemented") 1296 } 1297 1298 func (p *panicyResolver) HasPath(_ string) bool { 1299 return true 1300 } 1301 1302 func (p *panicyResolver) FilesByPath(_ ...string) ([]file.Location, error) { 1303 p.searchCalled = true 1304 return nil, errors.New("not implemented") 1305 } 1306 1307 func (p *panicyResolver) FilesByGlob(_ ...string) ([]file.Location, error) { 1308 p.searchCalled = true 1309 return nil, errors.New("not implemented") 1310 } 1311 1312 func (p *panicyResolver) FilesByMIMEType(_ ...string) ([]file.Location, error) { 1313 p.searchCalled = true 1314 return nil, errors.New("not implemented") 1315 } 1316 1317 func (p *panicyResolver) RelativeFileByPath(_ file.Location, _ string) *file.Location { 1318 return nil 1319 } 1320 1321 func (p *panicyResolver) AllLocations(_ context.Context) <-chan file.Location { 1322 return nil 1323 } 1324 1325 func (p *panicyResolver) FileMetadataByLocation(_ file.Location) (file.Metadata, error) { 1326 return file.Metadata{}, errors.New("not implemented") 1327 } 1328 1329 var _ file.Resolver = (*panicyResolver)(nil) 1330 1331 func Test_Cataloger_ResilientToErrors(t *testing.T) { 1332 c := NewClassifierCataloger(DefaultClassifierCatalogerConfig()) 1333 1334 resolver := &panicyResolver{} 1335 _, _, err := c.Catalog(context.Background(), resolver) 1336 assert.NoError(t, err) 1337 assert.True(t, resolver.searchCalled) 1338 } 1339 1340 func TestCatalogerConfig_MarshalJSON(t *testing.T) { 1341 1342 tests := []struct { 1343 name string 1344 cfg ClassifierCatalogerConfig 1345 want string 1346 wantErr assert.ErrorAssertionFunc 1347 }{ 1348 { 1349 name: "only show names of classes", 1350 cfg: ClassifierCatalogerConfig{ 1351 Classifiers: []Classifier{ 1352 { 1353 Class: "class", 1354 FileGlob: "glob", 1355 EvidenceMatcher: FileContentsVersionMatcher(".thing"), 1356 Package: "pkg", 1357 PURL: packageurl.PackageURL{ 1358 Type: "type", 1359 Namespace: "namespace", 1360 Name: "name", 1361 Version: "version", 1362 Qualifiers: nil, 1363 Subpath: "subpath", 1364 }, 1365 CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource)}, 1366 }, 1367 }, 1368 }, 1369 want: `["class"]`, 1370 }, 1371 } 1372 for _, tt := range tests { 1373 t.Run(tt.name, func(t *testing.T) { 1374 if tt.wantErr == nil { 1375 tt.wantErr = assert.NoError 1376 } 1377 got, err := tt.cfg.MarshalJSON() 1378 if !tt.wantErr(t, err) { 1379 return 1380 } 1381 assert.Equal(t, tt.want, string(got)) 1382 }) 1383 } 1384 }