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