From 608b22f65dec11a88be83b3f3ce97e6c9d6434dd Mon Sep 17 00:00:00 2001 From: Gar Date: Thu, 24 Aug 2023 08:43:30 -0700 Subject: [PATCH 1/2] feat: add no-package-lock mode to npm audit --- docs/lib/content/commands/npm-audit.md | 7 +++++++ docs/lib/content/commands/npm-query.md | 7 +++++++ lib/commands/audit.js | 6 +++++- tap-snapshots/test/lib/docs.js.test.cjs | 3 ++- test/lib/commands/audit.js | 12 ++++++++++++ workspaces/arborist/lib/arborist/audit.js | 10 +++++++++- workspaces/arborist/test/arborist/audit.js | 9 +++++++++ 7 files changed, 51 insertions(+), 3 deletions(-) diff --git a/docs/lib/content/commands/npm-audit.md b/docs/lib/content/commands/npm-audit.md index 7a39b34d875be..085b7b2ea5613 100644 --- a/docs/lib/content/commands/npm-audit.md +++ b/docs/lib/content/commands/npm-audit.md @@ -30,6 +30,13 @@ vulnerability is found. It may be useful in CI environments to include the will cause the command to fail. This option does not filter the report output, it simply changes the command's failure threshold. +### Package lock + +By default npm requires a package-lock or shrinkwrap in order to run the +audit. You can bypass the package lock with `--no-package-lock` but be +aware the results may be different with every run, since npm will +re-build the dependency tree each time. + ### Audit Signatures To ensure the integrity of packages you download from the public npm registry, or any registry that supports signatures, you can verify the registry signatures of downloaded packages using the npm CLI. diff --git a/docs/lib/content/commands/npm-query.md b/docs/lib/content/commands/npm-query.md index e6bf53f3de614..42b2d086f3bbe 100644 --- a/docs/lib/content/commands/npm-query.md +++ b/docs/lib/content/commands/npm-query.md @@ -134,6 +134,13 @@ npm query ":type(git)" | jq 'map(.name)' | xargs -I {} npm why {} ... ``` +### Package lock only mode + +If package-lock-only is enabled, only the information in the package +lock (or shrinkwrap) is loaded. This means that information from the +package.json files of your dependencies will not be included in the +result set (e.g. description, homepage, engines). + ### Configuration diff --git a/lib/commands/audit.js b/lib/commands/audit.js index 500620f2cd01b..f5e5855145278 100644 --- a/lib/commands/audit.js +++ b/lib/commands/audit.js @@ -404,6 +404,7 @@ class Audit extends ArboristWorkspaceCmd { 'force', 'json', 'package-lock-only', + 'package-lock', 'omit', 'foreground-scripts', 'ignore-scripts', @@ -439,6 +440,10 @@ class Audit extends ArboristWorkspaceCmd { } async auditAdvisories (args) { + const fix = args[0] === 'fix' + if (this.npm.config.get('package-lock') === false && fix) { + throw this.usageError('fix can not be used without a package-lock') + } const reporter = this.npm.config.get('json') ? 'json' : 'detail' const Arborist = require('@npmcli/arborist') const opts = { @@ -450,7 +455,6 @@ class Audit extends ArboristWorkspaceCmd { } const arb = new Arborist(opts) - const fix = args[0] === 'fix' await arb.audit({ fix }) if (fix) { await reifyFinish(this.npm, arb) diff --git a/tap-snapshots/test/lib/docs.js.test.cjs b/tap-snapshots/test/lib/docs.js.test.cjs index 2c8f8124235b6..8cf8afcdb7ee8 100644 --- a/tap-snapshots/test/lib/docs.js.test.cjs +++ b/tap-snapshots/test/lib/docs.js.test.cjs @@ -2526,7 +2526,7 @@ npm audit [fix|signatures] Options: [--audit-level ] [--dry-run] [-f|--force] -[--json] [--package-lock-only] +[--json] [--package-lock-only] [--no-package-lock] [--omit [--omit ...]] [--foreground-scripts] [--ignore-scripts] [-w|--workspace [-w|--workspace ...]] @@ -2543,6 +2543,7 @@ npm audit [fix|signatures] #### \`force\` #### \`json\` #### \`package-lock-only\` +#### \`package-lock\` #### \`omit\` #### \`foreground-scripts\` #### \`ignore-scripts\` diff --git a/test/lib/commands/audit.js b/test/lib/commands/audit.js index 4014e73387351..ea04cea53cecc 100644 --- a/test/lib/commands/audit.js +++ b/test/lib/commands/audit.js @@ -210,6 +210,18 @@ t.test('audit fix - bulk endpoint', async t => { ) }) +t.test('audit fix no package lock', async t => { + const { npm } = await loadMockNpm(t, { + config: { + 'package-lock': false, + }, + }) + await t.rejects( + npm.exec('audit', ['fix']), + { code: 'EUSAGE' } + ) +}) + t.test('completion', async t => { const { audit } = await loadMockNpm(t, { command: 'audit' }) t.test('fix', async t => { diff --git a/workspaces/arborist/lib/arborist/audit.js b/workspaces/arborist/lib/arborist/audit.js index eb4a3565531cc..af260bdc996fc 100644 --- a/workspaces/arborist/lib/arborist/audit.js +++ b/workspaces/arborist/lib/arborist/audit.js @@ -22,7 +22,15 @@ module.exports = cls => class Auditor extends cls { options = { ...this.options, ...options } process.emit('time', 'audit') - const tree = await this.loadVirtual() + let tree + if (options.packageLock === false) { + // build ideal tree + await this.loadActual(options) + await this.buildIdealTree() + tree = this.idealTree + } else { + tree = await this.loadVirtual() + } if (this[_workspaces] && this[_workspaces].length) { options.filterSet = this.workspaceDependencySet( tree, diff --git a/workspaces/arborist/test/arborist/audit.js b/workspaces/arborist/test/arborist/audit.js index ecfd9d70a0508..bf301eb62d11f 100644 --- a/workspaces/arborist/test/arborist/audit.js +++ b/workspaces/arborist/test/arborist/audit.js @@ -27,6 +27,15 @@ t.test('audit finds the bad deps', async t => { t.equal(report.size, 2) }) +t.test('no package lock finds no bad deps', async t => { + const path = resolve(fixtures, 'deprecated-dep') + t.teardown(auditResponse(resolve(fixtures, 'audit-nyc-mkdirp/audit.json'))) + const arb = newArb(path, { packageLock: false }) + const report = await arb.audit() + t.equal(report.topVulns.size, 0) + t.equal(report.size, 0) +}) + t.test('audit fix reifies out the bad deps', async t => { const path = fixture(t, 'deprecated-dep') t.teardown(auditResponse(resolve(fixtures, 'audit-nyc-mkdirp/audit.json'))) From 936a143d215c7ec136b7ab10f1294a95b3c97b3b Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 23 Aug 2023 11:26:14 -0700 Subject: [PATCH 2/2] feat: add package-lock-only mode to npm query --- docs/lib/content/commands/npm-query.md | 6 ++ lib/commands/query.js | 15 ++++- .../test/lib/commands/query.js.test.cjs | 48 +++++++++++++++ tap-snapshots/test/lib/docs.js.test.cjs | 3 +- test/lib/commands/query.js | 58 +++++++++++++++++++ 5 files changed, 128 insertions(+), 2 deletions(-) diff --git a/docs/lib/content/commands/npm-query.md b/docs/lib/content/commands/npm-query.md index 42b2d086f3bbe..9907d4aa2c79c 100644 --- a/docs/lib/content/commands/npm-query.md +++ b/docs/lib/content/commands/npm-query.md @@ -133,6 +133,12 @@ npm query ":type(git)" | jq 'map(.name)' | xargs -I {} npm why {} }, ... ``` +### Package lock only mode + +If package-lock-only is enabled, only the information in the package +lock (or shrinkwrap) is loaded. This means that information from the +package.json files of your dependencies will not be included in the +result set (e.g. description, homepage, engines). ### Package lock only mode diff --git a/lib/commands/query.js b/lib/commands/query.js index ba39f004fae23..68aa1fa2c06a5 100644 --- a/lib/commands/query.js +++ b/lib/commands/query.js @@ -2,6 +2,7 @@ const { resolve } = require('path') const BaseCommand = require('../base-command.js') +const log = require('../utils/log-shim.js') class QuerySelectorItem { constructor (node) { @@ -48,6 +49,7 @@ class Query extends BaseCommand { 'workspace', 'workspaces', 'include-workspace-root', + 'package-lock-only', ] get parsedResponse () { @@ -64,7 +66,18 @@ class Query extends BaseCommand { forceActual: true, } const arb = new Arborist(opts) - const tree = await arb.loadActual(opts) + let tree + if (this.npm.config.get('package-lock-only')) { + try { + tree = await arb.loadVirtual() + } catch (err) { + log.verbose('loadVirtual', err.stack) + /* eslint-disable-next-line max-len */ + throw this.usageError('A package lock or shrinkwrap file is required in package-lock-only mode') + } + } else { + tree = await arb.loadActual(opts) + } const items = await tree.querySelectorAll(args[0], this.npm.flatOptions) this.buildResponse(items) diff --git a/tap-snapshots/test/lib/commands/query.js.test.cjs b/tap-snapshots/test/lib/commands/query.js.test.cjs index a6dbfcf7c693c..e732135468965 100644 --- a/tap-snapshots/test/lib/commands/query.js.test.cjs +++ b/tap-snapshots/test/lib/commands/query.js.test.cjs @@ -99,6 +99,54 @@ exports[`test/lib/commands/query.js TAP linked node > should return linked node ] ` +exports[`test/lib/commands/query.js TAP package-lock-only with package lock > should return valid response with only lock info 1`] = ` +[ + { + "name": "project", + "dependencies": { + "a": "^1.0.0" + }, + "pkgid": "project@", + "location": "", + "path": "{CWD}/prefix", + "realpath": "{CWD}/prefix", + "resolved": null, + "from": [], + "to": [ + "node_modules/a" + ], + "dev": false, + "inBundle": false, + "deduped": false, + "overridden": false, + "queryContext": {} + }, + { + "version": "1.2.3", + "resolved": "https://dummy.npmjs.org/a/-/a-1.2.3.tgz", + "integrity": "sha512-dummy", + "engines": { + "node": ">=14.17" + }, + "name": "a", + "_id": "a@1.2.3", + "pkgid": "a@1.2.3", + "location": "node_modules/a", + "path": "{CWD}/prefix/node_modules/a", + "realpath": "{CWD}/prefix/node_modules/a", + "from": [ + "" + ], + "to": [], + "dev": false, + "inBundle": false, + "deduped": false, + "overridden": false, + "queryContext": {} + } +] +` + exports[`test/lib/commands/query.js TAP recursive tree > should return everything in the tree, accounting for recursion 1`] = ` [ { diff --git a/tap-snapshots/test/lib/docs.js.test.cjs b/tap-snapshots/test/lib/docs.js.test.cjs index 8cf8afcdb7ee8..b70b2fbe115ac 100644 --- a/tap-snapshots/test/lib/docs.js.test.cjs +++ b/tap-snapshots/test/lib/docs.js.test.cjs @@ -3767,7 +3767,7 @@ npm query Options: [-g|--global] [-w|--workspace [-w|--workspace ...]] -[-ws|--workspaces] [--include-workspace-root] +[-ws|--workspaces] [--include-workspace-root] [--package-lock-only] Run "npm help query" for more info @@ -3779,6 +3779,7 @@ npm query #### \`workspace\` #### \`workspaces\` #### \`include-workspace-root\` +#### \`package-lock-only\` ` exports[`test/lib/docs.js TAP usage rebuild > must match snapshot 1`] = ` diff --git a/test/lib/commands/query.js b/test/lib/commands/query.js index 2b9a5b4976323..adf617316772e 100644 --- a/test/lib/commands/query.js +++ b/test/lib/commands/query.js @@ -179,3 +179,61 @@ t.test('global', async t => { await npm.exec('query', ['[name=lorem]']) t.matchSnapshot(joinedOutput(), 'should return global package') }) + +t.test('package-lock-only', t => { + t.test('no package lock', async t => { + const { npm } = await loadMockNpm(t, { + config: { + 'package-lock-only': true, + }, + prefixDir: { + 'package.json': JSON.stringify({ + name: 'project', + dependencies: { + a: '^1.0.0', + }, + }), + }, + }) + await t.rejects(npm.exec('query', [':root, :root > *']), { code: 'EUSAGE' }) + }) + + t.test('with package lock', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { + 'package-lock-only': true, + }, + prefixDir: { + 'package.json': JSON.stringify({ + name: 'project', + dependencies: { + a: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + name: 'project', + lockfileVersion: 3, + requires: true, + packages: { + '': { + dependencies: { + a: '^1.0.0', + }, + }, + 'node_modules/a': { + version: '1.2.3', + resolved: 'https://dummy.npmjs.org/a/-/a-1.2.3.tgz', + integrity: 'sha512-dummy', + engines: { + node: '>=14.17', + }, + }, + }, + }), + }, + }) + await npm.exec('query', ['*']) + t.matchSnapshot(joinedOutput(), 'should return valid response with only lock info') + }) + t.end() +})