diff --git a/doc/cli/npm-audit.md b/doc/cli/npm-audit.md index 4c6d717418ab3..e551134458868 100644 --- a/doc/cli/npm-audit.md +++ b/doc/cli/npm-audit.md @@ -3,11 +3,21 @@ npm-audit(1) -- Run a security audit ## SYNOPSIS - npm audit [--json|--parseable] + npm audit [--json|--parseable|--production|--only=dev] npm audit fix [--force|--package-lock-only|--dry-run|--production|--only=dev] ## EXAMPLES +List vulnerabilities with a level of `high` or higher: +``` +$ npm audit --audit-level high +``` + +List vulnerabilities in `devDependencies`: +``` +$ npm audit --only=dev +``` + Scan your project for vulnerabilities and automatically install any compatible updates to vulnerable dependencies: ``` diff --git a/doc/misc/npm-config.md b/doc/misc/npm-config.md index 8f04a76010c1a..e58a0a34d6d2c 100644 --- a/doc/misc/npm-config.md +++ b/doc/misc/npm-config.md @@ -153,7 +153,8 @@ even for `GET` requests. * Type: String When "dev" or "development" and running local `npm shrinkwrap`, -`npm outdated`, or `npm update`, is an alias for `--dev`. +`npm outdated`, or `npm update`, or `npm audit`, is an alias for +`--dev`. ### audit @@ -329,7 +330,7 @@ Show the description in `npm search` * Default: false * Type: Boolean -Install `dev-dependencies` along with packages. +Install or audit `devDependencies` along with packages. ### dry-run @@ -723,15 +724,15 @@ usage. When "dev" or "development" and running local `npm install` without any arguments, only devDependencies (and their dependencies) are installed. -When "dev" or "development" and running local `npm ls`, `npm outdated`, or -`npm update`, is an alias for `--dev`. +When "dev" or "development" and running local `npm ls`, `npm outdated`, +`npm update`, or `npm audit`, is an alias for `--dev`. When "prod" or "production" and running local `npm install` without any arguments, only non-devDependencies (and their dependencies) are installed. -When "prod" or "production" and running local `npm ls`, `npm outdated`, or -`npm update`, is an alias for `--production`. +When "prod" or "production" and running local `npm ls`, `npm outdated`, +`npm update`, or `npm audit`, is an alias for `--production`. ### optional @@ -822,8 +823,9 @@ semver. Like the `rc` in `1.2.0-rc.8`. Set to true to run in "production" mode. 1. devDependencies are not installed at the topmost level when running - local `npm install` without any arguments. -2. Set the NODE_ENV="production" for lifecycle scripts. + local `npm install` without any arguments or `npm audit fix`. +2. devDependencies are excluded from the npm audit report +3. Set the NODE_ENV="production" for lifecycle scripts. ### progress diff --git a/lib/audit.js b/lib/audit.js index 076ca256b7c72..43e4195d8444b 100644 --- a/lib/audit.js +++ b/lib/audit.js @@ -130,15 +130,18 @@ function maybeReadFile (name) { }) } -function filterEnv (action, opts) { +function getEnvFilter (opts) { const includeDev = opts.dev || (!/^prod(uction)?$/.test(opts.only) && !opts.production) || /^dev(elopment)?$/.test(opts.only) || /^dev(elopment)?$/.test(opts.also) const includeProd = !/^dev(elopment)?$/.test(opts.only) - const resolves = action.resolves.filter(({dev}) => { - return (dev && includeDev) || (!dev && includeProd) - }) + + return ({dev}) => (dev && includeDev) || (!dev && includeProd) +} + +function filterEnv (action, envFilter) { + const resolves = action.resolves.filter(envFilter) if (resolves.length) { return Object.assign({}, action, {resolves}) } @@ -197,9 +200,10 @@ function auditCmd (args, cb) { } throw err }).then((auditResult) => { + const envFilter = getEnvFilter(opts) if (args[0] === 'fix') { const actions = (auditResult.actions || []).reduce((acc, action) => { - action = filterEnv(action, opts) + action = filterEnv(action, envFilter) if (!action) { return acc } if (action.isMajor) { acc.major.add(`${action.module}@${action.target}`) @@ -280,9 +284,15 @@ function auditCmd (args, cb) { } else { const levels = ['low', 'moderate', 'high', 'critical'] const minLevel = levels.indexOf(opts['audit-level']) - const vulns = levels.reduce((count, level, i) => { - return i < minLevel ? count : count + (auditResult.metadata.vulnerabilities[level] || 0) - }, 0) + let vulns = 0 + + for (const id in auditResult.advisories) { + const advisory = auditResult.advisories[id] + if (levels.indexOf(advisory.severity) >= minLevel) { + vulns += advisory.findings.filter(envFilter).length + } + } + if (vulns > 0) process.exitCode = 1 if (opts.parseable) { return audit.printParseableReport(auditResult) diff --git a/test/tap/audit.js b/test/tap/audit.js index 3384579f77ba7..a6619c84f0412 100644 --- a/test/tap/audit.js +++ b/test/tap/audit.js @@ -94,6 +94,12 @@ test('exits with zero exit code for vulnerabilities below the `audit-level` flag target: '1.2.3', resolves: [{path: 'baddep'}] }], + advisories: { + '1': { + severity: 'low', + findings: [{dev: false}] + } + }, metadata: { vulnerabilities: { low: 1 @@ -169,6 +175,12 @@ test('exits with non-zero exit code for vulnerabilities at the `audit-level` fla target: '1.2.3', resolves: [{path: 'baddep'}] }], + advisories: { + '1': { + severity: 'high', + findings: [{dev: false}] + } + }, metadata: { vulnerabilities: { high: 1 @@ -188,7 +200,7 @@ test('exits with non-zero exit code for vulnerabilities at the `audit-level` fla }) }) -test('exits with non-zero exit code for vulnerabilities at the `audit-level` flag', t => { +test('exits with non-zero exit code for vulnerabilities above the `audit-level` flag', t => { const fixture = new Tacks(new Dir({ 'package.json': new File({ name: 'foo', @@ -244,6 +256,12 @@ test('exits with non-zero exit code for vulnerabilities at the `audit-level` fla target: '1.2.3', resolves: [{path: 'baddep'}] }], + advisories: { + '1': { + severity: 'high', + findings: [{dev: false}] + } + }, metadata: { vulnerabilities: { high: 1 @@ -263,6 +281,88 @@ test('exits with non-zero exit code for vulnerabilities at the `audit-level` fla }) }) +test('exits with zero exit code for vulnerabilities not included with --only, --also and --production flags', t => { + const fixture = new Tacks(new Dir({ + 'package.json': new File({ + name: 'foo', + version: '1.0.0', + dependencies: { + baddep: '1.0.0' + } + }) + })) + fixture.create(testDir) + return tmock(t).then(srv => { + srv.filteringRequestBody(req => 'ok') + srv.post('/-/npm/v1/security/audits/quick', 'ok').reply(200, 'yeah') + srv.get('/baddep').twice().reply(200, { + name: 'baddep', + 'dist-tags': { + 'latest': '1.2.3' + }, + versions: { + '1.0.0': { + name: 'baddep', + version: '1.0.0', + _hasShrinkwrap: false, + dist: { + shasum: 'deadbeef', + tarball: common.registry + '/idk/-/idk-1.0.0.tgz' + } + }, + '1.2.3': { + name: 'baddep', + version: '1.2.3', + _hasShrinkwrap: false, + dist: { + shasum: 'deadbeef', + tarball: common.registry + '/idk/-/idk-1.2.3.tgz' + } + } + } + }) + return common.npm([ + 'install', + '--audit', + '--json', + '--package-lock-only', + '--registry', common.registry, + '--cache', path.join(testDir, 'npm-cache') + ], EXEC_OPTS).then(([code, stdout, stderr]) => { + srv.filteringRequestBody(req => 'ok') + srv.post('/-/npm/v1/security/audits', 'ok').reply(200, { + actions: [{ + action: 'update', + module: 'baddep', + target: '1.2.3', + resolves: [{path: 'baddep'}] + }], + advisories: { + '1': { + severity: 'high', + findings: [{dev: false}] + } + }, + metadata: { + vulnerabilities: { + high: 1 + } + } + }) + return common.npm([ + 'audit', + '--audit-level', 'moderate', + '--only', 'development', + '--json', + '--registry', common.registry, + '--cache', path.join(testDir, 'npm-cache') + ], EXEC_OPTS).then(([code, stdout, stderr]) => { + t.equal(code, 0, 'exited OK') + }) + }) + }) +}) + test('cleanup', t => { return rimraf(testDir) })