1const fs = require('fs') 2const zlib = require('zlib') 3const path = require('path') 4const t = require('tap') 5 6const { default: tufmock } = require('@tufjs/repo-mock') 7const { load: loadMockNpm } = require('../../fixtures/mock-npm') 8const MockRegistry = require('@npmcli/mock-registry') 9 10const gunzip = zlib.gunzipSync 11const gzip = zlib.gzipSync 12 13t.cleanSnapshot = str => str.replace(/package(s)? in [0-9]+[a-z]+/g, 'package$1 in xxx') 14 15const tree = { 16 'package.json': JSON.stringify({ 17 name: 'test-dep', 18 version: '1.0.0', 19 dependencies: { 20 'test-dep-a': '*', 21 }, 22 }), 23 'package-lock.json': JSON.stringify({ 24 name: 'test-dep', 25 version: '1.0.0', 26 lockfileVersion: 2, 27 requires: true, 28 packages: { 29 '': { 30 xname: 'scratch', 31 version: '1.0.0', 32 dependencies: { 33 'test-dep-a': '*', 34 }, 35 devDependencies: {}, 36 }, 37 'node_modules/test-dep-a': { 38 name: 'test-dep-a', 39 version: '1.0.0', 40 }, 41 }, 42 dependencies: { 43 'test-dep-a': { 44 version: '1.0.0', 45 }, 46 }, 47 }), 48 'test-dep-a-vuln': { 49 'package.json': JSON.stringify({ 50 name: 'test-dep-a', 51 version: '1.0.0', 52 }), 53 'vulnerable.txt': 'vulnerable test-dep-a', 54 }, 55 'test-dep-a-fixed': { 56 'package.json': JSON.stringify({ 57 name: 'test-dep-a', 58 version: '1.0.1', 59 }), 60 'fixed.txt': 'fixed test-dep-a', 61 }, 62} 63 64t.test('normal audit', async t => { 65 const { npm, joinedOutput } = await loadMockNpm(t, { 66 prefixDir: tree, 67 }) 68 const registry = new MockRegistry({ 69 tap: t, 70 registry: npm.config.get('registry'), 71 }) 72 73 const manifest = registry.manifest({ 74 name: 'test-dep-a', 75 packuments: [{ version: '1.0.0' }, { version: '1.0.1' }], 76 }) 77 await registry.package({ manifest }) 78 const advisory = registry.advisory({ 79 id: 100, 80 vulnerable_versions: '<1.0.1', 81 }) 82 const bulkBody = gzip(JSON.stringify({ 'test-dep-a': ['1.0.0'] })) 83 registry.nock.post('/-/npm/v1/security/advisories/bulk', bulkBody) 84 .reply(200, { 85 'test-dep-a': [advisory], 86 }) 87 88 await npm.exec('audit', []) 89 t.ok(process.exitCode, 'would have exited uncleanly') 90 t.matchSnapshot(joinedOutput()) 91}) 92 93t.test('fallback audit ', async t => { 94 const { npm, joinedOutput } = await loadMockNpm(t, { 95 prefixDir: tree, 96 }) 97 const registry = new MockRegistry({ 98 tap: t, 99 registry: npm.config.get('registry'), 100 }) 101 const manifest = registry.manifest({ 102 name: 'test-dep-a', 103 packuments: [{ version: '1.0.0' }, { version: '1.0.1' }], 104 }) 105 await registry.package({ manifest }) 106 const advisory = registry.advisory({ 107 id: 100, 108 module_name: 'test-dep-a', 109 vulnerable_versions: '<1.0.1', 110 findings: [{ version: '1.0.0', paths: ['test-dep-a'] }], 111 }) 112 registry.nock 113 .post('/-/npm/v1/security/advisories/bulk').reply(404) 114 .post('/-/npm/v1/security/audits/quick', body => { 115 const unzipped = JSON.parse(gunzip(Buffer.from(body, 'hex'))) 116 return t.match(unzipped, { 117 name: 'test-dep', 118 version: '1.0.0', 119 requires: { 'test-dep-a': '*' }, 120 dependencies: { 'test-dep-a': { version: '1.0.0' } }, 121 }) 122 }).reply(200, { 123 actions: [], 124 muted: [], 125 advisories: { 126 100: advisory, 127 }, 128 metadata: { 129 vulnerabilities: { info: 0, low: 0, moderate: 0, high: 1, critical: 0 }, 130 dependencies: 1, 131 devDependencies: 0, 132 optionalDependencies: 0, 133 totalDependencies: 1, 134 }, 135 }) 136 await npm.exec('audit', []) 137 t.ok(process.exitCode, 'would have exited uncleanly') 138 t.matchSnapshot(joinedOutput()) 139}) 140 141t.test('json audit', async t => { 142 const { npm, joinedOutput } = await loadMockNpm(t, { 143 prefixDir: tree, 144 config: { 145 json: true, 146 }, 147 }) 148 const registry = new MockRegistry({ 149 tap: t, 150 registry: npm.config.get('registry'), 151 }) 152 153 const manifest = registry.manifest({ 154 name: 'test-dep-a', 155 packuments: [{ version: '1.0.0' }, { version: '1.0.1' }], 156 }) 157 await registry.package({ manifest }) 158 const advisory = registry.advisory({ id: 100 }) 159 const bulkBody = gzip(JSON.stringify({ 'test-dep-a': ['1.0.0'] })) 160 registry.nock.post('/-/npm/v1/security/advisories/bulk', bulkBody) 161 .reply(200, { 162 'test-dep-a': [advisory], 163 }) 164 165 await npm.exec('audit', []) 166 t.ok(process.exitCode, 'would have exited uncleanly') 167 t.matchSnapshot(joinedOutput()) 168}) 169 170t.test('audit fix - bulk endpoint', async t => { 171 const { npm, joinedOutput } = await loadMockNpm(t, { 172 prefixDir: tree, 173 }) 174 const registry = new MockRegistry({ 175 tap: t, 176 registry: npm.config.get('registry'), 177 }) 178 const manifest = registry.manifest({ 179 name: 'test-dep-a', 180 packuments: [{ version: '1.0.0' }, { version: '1.0.1' }], 181 }) 182 await registry.package({ 183 manifest, 184 tarballs: { 185 '1.0.1': path.join(npm.prefix, 'test-dep-a-fixed'), 186 }, 187 }) 188 const advisory = registry.advisory({ id: 100, vulnerable_versions: '1.0.0' }) 189 registry.nock.post('/-/npm/v1/security/advisories/bulk', body => { 190 const unzipped = JSON.parse(gunzip(Buffer.from(body, 'hex'))) 191 return t.same(unzipped, { 'test-dep-a': ['1.0.0'] }) 192 }) 193 .reply(200, { // first audit 194 'test-dep-a': [advisory], 195 }) 196 .post('/-/npm/v1/security/advisories/bulk', body => { 197 const unzipped = JSON.parse(gunzip(Buffer.from(body, 'hex'))) 198 return t.same(unzipped, { 'test-dep-a': ['1.0.1'] }) 199 }) 200 .reply(200, { // after fix 201 'test-dep-a': [], 202 }) 203 await npm.exec('audit', ['fix']) 204 t.matchSnapshot(joinedOutput()) 205 const pkg = fs.readFileSync(path.join(npm.prefix, 'package-lock.json'), 'utf8') 206 t.matchSnapshot(pkg, 'lockfile has test-dep-a@1.0.1') 207 t.ok( 208 fs.existsSync(path.join(npm.prefix, 'node_modules', 'test-dep-a', 'fixed.txt')), 209 'has test-dep-a@1.0.1 on disk' 210 ) 211}) 212 213t.test('completion', async t => { 214 const { audit } = await loadMockNpm(t, { command: 'audit' }) 215 t.test('fix', async t => { 216 await t.resolveMatch( 217 audit.completion({ conf: { argv: { remain: ['npm', 'audit'] } } }), 218 ['fix'], 219 'completes to fix' 220 ) 221 }) 222 223 t.test('subcommand fix', async t => { 224 await t.resolveMatch( 225 audit.completion({ conf: { argv: { remain: ['npm', 'audit', 'fix'] } } }), 226 [], 227 'resolves to ?' 228 ) 229 }) 230 231 t.test('subcommand not recognized', async t => { 232 await t.rejects(audit.completion({ conf: { argv: { remain: ['npm', 'audit', 'repare'] } } }), { 233 message: 'repare not recognized', 234 }) 235 }) 236}) 237 238t.test('audit signatures', async t => { 239 const VALID_REGISTRY_KEYS = { 240 keys: [{ 241 expires: null, 242 keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 243 keytype: 'ecdsa-sha2-nistp256', 244 scheme: 'ecdsa-sha2-nistp256', 245 key: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+' + 246 'IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==', 247 }], 248 } 249 250 const TUF_VALID_REGISTRY_KEYS = { 251 keys: [{ 252 keyId: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 253 keyUsage: 'npm:signatures', 254 publicKey: { 255 rawBytes: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+' + 256 'IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==', 257 keyDetails: 'PKIX_ECDSA_P256_SHA_256', 258 validFor: { 259 start: '1999-01-01T00:00:00.000Z', 260 }, 261 }, 262 }], 263 } 264 265 const TUF_MISMATCHING_REGISTRY_KEYS = { 266 keys: [{ 267 keyId: 'SHA256:2l3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 268 keyUsage: 'npm:signatures', 269 publicKey: { 270 rawBytes: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+' + 271 'IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==', 272 keyDetails: 'PKIX_ECDSA_P256_SHA_256', 273 validFor: { 274 start: '1999-01-01T00:00:00.000Z', 275 }, 276 }, 277 }], 278 } 279 280 const TUF_EXPIRED_REGISTRY_KEYS = { 281 keys: [{ 282 keyId: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 283 keyUsage: 'npm:signatures', 284 publicKey: { 285 rawBytes: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+' + 286 'IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==', 287 keyDetails: 'PKIX_ECDSA_P256_SHA_256', 288 validFor: { 289 start: '1999-01-01T00:00:00.000Z', 290 end: '2021-01-11T15:45:42.144Z', 291 }, 292 }, 293 }], 294 } 295 296 const TUF_VALID_KEYS_TARGET = { 297 name: 'registry.npmjs.org/keys.json', 298 content: JSON.stringify(TUF_VALID_REGISTRY_KEYS), 299 } 300 301 const TUF_MISMATCHING_KEYS_TARGET = { 302 name: 'registry.npmjs.org/keys.json', 303 content: JSON.stringify(TUF_MISMATCHING_REGISTRY_KEYS), 304 } 305 306 const TUF_EXPIRED_KEYS_TARGET = { 307 name: 'registry.npmjs.org/keys.json', 308 content: JSON.stringify(TUF_EXPIRED_REGISTRY_KEYS), 309 } 310 311 const TUF_TARGET_NOT_FOUND = [] 312 313 const installWithValidSigs = { 314 'package.json': JSON.stringify({ 315 name: 'test-dep', 316 version: '1.0.0', 317 dependencies: { 318 'kms-demo': '1.0.0', 319 }, 320 }), 321 node_modules: { 322 'kms-demo': { 323 'package.json': JSON.stringify({ 324 name: 'kms-demo', 325 version: '1.0.0', 326 }), 327 }, 328 }, 329 'package-lock.json': JSON.stringify({ 330 name: 'test-dep', 331 version: '1.0.0', 332 lockfileVersion: 2, 333 requires: true, 334 packages: { 335 '': { 336 name: 'scratch', 337 version: '1.0.0', 338 dependencies: { 339 'kms-demo': '^1.0.0', 340 }, 341 }, 342 'node_modules/kms-demo': { 343 version: '1.0.0', 344 }, 345 }, 346 dependencies: { 347 'kms-demo': { 348 version: '1.0.0', 349 }, 350 }, 351 }), 352 } 353 354 const installWithValidAttestations = { 355 'package.json': JSON.stringify({ 356 name: 'test-dep', 357 version: '1.0.0', 358 dependencies: { 359 sigstore: '1.0.0', 360 }, 361 }), 362 node_modules: { 363 sigstore: { 364 'package.json': JSON.stringify({ 365 name: 'sigstore', 366 version: '1.0.0', 367 }), 368 }, 369 }, 370 'package-lock.json': JSON.stringify({ 371 name: 'test-dep', 372 version: '1.0.0', 373 lockfileVersion: 2, 374 requires: true, 375 packages: { 376 '': { 377 name: 'test-dep', 378 version: '1.0.0', 379 dependencies: { 380 sigstore: '^1.0.0', 381 }, 382 }, 383 'node_modules/sigstore': { 384 version: '1.0.0', 385 }, 386 }, 387 dependencies: { 388 sigstore: { 389 version: '1.0.0', 390 }, 391 }, 392 }), 393 } 394 395 const installWithMultipleValidAttestations = { 396 'package.json': JSON.stringify({ 397 name: 'test-dep', 398 version: '1.0.0', 399 dependencies: { 400 sigstore: '1.0.0', 401 'tuf-js': '1.0.0', 402 }, 403 }), 404 node_modules: { 405 sigstore: { 406 'package.json': JSON.stringify({ 407 name: 'sigstore', 408 version: '1.0.0', 409 }), 410 }, 411 'tuf-js': { 412 'package.json': JSON.stringify({ 413 name: 'tuf-js', 414 version: '1.0.0', 415 }), 416 }, 417 }, 418 'package-lock.json': JSON.stringify({ 419 name: 'test-dep', 420 version: '1.0.0', 421 lockfileVersion: 2, 422 requires: true, 423 packages: { 424 '': { 425 name: 'test-dep', 426 version: '1.0.0', 427 dependencies: { 428 sigstore: '^1.0.0', 429 'tuf-js': '^1.0.0', 430 }, 431 }, 432 'node_modules/sigstore': { 433 version: '1.0.0', 434 }, 435 'node_modules/tuf-js': { 436 version: '1.0.0', 437 }, 438 }, 439 dependencies: { 440 sigstore: { 441 version: '1.0.0', 442 }, 443 'tuf-js': { 444 version: '1.0.0', 445 }, 446 }, 447 }), 448 } 449 450 const installWithAlias = { 451 'package.json': JSON.stringify({ 452 name: 'test-dep', 453 version: '1.0.0', 454 dependencies: { 455 get: 'npm:node-fetch@^1.0.0', 456 }, 457 }), 458 node_modules: { 459 get: { 460 'package.json': JSON.stringify({ 461 name: 'node-fetch', 462 version: '1.7.1', 463 }), 464 }, 465 }, 466 'package-lock.json': JSON.stringify({ 467 name: 'test-dep', 468 version: '1.0.0', 469 lockfileVersion: 2, 470 requires: true, 471 packages: { 472 '': { 473 name: 'test-dep', 474 version: '1.0.0', 475 dependencies: { 476 get: 'npm:node-fetch@^1.0.0', 477 }, 478 }, 479 'node_modules/demo': { 480 name: 'node-fetch', 481 version: '1.7.1', 482 }, 483 }, 484 dependencies: { 485 get: { 486 version: 'npm:node-fetch@1.7.1', 487 }, 488 }, 489 }), 490 } 491 492 const noInstall = { 493 'package.json': JSON.stringify({ 494 name: 'test-dep', 495 version: '1.0.0', 496 dependencies: { 497 'kms-demo': '1.0.0', 498 }, 499 }), 500 'package-lock.json': JSON.stringify({ 501 name: 'test-dep', 502 version: '1.0.0', 503 lockfileVersion: 2, 504 requires: true, 505 packages: { 506 '': { 507 name: 'scratch', 508 version: '1.0.0', 509 dependencies: { 510 'kms-demo': '^1.0.0', 511 }, 512 }, 513 'node_modules/kms-demo': { 514 version: '1.0.0', 515 }, 516 }, 517 dependencies: { 518 'kms-demo': { 519 version: '1.0.0', 520 }, 521 }, 522 }), 523 } 524 525 const workspaceInstall = { 526 'package.json': JSON.stringify({ 527 name: 'workspaces-project', 528 version: '1.0.0', 529 workspaces: ['packages/*'], 530 dependencies: { 531 'kms-demo': '^1.0.0', 532 }, 533 }), 534 node_modules: { 535 a: t.fixture('symlink', '../packages/a'), 536 b: t.fixture('symlink', '../packages/b'), 537 c: t.fixture('symlink', '../packages/c'), 538 'kms-demo': { 539 'package.json': JSON.stringify({ 540 name: 'kms-demo', 541 version: '1.0.0', 542 }), 543 }, 544 async: { 545 'package.json': JSON.stringify({ 546 name: 'async', 547 version: '2.5.0', 548 }), 549 }, 550 'light-cycle': { 551 'package.json': JSON.stringify({ 552 name: 'light-cycle', 553 version: '1.4.2', 554 }), 555 }, 556 }, 557 packages: { 558 a: { 559 'package.json': JSON.stringify({ 560 name: 'a', 561 version: '1.0.0', 562 dependencies: { 563 b: '^1.0.0', 564 async: '^2.0.0', 565 }, 566 }), 567 }, 568 b: { 569 'package.json': JSON.stringify({ 570 name: 'b', 571 version: '1.0.0', 572 dependencies: { 573 'light-cycle': '^1.0.0', 574 }, 575 }), 576 }, 577 c: { 578 'package.json': JSON.stringify({ 579 name: 'c', 580 version: '1.0.0', 581 }), 582 }, 583 }, 584 } 585 586 const installWithMultipleDeps = { 587 'package.json': JSON.stringify({ 588 name: 'test-dep', 589 version: '1.0.0', 590 dependencies: { 591 'kms-demo': '^1.0.0', 592 }, 593 devDependencies: { 594 async: '~1.1.0', 595 }, 596 }), 597 node_modules: { 598 'kms-demo': { 599 'package.json': JSON.stringify({ 600 name: 'kms-demo', 601 version: '1.0.0', 602 }), 603 }, 604 async: { 605 'package.json': JSON.stringify({ 606 name: 'async', 607 version: '1.1.1', 608 dependencies: { 609 'kms-demo': '^1.0.0', 610 }, 611 }), 612 }, 613 }, 614 'package-lock.json': JSON.stringify({ 615 name: 'test-dep', 616 version: '1.0.0', 617 lockfileVersion: 2, 618 requires: true, 619 packages: { 620 '': { 621 name: 'scratch', 622 version: '1.0.0', 623 dependencies: { 624 'kms-demo': '^1.0.0', 625 }, 626 devDependencies: { 627 async: '~1.0.0', 628 }, 629 }, 630 'node_modules/kms-demo': { 631 version: '1.0.0', 632 }, 633 'node_modules/async': { 634 version: '1.1.1', 635 }, 636 }, 637 dependencies: { 638 'kms-demo': { 639 version: '1.0.0', 640 }, 641 async: { 642 version: '1.1.1', 643 dependencies: { 644 'kms-demo': '^1.0.0', 645 }, 646 }, 647 }, 648 }), 649 } 650 651 const installWithPeerDeps = { 652 'package.json': JSON.stringify({ 653 name: 'test-dep', 654 version: '1.0.0', 655 peerDependencies: { 656 'kms-demo': '^1.0.0', 657 }, 658 }), 659 node_modules: { 660 'kms-demo': { 661 'package.json': JSON.stringify({ 662 name: 'kms-demo', 663 version: '1.0.0', 664 }), 665 }, 666 }, 667 'package-lock.json': JSON.stringify({ 668 name: 'test-dep', 669 version: '1.0.0', 670 lockfileVersion: 2, 671 requires: true, 672 packages: { 673 '': { 674 name: 'scratch', 675 version: '1.0.0', 676 peerDependencies: { 677 'kms-demo': '^1.0.0', 678 }, 679 }, 680 'node_modules/kms-demo': { 681 version: '1.0.0', 682 }, 683 }, 684 dependencies: { 685 'kms-demo': { 686 version: '1.0.0', 687 }, 688 }, 689 }), 690 } 691 692 const installWithOptionalDeps = { 693 'package.json': JSON.stringify({ 694 name: 'test-dep', 695 version: '1.0.0', 696 dependencies: { 697 'kms-demo': '^1.0.0', 698 }, 699 optionalDependencies: { 700 lorem: '^1.0.0', 701 }, 702 }, null, 2), 703 node_modules: { 704 'kms-demo': { 705 'package.json': JSON.stringify({ 706 name: 'kms-demo', 707 version: '1.0.0', 708 }), 709 }, 710 }, 711 'package-lock.json': JSON.stringify({ 712 name: 'test-dep', 713 version: '1.0.0', 714 lockfileVersion: 2, 715 requires: true, 716 packages: { 717 '': { 718 name: 'scratch', 719 version: '1.0.0', 720 dependencies: { 721 'kms-demo': '^1.0.0', 722 }, 723 optionalDependencies: { 724 lorem: '^1.0.0', 725 }, 726 }, 727 'node_modules/kms-demo': { 728 version: '1.0.0', 729 }, 730 }, 731 dependencies: { 732 'kms-demo': { 733 version: '1.0.0', 734 }, 735 }, 736 }), 737 } 738 739 const installWithMultipleRegistries = { 740 'package.json': JSON.stringify({ 741 name: 'test-dep', 742 version: '1.0.0', 743 dependencies: { 744 '@npmcli/arborist': '^1.0.0', 745 'kms-demo': '^1.0.0', 746 }, 747 }), 748 node_modules: { 749 '@npmcli/arborist': { 750 'package.json': JSON.stringify({ 751 name: '@npmcli/arborist', 752 version: '1.0.14', 753 }), 754 }, 755 'kms-demo': { 756 'package.json': JSON.stringify({ 757 name: 'kms-demo', 758 version: '1.0.0', 759 }), 760 }, 761 }, 762 'package-lock.json': JSON.stringify({ 763 name: 'test-dep', 764 version: '1.0.0', 765 lockfileVersion: 2, 766 requires: true, 767 packages: { 768 '': { 769 name: 'test-dep', 770 version: '1.0.0', 771 dependencies: { 772 '@npmcli/arborist': '^1.0.0', 773 'kms-demo': '^1.0.0', 774 }, 775 }, 776 'node_modules/@npmcli/arborist': { 777 version: '1.0.14', 778 }, 779 'node_modules/kms-demo': { 780 version: '1.0.0', 781 }, 782 }, 783 dependencies: { 784 '@npmcli/arborist': { 785 version: '1.0.14', 786 }, 787 'kms-demo': { 788 version: '1.0.0', 789 }, 790 }, 791 }), 792 } 793 794 const installWithThirdPartyRegistry = { 795 'package.json': JSON.stringify({ 796 name: 'test-dep', 797 version: '1.0.0', 798 dependencies: { 799 '@npmcli/arborist': '^1.0.0', 800 }, 801 }), 802 node_modules: { 803 '@npmcli/arborist': { 804 'package.json': JSON.stringify({ 805 name: '@npmcli/arborist', 806 version: '1.0.14', 807 }), 808 }, 809 }, 810 'package-lock.json': JSON.stringify({ 811 name: 'test-dep', 812 version: '1.0.0', 813 lockfileVersion: 2, 814 requires: true, 815 packages: { 816 '': { 817 name: 'test-dep', 818 version: '1.0.0', 819 dependencies: { 820 '@npmcli/arborist': '^1.0.0', 821 }, 822 }, 823 'node_modules/@npmcli/arborist': { 824 version: '1.0.14', 825 }, 826 }, 827 dependencies: { 828 '@npmcli/arborist': { 829 version: '1.0.14', 830 }, 831 }, 832 }), 833 } 834 835 async function manifestWithValidSigs ({ registry }) { 836 const manifest = registry.manifest({ 837 name: 'kms-demo', 838 packuments: [{ 839 version: '1.0.0', 840 dist: { 841 tarball: 'https://registry.npmjs.org/kms-demo/-/kms-demo-1.0.0.tgz', 842 integrity: 'sha512-QqZ7VJ/8xPkS9s2IWB7Shj3qTJdcRyeXKbPQnsZjsPEwvutGv0EGeVchPca' + 843 'uoiDFJlGbZMFq5GDCurAGNSghJQ==', 844 signatures: [ 845 { 846 keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 847 sig: 'MEUCIDrLNspFeU5NZ6d55ycVBZIMXnPJi/XnI1Y2dlJvK8P1AiEAnXjn1IOMUd+U7YfPH' + 848 '+FNjwfLq+jCwfH8uaxocq+mpPk=', 849 }, 850 ], 851 }, 852 }], 853 }) 854 await registry.package({ manifest }) 855 } 856 857 async function manifestWithValidAttestations ({ registry }) { 858 const manifest = registry.manifest({ 859 name: 'sigstore', 860 packuments: [{ 861 version: '1.0.0', 862 dist: { 863 // eslint-disable-next-line max-len 864 integrity: 'sha512-e+qfbn/zf1+rCza/BhIA//Awmf0v1pa5HQS8Xk8iXrn9bgytytVLqYD0P7NSqZ6IELTgq+tcDvLPkQjNHyWLNg==', 865 tarball: 'https://registry.npmjs.org/sigstore/-/sigstore-1.0.0.tgz', 866 // eslint-disable-next-line max-len 867 attestations: { url: 'https://registry.npmjs.org/-/npm/v1/attestations/sigstore@1.0.0', provenance: { predicateType: 'https://slsa.dev/provenance/v0.2' } }, 868 // eslint-disable-next-line max-len 869 signatures: [{ keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', sig: 'MEQCIBlpcHT68iWOpx8pJr3WUzD1EqQ7tb0CmY36ebbceR6IAiAVGRaxrFoyh0/5B7H1o4VFhfsHw9F8G+AxOZQq87q+lg==' }], 870 }, 871 }], 872 }) 873 await registry.package({ manifest }) 874 } 875 876 async function manifestWithMultipleValidAttestations ({ registry }) { 877 const manifest = registry.manifest({ 878 name: 'tuf-js', 879 packuments: [{ 880 version: '1.0.0', 881 dist: { 882 // eslint-disable-next-line max-len 883 integrity: 'sha512-1dxsQwESDzACJjTdYHQ4wJ1f/of7jALWKfJEHSBWUQB/5UTJUx9SW6GHXp4mZ1KvdBRJCpGjssoPFGi4hvw8/A==', 884 tarball: 'https://registry.npmjs.org/tuf-js/-/tuf-js-1.0.0.tgz', 885 // eslint-disable-next-line max-len 886 attestations: { url: 'https://registry.npmjs.org/-/npm/v1/attestations/tuf-js@1.0.0', provenance: { predicateType: 'https://slsa.dev/provenance/v0.2' } }, 887 // eslint-disable-next-line max-len 888 signatures: [{ keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', sig: 'MEYCIQDgGQeY2QLkLuoO9YxOqFZ+a6zYuaZpXhc77kUfdCUXDQIhAJp/vV+9Xg1bfM5YlTvKIH9agUEOu5T76+tQaHY2vZyO' }], 889 }, 890 }], 891 }) 892 await registry.package({ manifest }) 893 } 894 895 async function manifestWithInvalidSigs ({ registry, name = 'kms-demo', version = '1.0.0' }) { 896 const manifest = registry.manifest({ 897 name, 898 packuments: [{ 899 version, 900 dist: { 901 tarball: `https://registry.npmjs.org/${name}/-/${name}-${version}.tgz`, 902 integrity: 'sha512-QqZ7VJ/8xPkS9s2IWB7Shj3qTJdcRyeXKbPQnsZjsPEwvutGv0EGeVchPca' + 903 'uoiDFJlGbZMFq5GDCurAGNSghJQ==', 904 signatures: [ 905 { 906 keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 907 sig: 'bogus', 908 }, 909 ], 910 }, 911 }], 912 }) 913 await registry.package({ manifest }) 914 } 915 916 async function manifestWithoutSigs ({ registry, name = 'kms-demo', version = '1.0.0' }) { 917 const manifest = registry.manifest({ 918 name, 919 packuments: [{ 920 version, 921 }], 922 }) 923 await registry.package({ manifest }) 924 } 925 926 function mockTUF ({ target, npm }) { 927 const opts = { 928 baseURL: 'https://tuf-repo-cdn.sigstore.dev', 929 metadataPathPrefix: '', 930 cachePath: path.join(npm.cache, '_tuf'), 931 } 932 return tufmock(target, opts) 933 } 934 935 t.test('with valid signatures', async t => { 936 const { npm, joinedOutput } = await loadMockNpm(t, { 937 prefixDir: installWithValidSigs, 938 }) 939 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 940 await manifestWithValidSigs({ registry }) 941 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 942 943 await npm.exec('audit', ['signatures']) 944 945 t.notOk(process.exitCode, 'should exit successfully') 946 t.match(joinedOutput(), /audited 1 package/) 947 t.matchSnapshot(joinedOutput()) 948 }) 949 950 t.test('with valid signatures using alias', async t => { 951 const { npm, joinedOutput } = await loadMockNpm(t, { 952 prefixDir: installWithAlias, 953 }) 954 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 955 const manifest = registry.manifest({ 956 name: 'node-fetch', 957 packuments: [{ 958 version: '1.7.1', 959 dist: { 960 tarball: 'https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.1.tgz', 961 integrity: 'sha512-j8XsFGCLw79vWXkZtMSmmLaOk9z5SQ9bV/tkbZVCqvgwzrjAGq6' + 962 '6igobLofHtF63NvMTp2WjytpsNTGKa+XRIQ==', 963 signatures: [ 964 { 965 keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 966 sig: 'MEYCIQDEn2XrrMXlRm+wh2tOIUyb0Km3ZujfT+6Mf61OXGK9zQIhANnPauUwx3' + 967 'N9RcQYQakDpOmLvYzNkySh7fmzmvyhk21j', 968 }, 969 ], 970 }, 971 }], 972 }) 973 await registry.package({ manifest }) 974 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 975 976 await npm.exec('audit', ['signatures']) 977 978 t.notOk(process.exitCode, 'should exit successfully') 979 t.match(joinedOutput(), /audited 1 package/) 980 t.matchSnapshot(joinedOutput()) 981 }) 982 983 t.test('with key fallback to legacy API', async t => { 984 const { npm, joinedOutput } = await loadMockNpm(t, { 985 prefixDir: installWithValidSigs, 986 }) 987 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 988 await manifestWithValidSigs({ registry }) 989 mockTUF({ npm, target: TUF_TARGET_NOT_FOUND }) 990 registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) 991 992 await npm.exec('audit', ['signatures']) 993 994 t.notOk(process.exitCode, 'should exit successfully') 995 t.match(joinedOutput(), /audited 1 package/) 996 t.matchSnapshot(joinedOutput()) 997 }) 998 999 t.test('with multiple valid signatures and one invalid', async t => { 1000 const { npm, joinedOutput } = await loadMockNpm(t, { 1001 prefixDir: { 1002 'package.json': JSON.stringify({ 1003 name: 'test-dep', 1004 version: '1.0.0', 1005 dependencies: { 1006 'kms-demo': '^1.0.0', 1007 'node-fetch': '^1.6.0', 1008 }, 1009 devDependencies: { 1010 async: '~2.1.0', 1011 }, 1012 }), 1013 node_modules: { 1014 'kms-demo': { 1015 'package.json': JSON.stringify({ 1016 name: 'kms-demo', 1017 version: '1.0.0', 1018 }), 1019 }, 1020 async: { 1021 'package.json': JSON.stringify({ 1022 name: 'async', 1023 version: '2.5.0', 1024 }), 1025 }, 1026 'node-fetch': { 1027 'package.json': JSON.stringify({ 1028 name: 'node-fetch', 1029 version: '1.6.0', 1030 }), 1031 }, 1032 }, 1033 'package-lock.json': JSON.stringify({ 1034 name: 'test-dep', 1035 version: '1.0.0', 1036 lockfileVersion: 2, 1037 requires: true, 1038 packages: { 1039 '': { 1040 name: 'test-dep', 1041 version: '1.0.0', 1042 dependencies: { 1043 'kms-demo': '^1.0.0', 1044 'node-fetch': '^1.6.0', 1045 }, 1046 devDependencies: { 1047 async: '~2.1.0', 1048 }, 1049 }, 1050 'node_modules/kms-demo': { 1051 version: '1.0.0', 1052 }, 1053 'node_modules/async': { 1054 version: '2.5.0', 1055 }, 1056 'node_modules/node-fetch': { 1057 version: '1.6.0', 1058 }, 1059 }, 1060 dependencies: { 1061 'kms-demo': { 1062 version: '1.0.0', 1063 }, 1064 'node-fetch': { 1065 version: '1.6.0', 1066 }, 1067 async: { 1068 version: '2.5.0', 1069 }, 1070 }, 1071 }), 1072 }, 1073 }) 1074 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1075 await manifestWithValidSigs({ registry }) 1076 const asyncManifest = registry.manifest({ 1077 name: 'async', 1078 packuments: [{ 1079 version: '2.5.0', 1080 dist: { 1081 tarball: 'https://registry.npmjs.org/async/-/async-2.5.0.tgz', 1082 integrity: 'sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFT' 1083 + 'KE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==', 1084 signatures: [ 1085 { 1086 keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 1087 sig: 'MEUCIQCM8cX2U3IVZKKhzQx1w5AlNSDUI+fVf4857K1qT0NTNgIgdT4qwEl' + 1088 '/kg2vU1uIWUI0bGikRvVHCHlRs1rgjPMpRFA=', 1089 }, 1090 ], 1091 }, 1092 }], 1093 }) 1094 await registry.package({ manifest: asyncManifest }) 1095 await manifestWithInvalidSigs({ registry, name: 'node-fetch', version: '1.6.0' }) 1096 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1097 1098 await npm.exec('audit', ['signatures']) 1099 1100 t.equal(process.exitCode, 1, 'should exit with error') 1101 t.match(joinedOutput(), /audited 3 packages/) 1102 t.match(joinedOutput(), /2 packages have verified registry signatures/) 1103 t.match(joinedOutput(), /1 package has an invalid registry signature/) 1104 t.matchSnapshot(joinedOutput()) 1105 }) 1106 1107 t.test('with bundled and peer deps and no signatures', async t => { 1108 const { npm, joinedOutput } = await loadMockNpm(t, { 1109 prefixDir: installWithPeerDeps, 1110 }) 1111 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1112 await manifestWithValidSigs({ registry }) 1113 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1114 1115 await npm.exec('audit', ['signatures']) 1116 1117 t.notOk(process.exitCode, 'should exit successfully') 1118 t.match(joinedOutput(), /audited 1 package/) 1119 t.matchSnapshot(joinedOutput()) 1120 }) 1121 1122 t.test('with invalid signatures', async t => { 1123 const { npm, joinedOutput } = await loadMockNpm(t, { 1124 prefixDir: installWithValidSigs, 1125 }) 1126 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1127 await manifestWithInvalidSigs({ registry }) 1128 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1129 1130 await npm.exec('audit', ['signatures']) 1131 1132 t.equal(process.exitCode, 1, 'should exit with error') 1133 t.match(joinedOutput(), /invalid registry signature/) 1134 t.match(joinedOutput(), /kms-demo@1.0.0/) 1135 t.matchSnapshot(joinedOutput()) 1136 }) 1137 1138 t.test('with valid and missing signatures', async t => { 1139 const { npm, joinedOutput } = await loadMockNpm(t, { 1140 prefixDir: installWithMultipleDeps, 1141 }) 1142 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1143 await manifestWithValidSigs({ registry }) 1144 await manifestWithoutSigs({ registry, name: 'async', version: '1.1.1' }) 1145 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1146 1147 await npm.exec('audit', ['signatures']) 1148 1149 t.equal(process.exitCode, 1, 'should exit with error') 1150 t.match(joinedOutput(), /audited 2 packages/) 1151 t.match(joinedOutput(), /verified registry signature/) 1152 t.match(joinedOutput(), /missing registry signature/) 1153 t.matchSnapshot(joinedOutput()) 1154 }) 1155 1156 t.test('with both invalid and missing signatures', async t => { 1157 const { npm, joinedOutput } = await loadMockNpm(t, { 1158 prefixDir: installWithMultipleDeps, 1159 }) 1160 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1161 await manifestWithInvalidSigs({ registry }) 1162 await manifestWithoutSigs({ registry, name: 'async', version: '1.1.1' }) 1163 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1164 1165 await npm.exec('audit', ['signatures']) 1166 1167 t.equal(process.exitCode, 1, 'should exit with error') 1168 t.match(joinedOutput(), /audited 2 packages/) 1169 t.match(joinedOutput(), /invalid/) 1170 t.match(joinedOutput(), /missing/) 1171 t.matchSnapshot(joinedOutput()) 1172 }) 1173 1174 t.test('with multiple invalid signatures', async t => { 1175 const { npm, joinedOutput } = await loadMockNpm(t, { 1176 prefixDir: installWithMultipleDeps, 1177 }) 1178 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1179 await manifestWithInvalidSigs({ registry, name: 'kms-demo', version: '1.0.0' }) 1180 await manifestWithInvalidSigs({ registry, name: 'async', version: '1.1.1' }) 1181 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1182 1183 await npm.exec('audit', ['signatures']) 1184 1185 t.equal(process.exitCode, 1, 'should exit with error') 1186 t.matchSnapshot(joinedOutput()) 1187 }) 1188 1189 t.test('with multiple missing signatures', async t => { 1190 const { npm, joinedOutput } = await loadMockNpm(t, { 1191 prefixDir: installWithMultipleDeps, 1192 }) 1193 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1194 await manifestWithoutSigs({ registry, name: 'kms-demo', version: '1.0.0' }) 1195 await manifestWithoutSigs({ registry, name: 'async', version: '1.1.1' }) 1196 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1197 1198 await npm.exec('audit', ['signatures']) 1199 1200 t.equal(process.exitCode, 1, 'should exit with error') 1201 t.matchSnapshot(joinedOutput()) 1202 }) 1203 1204 t.test('with signatures but no public keys', async t => { 1205 const { npm } = await loadMockNpm(t, { 1206 prefixDir: installWithValidSigs, 1207 }) 1208 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1209 await manifestWithValidSigs({ registry }) 1210 mockTUF({ npm, target: TUF_TARGET_NOT_FOUND }) 1211 registry.nock.get('/-/npm/v1/keys').reply(404) 1212 1213 await t.rejects( 1214 npm.exec('audit', ['signatures']), 1215 /no corresponding public key can be found/, 1216 'should throw with error' 1217 ) 1218 }) 1219 1220 t.test('with signatures but the public keys are expired', async t => { 1221 const { npm } = await loadMockNpm(t, { 1222 prefixDir: installWithValidSigs, 1223 }) 1224 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1225 await manifestWithValidSigs({ registry }) 1226 mockTUF({ npm, target: TUF_EXPIRED_KEYS_TARGET }) 1227 1228 await t.rejects( 1229 npm.exec('audit', ['signatures']), 1230 /the corresponding public key has expired/, 1231 'should throw with error' 1232 ) 1233 }) 1234 1235 t.test('with signatures but the public keyid does not match', async t => { 1236 const { npm } = await loadMockNpm(t, { 1237 prefixDir: installWithValidSigs, 1238 }) 1239 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1240 await manifestWithValidSigs({ registry }) 1241 mockTUF({ npm, target: TUF_MISMATCHING_KEYS_TARGET }) 1242 1243 await t.rejects( 1244 npm.exec('audit', ['signatures']), 1245 /no corresponding public key can be found/, 1246 'should throw with error' 1247 ) 1248 }) 1249 1250 t.test('with keys but missing signature', async t => { 1251 const { npm, joinedOutput } = await loadMockNpm(t, { 1252 prefixDir: installWithValidSigs, 1253 }) 1254 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1255 await manifestWithoutSigs({ registry }) 1256 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1257 1258 await npm.exec('audit', ['signatures']) 1259 1260 t.equal(process.exitCode, 1, 'should exit with error') 1261 t.match( 1262 joinedOutput(), 1263 /registry is providing signing keys/ 1264 ) 1265 t.matchSnapshot(joinedOutput()) 1266 }) 1267 1268 t.test('output details about missing signatures', async t => { 1269 const { npm, joinedOutput } = await loadMockNpm(t, { 1270 prefixDir: installWithValidSigs, 1271 }) 1272 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1273 await manifestWithoutSigs({ registry }) 1274 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1275 1276 await npm.exec('audit', ['signatures']) 1277 1278 t.equal(process.exitCode, 1, 'should exit with error') 1279 t.match( 1280 joinedOutput(), 1281 /kms-demo/ 1282 ) 1283 t.matchSnapshot(joinedOutput()) 1284 }) 1285 1286 t.test('json output with valid signatures', async t => { 1287 const { npm, joinedOutput } = await loadMockNpm(t, { 1288 prefixDir: installWithValidSigs, 1289 config: { 1290 json: true, 1291 }, 1292 }) 1293 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1294 await manifestWithValidSigs({ registry }) 1295 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1296 1297 await npm.exec('audit', ['signatures']) 1298 1299 t.notOk(process.exitCode, 'should exit successfully') 1300 t.match(joinedOutput(), JSON.stringify({ invalid: [], missing: [] }, null, 2)) 1301 t.matchSnapshot(joinedOutput()) 1302 }) 1303 1304 t.test('json output with invalid signatures', async t => { 1305 const { npm, joinedOutput } = await loadMockNpm(t, { 1306 prefixDir: installWithValidSigs, 1307 config: { 1308 json: true, 1309 }, 1310 }) 1311 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1312 await manifestWithInvalidSigs({ registry }) 1313 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1314 1315 await npm.exec('audit', ['signatures']) 1316 1317 t.equal(process.exitCode, 1, 'should exit with error') 1318 t.matchSnapshot(joinedOutput()) 1319 }) 1320 1321 t.test('json output with invalid and missing signatures', async t => { 1322 const { npm, joinedOutput } = await loadMockNpm(t, { 1323 prefixDir: installWithMultipleDeps, 1324 config: { 1325 json: true, 1326 }, 1327 }) 1328 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1329 await manifestWithInvalidSigs({ registry }) 1330 await manifestWithoutSigs({ registry, name: 'async', version: '1.1.1' }) 1331 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1332 1333 await npm.exec('audit', ['signatures']) 1334 1335 t.equal(process.exitCode, 1, 'should exit with error') 1336 t.matchSnapshot(joinedOutput()) 1337 }) 1338 1339 t.test('omit dev dependencies with missing signature', async t => { 1340 const { npm, joinedOutput } = await loadMockNpm(t, { 1341 prefixDir: installWithMultipleDeps, 1342 config: { 1343 omit: ['dev'], 1344 }, 1345 }) 1346 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1347 await manifestWithValidSigs({ registry }) 1348 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1349 1350 await npm.exec('audit', ['signatures']) 1351 1352 t.notOk(process.exitCode, 'should exit successfully') 1353 t.match(joinedOutput(), /audited 1 package/) 1354 t.matchSnapshot(joinedOutput()) 1355 }) 1356 1357 t.test('third-party registry without keys (E404) does not verify', async t => { 1358 const registryUrl = 'https://verdaccio-clone2.org' 1359 const { npm } = await loadMockNpm(t, { 1360 prefixDir: installWithThirdPartyRegistry, 1361 config: { 1362 scope: '@npmcli', 1363 registry: registryUrl, 1364 }, 1365 }) 1366 const registry = new MockRegistry({ tap: t, registry: registryUrl }) 1367 const manifest = registry.manifest({ 1368 name: '@npmcli/arborist', 1369 packuments: [{ 1370 version: '1.0.14', 1371 dist: { 1372 tarball: 'https://registry.npmjs.org/@npmcli/arborist/-/@npmcli/arborist-1.0.14.tgz', 1373 integrity: 'sha512-caa8hv5rW9VpQKk6tyNRvSaVDySVjo9GkI7Wj/wcsFyxPm3tYrE' + 1374 'sFyTjSnJH8HCIfEGVQNjqqKXaXLFVp7UBag==', 1375 }, 1376 }], 1377 }) 1378 await registry.package({ manifest }) 1379 mockTUF({ npm, target: TUF_TARGET_NOT_FOUND }) 1380 registry.nock.get('/-/npm/v1/keys').reply(404) 1381 1382 await t.rejects( 1383 npm.exec('audit', ['signatures']), 1384 /found no dependencies to audit that where installed from a supported registry/ 1385 ) 1386 }) 1387 1388 t.test('third-party registry without keys (E400) does not verify', async t => { 1389 const registryUrl = 'https://verdaccio-clone2.org' 1390 const { npm } = await loadMockNpm(t, { 1391 prefixDir: installWithThirdPartyRegistry, 1392 config: { 1393 scope: '@npmcli', 1394 registry: registryUrl, 1395 }, 1396 }) 1397 const registry = new MockRegistry({ tap: t, registry: registryUrl }) 1398 const manifest = registry.manifest({ 1399 name: '@npmcli/arborist', 1400 packuments: [{ 1401 version: '1.0.14', 1402 dist: { 1403 tarball: 'https://registry.npmjs.org/@npmcli/arborist/-/@npmcli/arborist-1.0.14.tgz', 1404 integrity: 'sha512-caa8hv5rW9VpQKk6tyNRvSaVDySVjo9GkI7Wj/wcsFyxPm3tYrE' + 1405 'sFyTjSnJH8HCIfEGVQNjqqKXaXLFVp7UBag==', 1406 }, 1407 }], 1408 }) 1409 await registry.package({ manifest }) 1410 mockTUF({ npm, target: TUF_TARGET_NOT_FOUND }) 1411 registry.nock.get('/-/npm/v1/keys').reply(400) 1412 1413 await t.rejects( 1414 npm.exec('audit', ['signatures']), 1415 /found no dependencies to audit that where installed from a supported registry/ 1416 ) 1417 }) 1418 1419 t.test('third-party registry with keys and signatures', async t => { 1420 const registryUrl = 'https://verdaccio-clone.org' 1421 const { npm, joinedOutput } = await loadMockNpm(t, { 1422 prefixDir: installWithThirdPartyRegistry, 1423 config: { 1424 scope: '@npmcli', 1425 registry: registryUrl, 1426 }, 1427 }) 1428 const registry = new MockRegistry({ tap: t, registry: registryUrl }) 1429 1430 const manifest = registry.manifest({ 1431 name: '@npmcli/arborist', 1432 packuments: [{ 1433 version: '1.0.14', 1434 dist: { 1435 tarball: 'https://registry.npmjs.org/@npmcli/arborist/-/@npmcli/arborist-1.0.14.tgz', 1436 integrity: 'sha512-caa8hv5rW9VpQKk6tyNRvSaVDySVjo9GkI7Wj/wcsFyxPm3tYrE' + 1437 'sFyTjSnJH8HCIfEGVQNjqqKXaXLFVp7UBag==', 1438 signatures: [ 1439 { 1440 keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 1441 sig: 'MEUCIAvNpR3G0j7WOPUuVMhE0ZdM8PnDNcsoeFD8Iwz9YWIMAiEAn8cicDC2' + 1442 'Sf9MFQydqTv6S5XYsAh9Af1sig1nApNI11M=', 1443 }, 1444 ], 1445 }, 1446 }], 1447 }) 1448 await registry.package({ manifest }) 1449 mockTUF({ npm, 1450 target: { 1451 name: 'verdaccio-clone.org/keys.json', 1452 content: JSON.stringify(TUF_VALID_REGISTRY_KEYS), 1453 } }) 1454 1455 await npm.exec('audit', ['signatures']) 1456 1457 t.notOk(process.exitCode, 'should exit successfully') 1458 t.match(joinedOutput(), /audited 1 package/) 1459 t.matchSnapshot(joinedOutput()) 1460 }) 1461 1462 t.test('third-party registry with invalid signatures errors', async t => { 1463 const registryUrl = 'https://verdaccio-clone.org' 1464 const { npm, joinedOutput } = await loadMockNpm(t, { 1465 prefixDir: installWithThirdPartyRegistry, 1466 config: { 1467 scope: '@npmcli', 1468 registry: registryUrl, 1469 }, 1470 }) 1471 const registry = new MockRegistry({ tap: t, registry: registryUrl }) 1472 1473 const manifest = registry.manifest({ 1474 name: '@npmcli/arborist', 1475 packuments: [{ 1476 version: '1.0.14', 1477 dist: { 1478 tarball: 'https://registry.npmjs.org/@npmcli/arborist/-/@npmcli/arborist-1.0.14.tgz', 1479 integrity: 'sha512-caa8hv5rW9VpQKk6tyNRvSaVDySVjo9GkI7Wj/wcsFyxPm3tYrE' + 1480 'sFyTjSnJH8HCIfEGVQNjqqKXaXLFVp7UBag==', 1481 signatures: [ 1482 { 1483 keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 1484 sig: 'bogus', 1485 }, 1486 ], 1487 }, 1488 }], 1489 }) 1490 await registry.package({ manifest }) 1491 mockTUF({ npm, 1492 target: { 1493 name: 'verdaccio-clone.org/keys.json', 1494 content: JSON.stringify(TUF_VALID_REGISTRY_KEYS), 1495 } }) 1496 1497 await npm.exec('audit', ['signatures']) 1498 1499 t.equal(process.exitCode, 1, 'should exit with error') 1500 t.match(joinedOutput(), /https:\/\/verdaccio-clone.org/) 1501 t.matchSnapshot(joinedOutput()) 1502 }) 1503 1504 t.test('third-party registry with keys and missing signatures errors', async t => { 1505 const registryUrl = 'https://verdaccio-clone.org' 1506 const { npm, joinedOutput } = await loadMockNpm(t, { 1507 prefixDir: installWithThirdPartyRegistry, 1508 config: { 1509 scope: '@npmcli', 1510 registry: registryUrl, 1511 }, 1512 }) 1513 const registry = new MockRegistry({ tap: t, registry: registryUrl }) 1514 1515 const manifest = registry.manifest({ 1516 name: '@npmcli/arborist', 1517 packuments: [{ 1518 version: '1.0.14', 1519 dist: { 1520 tarball: 'https://registry.npmjs.org/@npmcli/arborist/-/@npmcli/arborist-1.0.14.tgz', 1521 integrity: 'sha512-caa8hv5rW9VpQKk6tyNRvSaVDySVjo9GkI7Wj/wcsFyxPm3tYrE' + 1522 'sFyTjSnJH8HCIfEGVQNjqqKXaXLFVp7UBag==', 1523 }, 1524 }], 1525 }) 1526 await registry.package({ manifest }) 1527 mockTUF({ npm, 1528 target: { 1529 name: 'verdaccio-clone.org/keys.json', 1530 content: JSON.stringify(TUF_VALID_REGISTRY_KEYS), 1531 } }) 1532 1533 await npm.exec('audit', ['signatures']) 1534 1535 t.equal(process.exitCode, 1, 'should exit with error') 1536 t.match(joinedOutput(), /1 package has a missing registry signature/) 1537 t.matchSnapshot(joinedOutput()) 1538 }) 1539 1540 t.test('third-party registry with sub-path', async t => { 1541 const registryUrl = 'https://verdaccio-clone.org/npm' 1542 const { npm, joinedOutput } = await loadMockNpm(t, { 1543 prefixDir: installWithThirdPartyRegistry, 1544 config: { 1545 scope: '@npmcli', 1546 registry: registryUrl, 1547 }, 1548 }) 1549 const registry = new MockRegistry({ tap: t, registry: registryUrl }) 1550 1551 const manifest = registry.manifest({ 1552 name: '@npmcli/arborist', 1553 packuments: [{ 1554 version: '1.0.14', 1555 dist: { 1556 tarball: 'https://registry.npmjs.org/@npmcli/arborist/-/@npmcli/arborist-1.0.14.tgz', 1557 integrity: 'sha512-caa8hv5rW9VpQKk6tyNRvSaVDySVjo9GkI7Wj/wcsFyxPm3tYrE' + 1558 'sFyTjSnJH8HCIfEGVQNjqqKXaXLFVp7UBag==', 1559 signatures: [ 1560 { 1561 keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 1562 sig: 'MEUCIAvNpR3G0j7WOPUuVMhE0ZdM8PnDNcsoeFD8Iwz9YWIMAiEAn8cicDC2' + 1563 'Sf9MFQydqTv6S5XYsAh9Af1sig1nApNI11M=', 1564 }, 1565 ], 1566 }, 1567 }], 1568 }) 1569 await registry.package({ manifest }) 1570 1571 mockTUF({ npm, 1572 target: { 1573 name: 'verdaccio-clone.org/npm/keys.json', 1574 content: JSON.stringify(TUF_VALID_REGISTRY_KEYS), 1575 } }) 1576 1577 await npm.exec('audit', ['signatures']) 1578 1579 t.notOk(process.exitCode, 'should exit successfully') 1580 t.match(joinedOutput(), /audited 1 package/) 1581 t.matchSnapshot(joinedOutput()) 1582 }) 1583 1584 t.test('third-party registry with sub-path (trailing slash)', async t => { 1585 const registryUrl = 'https://verdaccio-clone.org/npm/' 1586 const { npm, joinedOutput } = await loadMockNpm(t, { 1587 prefixDir: installWithThirdPartyRegistry, 1588 config: { 1589 scope: '@npmcli', 1590 registry: registryUrl, 1591 }, 1592 }) 1593 const registry = new MockRegistry({ tap: t, registry: registryUrl }) 1594 1595 const manifest = registry.manifest({ 1596 name: '@npmcli/arborist', 1597 packuments: [{ 1598 version: '1.0.14', 1599 dist: { 1600 tarball: 'https://registry.npmjs.org/@npmcli/arborist/-/@npmcli/arborist-1.0.14.tgz', 1601 integrity: 'sha512-caa8hv5rW9VpQKk6tyNRvSaVDySVjo9GkI7Wj/wcsFyxPm3tYrE' + 1602 'sFyTjSnJH8HCIfEGVQNjqqKXaXLFVp7UBag==', 1603 signatures: [ 1604 { 1605 keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 1606 sig: 'MEUCIAvNpR3G0j7WOPUuVMhE0ZdM8PnDNcsoeFD8Iwz9YWIMAiEAn8cicDC2' + 1607 'Sf9MFQydqTv6S5XYsAh9Af1sig1nApNI11M=', 1608 }, 1609 ], 1610 }, 1611 }], 1612 }) 1613 await registry.package({ manifest }) 1614 1615 mockTUF({ npm, 1616 target: { 1617 name: 'verdaccio-clone.org/npm/keys.json', 1618 content: JSON.stringify(TUF_VALID_REGISTRY_KEYS), 1619 } }) 1620 1621 await npm.exec('audit', ['signatures']) 1622 1623 t.notOk(process.exitCode, 'should exit successfully') 1624 t.match(joinedOutput(), /audited 1 package/) 1625 t.matchSnapshot(joinedOutput()) 1626 }) 1627 1628 t.test('multiple registries with keys and signatures', async t => { 1629 const registryUrl = 'https://verdaccio-clone.org' 1630 const { npm, joinedOutput } = await loadMockNpm(t, { 1631 prefixDir: { 1632 ...installWithMultipleRegistries, 1633 '.npmrc': `@npmcli:registry=${registryUrl}\n`, 1634 }, 1635 }) 1636 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1637 const thirdPartyRegistry = new MockRegistry({ 1638 tap: t, 1639 registry: registryUrl, 1640 }) 1641 await manifestWithValidSigs({ registry }) 1642 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1643 1644 const manifest = thirdPartyRegistry.manifest({ 1645 name: '@npmcli/arborist', 1646 packuments: [{ 1647 version: '1.0.14', 1648 dist: { 1649 tarball: 'https://registry.npmjs.org/@npmcli/arborist/-/@npmcli/arborist-1.0.14.tgz', 1650 integrity: 'sha512-caa8hv5rW9VpQKk6tyNRvSaVDySVjo9GkI7Wj/wcsFyxPm3tYrE' + 1651 'sFyTjSnJH8HCIfEGVQNjqqKXaXLFVp7UBag==', 1652 signatures: [ 1653 { 1654 keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 1655 sig: 'MEUCIAvNpR3G0j7WOPUuVMhE0ZdM8PnDNcsoeFD8Iwz9YWIMAiEAn8cicDC2' + 1656 'Sf9MFQydqTv6S5XYsAh9Af1sig1nApNI11M=', 1657 }, 1658 ], 1659 }, 1660 }], 1661 }) 1662 await thirdPartyRegistry.package({ manifest }) 1663 thirdPartyRegistry.nock.get('/-/npm/v1/keys') 1664 .reply(200, { 1665 keys: [{ 1666 expires: null, 1667 keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 1668 keytype: 'ecdsa-sha2-nistp256', 1669 scheme: 'ecdsa-sha2-nistp256', 1670 key: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+' + 1671 'IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==', 1672 }], 1673 }) 1674 1675 await npm.exec('audit', ['signatures']) 1676 1677 t.notOk(process.exitCode, 'should exit successfully') 1678 t.match(joinedOutput(), /audited 2 packages/) 1679 t.matchSnapshot(joinedOutput()) 1680 }) 1681 1682 t.test('errors with an empty install', async t => { 1683 const { npm } = await loadMockNpm(t, { 1684 prefixDir: { 1685 'package.json': JSON.stringify({ 1686 name: 'test-dep', 1687 version: '1.0.0', 1688 }), 1689 }, 1690 }) 1691 1692 await t.rejects( 1693 npm.exec('audit', ['signatures']), 1694 /found no installed dependencies to audit/ 1695 ) 1696 }) 1697 1698 t.test('errors when TUF errors', async t => { 1699 const { npm } = await loadMockNpm(t, { 1700 prefixDir: installWithMultipleDeps, 1701 mocks: { 1702 sigstore: { 1703 sigstore: { 1704 tuf: { 1705 client: async () => ({ 1706 getTarget: async () => { 1707 throw new Error('error refreshing TUF metadata') 1708 }, 1709 }), 1710 }, 1711 }, 1712 }, 1713 }, 1714 }) 1715 1716 await t.rejects( 1717 npm.exec('audit', ['signatures']), 1718 /error refreshing TUF metadata/ 1719 ) 1720 }) 1721 1722 t.test('errors when the keys endpoint errors', async t => { 1723 const { npm } = await loadMockNpm(t, { 1724 prefixDir: installWithMultipleDeps, 1725 }) 1726 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1727 mockTUF({ npm, target: TUF_TARGET_NOT_FOUND }) 1728 registry.nock.get('/-/npm/v1/keys') 1729 .reply(500, { error: 'keys broke' }) 1730 1731 await t.rejects( 1732 npm.exec('audit', ['signatures']), 1733 /keys broke/ 1734 ) 1735 }) 1736 1737 t.test('ignores optional dependencies', async t => { 1738 const { npm, joinedOutput } = await loadMockNpm(t, { 1739 prefixDir: installWithOptionalDeps, 1740 }) 1741 1742 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1743 await manifestWithValidSigs({ registry }) 1744 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1745 1746 await npm.exec('audit', ['signatures']) 1747 1748 t.notOk(process.exitCode, 'should exit successfully') 1749 t.match(joinedOutput(), /audited 1 package/) 1750 t.matchSnapshot(joinedOutput()) 1751 }) 1752 1753 t.test('errors when no installed dependencies', async t => { 1754 const { npm } = await loadMockNpm(t, { 1755 prefixDir: noInstall, 1756 }) 1757 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1758 1759 await t.rejects( 1760 npm.exec('audit', ['signatures']), 1761 /found no dependencies to audit that where installed from a supported registry/ 1762 ) 1763 }) 1764 1765 t.test('should skip missing non-prod deps', async t => { 1766 const { npm } = await loadMockNpm(t, { 1767 prefixDir: { 1768 'package.json': JSON.stringify({ 1769 name: 'delta', 1770 version: '1.0.0', 1771 devDependencies: { 1772 chai: '^1.0.0', 1773 }, 1774 }, null, 2), 1775 node_modules: {}, 1776 }, 1777 }) 1778 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1779 1780 await t.rejects( 1781 npm.exec('audit', ['signatures']), 1782 /found no dependencies to audit that where installed from a supported registry/ 1783 ) 1784 }) 1785 1786 t.test('should skip invalid pkg ranges', async t => { 1787 const { npm } = await loadMockNpm(t, { 1788 prefixDir: { 1789 'package.json': JSON.stringify({ 1790 name: 'delta', 1791 version: '1.0.0', 1792 dependencies: { 1793 cat: '>=^2', 1794 }, 1795 }, null, 2), 1796 node_modules: { 1797 cat: { 1798 'package.json': JSON.stringify({ 1799 name: 'cat', 1800 version: '1.0.0', 1801 }, null, 2), 1802 }, 1803 }, 1804 }, 1805 }) 1806 mockTUF({ npm, target: TUF_TARGET_NOT_FOUND }) 1807 1808 await t.rejects( 1809 npm.exec('audit', ['signatures']), 1810 /found no dependencies to audit that where installed from a supported registry/ 1811 ) 1812 }) 1813 1814 t.test('should skip git specs', async t => { 1815 const { npm } = await loadMockNpm(t, { 1816 prefixDir: { 1817 'package.json': JSON.stringify({ 1818 name: 'delta', 1819 version: '1.0.0', 1820 dependencies: { 1821 cat: 'github:username/foo', 1822 }, 1823 }, null, 2), 1824 node_modules: { 1825 cat: { 1826 'package.json': JSON.stringify({ 1827 name: 'cat', 1828 version: '1.0.0', 1829 }, null, 2), 1830 }, 1831 }, 1832 }, 1833 }) 1834 1835 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1836 1837 await t.rejects( 1838 npm.exec('audit', ['signatures']), 1839 /found no dependencies to audit that where installed from a supported registry/ 1840 ) 1841 }) 1842 1843 t.test('errors for global packages', async t => { 1844 const { npm } = await loadMockNpm(t, { 1845 config: { global: true }, 1846 }) 1847 1848 await t.rejects( 1849 npm.exec('audit', ['signatures']), 1850 /`npm audit signatures` does not support global packages/, 1851 { code: 'ECIGLOBAL' } 1852 ) 1853 }) 1854 1855 t.test('with invalid signtaures and color output enabled', async t => { 1856 const { npm, joinedOutput } = await loadMockNpm(t, { 1857 prefixDir: installWithValidSigs, 1858 config: { color: 'always' }, 1859 }) 1860 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1861 await manifestWithInvalidSigs({ registry }) 1862 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1863 1864 await npm.exec('audit', ['signatures']) 1865 1866 t.equal(process.exitCode, 1, 'should exit with error') 1867 t.match( 1868 joinedOutput(), 1869 // eslint-disable-next-line no-control-regex 1870 /\u001b\[1m\u001b\[31minvalid\u001b\[39m\u001b\[22m registry signature/ 1871 ) 1872 t.matchSnapshot(joinedOutput()) 1873 }) 1874 1875 t.test('with valid attestations', async t => { 1876 const { npm, joinedOutput } = await loadMockNpm(t, { 1877 prefixDir: installWithValidAttestations, 1878 mocks: { 1879 pacote: t.mock('pacote', { 1880 sigstore: { 1881 sigstore: { verify: async () => true }, 1882 }, 1883 }), 1884 }, 1885 }) 1886 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1887 await manifestWithValidAttestations({ registry }) 1888 const fixture = fs.readFileSync( 1889 path.join(__dirname, '..', 'fixtures', 'sigstore/valid-sigstore-attestations.json'), 1890 'utf8' 1891 ) 1892 registry.nock.get('/-/npm/v1/attestations/sigstore@1.0.0').reply(200, fixture) 1893 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1894 1895 await npm.exec('audit', ['signatures']) 1896 1897 t.notOk(process.exitCode, 'should exit successfully') 1898 t.match(joinedOutput(), /1 package has a verified attestation/) 1899 t.matchSnapshot(joinedOutput()) 1900 }) 1901 1902 t.test('with multiple valid attestations', async t => { 1903 const { npm, joinedOutput } = await loadMockNpm(t, { 1904 prefixDir: installWithMultipleValidAttestations, 1905 mocks: { 1906 pacote: t.mock('pacote', { 1907 sigstore: { 1908 sigstore: { verify: async () => true }, 1909 }, 1910 }), 1911 }, 1912 }) 1913 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1914 await manifestWithValidAttestations({ registry }) 1915 await manifestWithMultipleValidAttestations({ registry }) 1916 const fixture1 = fs.readFileSync( 1917 path.join(__dirname, '..', 'fixtures', 'sigstore/valid-sigstore-attestations.json'), 1918 'utf8' 1919 ) 1920 const fixture2 = fs.readFileSync( 1921 path.join(__dirname, '..', 'fixtures', 'sigstore/valid-tuf-js-attestations.json'), 1922 'utf8' 1923 ) 1924 registry.nock.get('/-/npm/v1/attestations/sigstore@1.0.0').reply(200, fixture1) 1925 registry.nock.get('/-/npm/v1/attestations/tuf-js@1.0.0').reply(200, fixture2) 1926 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1927 1928 await npm.exec('audit', ['signatures']) 1929 1930 t.notOk(process.exitCode, 'should exit successfully') 1931 t.match(joinedOutput(), /2 packages have verified attestations/) 1932 }) 1933 1934 t.test('with invalid attestations', async t => { 1935 const { npm, joinedOutput } = await loadMockNpm(t, { 1936 prefixDir: installWithValidAttestations, 1937 mocks: { 1938 pacote: t.mock('pacote', { 1939 sigstore: { 1940 sigstore: { 1941 verify: async () => { 1942 throw new Error(`artifact signature verification failed`) 1943 }, 1944 }, 1945 }, 1946 }), 1947 }, 1948 }) 1949 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1950 await manifestWithValidAttestations({ registry }) 1951 const fixture = fs.readFileSync( 1952 path.join(__dirname, '..', 'fixtures', 'sigstore/valid-sigstore-attestations.json'), 1953 'utf8' 1954 ) 1955 registry.nock.get('/-/npm/v1/attestations/sigstore@1.0.0').reply(200, fixture) 1956 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1957 1958 await npm.exec('audit', ['signatures']) 1959 1960 t.equal(process.exitCode, 1, 'should exit with error') 1961 t.match( 1962 joinedOutput(), 1963 '1 package has an invalid attestation' 1964 ) 1965 t.matchSnapshot(joinedOutput()) 1966 }) 1967 1968 t.test('json output with invalid attestations', async t => { 1969 const { npm, joinedOutput } = await loadMockNpm(t, { 1970 prefixDir: installWithValidAttestations, 1971 config: { 1972 json: true, 1973 }, 1974 mocks: { 1975 pacote: t.mock('pacote', { 1976 sigstore: { 1977 sigstore: { 1978 verify: async () => { 1979 throw new Error(`artifact signature verification failed`) 1980 }, 1981 }, 1982 }, 1983 }), 1984 }, 1985 }) 1986 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 1987 await manifestWithValidAttestations({ registry }) 1988 const fixture = fs.readFileSync( 1989 path.join(__dirname, '..', 'fixtures', 'sigstore/valid-sigstore-attestations.json'), 1990 'utf8' 1991 ) 1992 registry.nock.get('/-/npm/v1/attestations/sigstore@1.0.0').reply(200, fixture) 1993 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 1994 1995 await npm.exec('audit', ['signatures']) 1996 1997 t.equal(process.exitCode, 1, 'should exit with error') 1998 t.match(joinedOutput(), 'artifact signature verification failed') 1999 t.matchSnapshot(joinedOutput()) 2000 }) 2001 2002 t.test('with multiple invalid attestations', async t => { 2003 const { npm, joinedOutput } = await loadMockNpm(t, { 2004 prefixDir: installWithMultipleValidAttestations, 2005 mocks: { 2006 pacote: t.mock('pacote', { 2007 sigstore: { 2008 sigstore: { 2009 verify: async () => { 2010 throw new Error(`artifact signature verification failed`) 2011 }, 2012 }, 2013 }, 2014 }), 2015 }, 2016 }) 2017 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 2018 await manifestWithValidAttestations({ registry }) 2019 await manifestWithMultipleValidAttestations({ registry }) 2020 const fixture1 = fs.readFileSync( 2021 path.join(__dirname, '..', 'fixtures', 'sigstore/valid-sigstore-attestations.json'), 2022 'utf8' 2023 ) 2024 const fixture2 = fs.readFileSync( 2025 path.join(__dirname, '..', 'fixtures', 'sigstore/valid-tuf-js-attestations.json'), 2026 'utf8' 2027 ) 2028 registry.nock.get('/-/npm/v1/attestations/sigstore@1.0.0').reply(200, fixture1) 2029 registry.nock.get('/-/npm/v1/attestations/tuf-js@1.0.0').reply(200, fixture2) 2030 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 2031 2032 await npm.exec('audit', ['signatures']) 2033 2034 t.equal(process.exitCode, 1, 'should exit with error') 2035 t.match( 2036 joinedOutput(), 2037 '2 packages have invalid attestations' 2038 ) 2039 t.matchSnapshot(joinedOutput()) 2040 }) 2041 2042 t.test('workspaces', async t => { 2043 t.test('verifies registry deps and ignores local workspace deps', async t => { 2044 const { npm, joinedOutput } = await loadMockNpm(t, { 2045 prefixDir: workspaceInstall, 2046 }) 2047 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 2048 await manifestWithValidSigs({ registry }) 2049 const asyncManifest = registry.manifest({ 2050 name: 'async', 2051 packuments: [{ 2052 version: '2.5.0', 2053 dist: { 2054 tarball: 'https://registry.npmjs.org/async/-/async-2.5.0.tgz', 2055 integrity: 'sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFT' 2056 + 'KE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==', 2057 signatures: [ 2058 { 2059 keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 2060 sig: 'MEUCIQCM8cX2U3IVZKKhzQx1w5AlNSDUI+fVf4857K1qT0NTNgIgdT4qwEl' + 2061 '/kg2vU1uIWUI0bGikRvVHCHlRs1rgjPMpRFA=', 2062 }, 2063 ], 2064 }, 2065 }], 2066 }) 2067 const lightCycleManifest = registry.manifest({ 2068 name: 'light-cycle', 2069 packuments: [{ 2070 version: '1.4.2', 2071 dist: { 2072 tarball: 'https://registry.npmjs.org/light-cycle/-/light-cycle-1.4.2.tgz', 2073 integrity: 'sha512-badZ3KMUaGwQfVcHjXTXSecYSXxT6f99bT+kVzBqmO10U1UNlE' + 2074 'thJ1XAok97E4gfDRTA2JJ3r0IeMPtKf0EJMw==', 2075 signatures: [ 2076 { 2077 keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 2078 sig: 'MEUCIQDXjoxQz4MzPqaIuy2RJmBlcFp0UD3h9EhKZxxEz9IYZAIgLO0znG5' + 2079 'aGciTAg4u8fE0/UXBU4gU7JcvTZGxW2BmKGw=', 2080 }, 2081 ], 2082 }, 2083 }], 2084 }) 2085 await registry.package({ manifest: asyncManifest }) 2086 await registry.package({ manifest: lightCycleManifest }) 2087 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 2088 2089 await npm.exec('audit', ['signatures']) 2090 2091 t.notOk(process.exitCode, 'should exit successfully') 2092 t.match(joinedOutput(), /audited 3 packages/) 2093 t.matchSnapshot(joinedOutput()) 2094 }) 2095 2096 t.test('verifies registry deps when filtering by workspace name', async t => { 2097 const { npm, joinedOutput } = await loadMockNpm(t, { 2098 prefixDir: workspaceInstall, 2099 config: { workspace: './packages/a' }, 2100 }) 2101 const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) 2102 const asyncManifest = registry.manifest({ 2103 name: 'async', 2104 packuments: [{ 2105 version: '2.5.0', 2106 dist: { 2107 tarball: 'https://registry.npmjs.org/async/-/async-2.5.0.tgz', 2108 integrity: 'sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFT' 2109 + 'KE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==', 2110 signatures: [ 2111 { 2112 keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 2113 sig: 'MEUCIQCM8cX2U3IVZKKhzQx1w5AlNSDUI+fVf4857K1qT0NTNgIgdT4qwEl' + 2114 '/kg2vU1uIWUI0bGikRvVHCHlRs1rgjPMpRFA=', 2115 }, 2116 ], 2117 }, 2118 }], 2119 }) 2120 const lightCycleManifest = registry.manifest({ 2121 name: 'light-cycle', 2122 packuments: [{ 2123 version: '1.4.2', 2124 dist: { 2125 tarball: 'https://registry.npmjs.org/light-cycle/-/light-cycle-1.4.2.tgz', 2126 integrity: 'sha512-badZ3KMUaGwQfVcHjXTXSecYSXxT6f99bT+kVzBqmO10U1UNlE' + 2127 'thJ1XAok97E4gfDRTA2JJ3r0IeMPtKf0EJMw==', 2128 signatures: [ 2129 { 2130 keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', 2131 sig: 'MEUCIQDXjoxQz4MzPqaIuy2RJmBlcFp0UD3h9EhKZxxEz9IYZAIgLO0znG5' + 2132 'aGciTAg4u8fE0/UXBU4gU7JcvTZGxW2BmKGw=', 2133 }, 2134 ], 2135 }, 2136 }], 2137 }) 2138 await registry.package({ manifest: asyncManifest }) 2139 await registry.package({ manifest: lightCycleManifest }) 2140 mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) 2141 2142 await npm.exec('audit', ['signatures']) 2143 2144 t.notOk(process.exitCode, 'should exit successfully') 2145 t.match(joinedOutput(), /audited 2 packages/) 2146 t.matchSnapshot(joinedOutput()) 2147 }) 2148 2149 // TODO: This should verify kms-demo, but doesn't because arborist filters 2150 // workspace deps even if they're also root deps 2151 t.test('verifies registry dep if workspaces is disabled', async t => { 2152 const { npm } = await loadMockNpm(t, { 2153 prefixDir: workspaceInstall, 2154 config: { workspaces: false }, 2155 }) 2156 2157 await t.rejects( 2158 npm.exec('audit', ['signatures']), 2159 /found no installed dependencies to audit/ 2160 ) 2161 }) 2162 }) 2163}) 2164