Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions docs/content/cli-commands/npm-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ description: Run a security audit
### Synopsis

```bash
npm audit [--json|--parseable|--audit-level=(low|moderate|high|critical)]
npm audit [--json|--parseable|--audit-level=(low|moderate|high|critical)|--acknowledged-issues=(comma,separated,list,of,issues)]
npm audit fix [--force|--package-lock-only|--dry-run]

common options: [--production] [--only=(dev|prod)]
Expand Down Expand Up @@ -76,6 +76,11 @@ Fail an audit only if the results include a vulnerability with a level of modera
$ npm audit --audit-level=moderate
```

Do not fail an audit if the only reason for failure would be issues: 1234 and 2314
```bash
$ npm audit --acknowledged-issues 1234,2314
```

### Description

The audit command submits a description of the dependencies configured in
Expand All @@ -93,8 +98,9 @@ installer will also apply to `npm install` -- so things like `npm audit fix

By default, the audit command will exit with a non-zero code if any vulnerability
is found. It may be useful in CI environments to include the `--audit-level` parameter
to specify the minimum vulnerability level that will cause the command to fail. This
option does not filter the report output, it simply changes the command's failure
to specify the minimum vulnerability level that will cause the command to fail. One
can also acknowledge issues by using `--acknowledged-issues` option.
These options do not filter the report output, it simply changes the command's failure
threshold.

### Content Submitted
Expand Down Expand Up @@ -127,7 +133,7 @@ different between runs.
The `npm audit` command will exit with a 0 exit code if no vulnerabilities were found.

If vulnerabilities were found the exit code will depend on the `audit-level`
configuration setting.
and `acknowledged-issues` configuration setting.

### See Also

Expand Down
8 changes: 8 additions & 0 deletions docs/content/using-npm/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,14 @@ for [`npm audit`](/cli-commands/audit) for details on what is submitted.
The minimum level of vulnerability for `npm audit` to exit with
a non-zero exit code.

#### acknowledged-issues

* Default: `''`
* Type: String

Comma separated list of acknowledged issues that should not cause `npm audit` to exit with
a non-zero exit code.

#### auth-type

* Default: `'legacy'`
Expand Down
32 changes: 27 additions & 5 deletions lib/audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const parseJson = require('json-parse-better-errors')
const readFile = Bluebird.promisify(fs.readFile)

const AuditConfig = figgyPudding({
'acknowledged-issues': {},
also: {},
'audit-level': {},
deepArgs: 'deep-args',
Expand All @@ -41,7 +42,7 @@ auditCmd.usage = usage(
'audit',
'\nnpm audit [--json] [--production]' +
'\nnpm audit fix ' +
'[--force|--package-lock-only|--dry-run|--production|--only=(dev|prod)]'
'[--force|--package-lock-only|--dry-run|--production|--only=(dev|prod)|--acknowledged-issues=(comma,separated,list,of,issues)]'
)

auditCmd.completion = function (opts, cb) {
Expand Down Expand Up @@ -291,10 +292,31 @@ 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)
if (vulns > 0) process.exitCode = 1
const acknowledgedIssues = opts['acknowledged-issues'] ? opts['acknowledged-issues'].split(',') : []
const advisories = auditResult.advisories
if (advisories) {
for (const issueId in advisories) {
if (!advisories.hasOwnProperty(issueId)) {
continue
}
const advisory = advisories[issueId]
if (levels.indexOf(advisory.severity || 0) < minLevel) {
continue
}
if (acknowledgedIssues.indexOf(issueId) >= 0) {
continue
}
process.exitCode = 1
break
}
} else {
if (levels.reduce((count, level, i) => {
return i < minLevel ? count : count + (auditResult.metadata.vulnerabilities[level] || 0)
}, 0) > 0) {
process.exitCode = 1
}
}

if (opts.parseable) {
return audit.printParseableReport(auditResult)
} else {
Expand Down
2 changes: 2 additions & 0 deletions lib/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ Object.defineProperty(exports, 'defaults', {get: function () {

defaults = {
access: null,
'acknowledged-issues': '',
'allow-same-version': false,
'always-auth': false,
also: null,
Expand Down Expand Up @@ -259,6 +260,7 @@ Object.defineProperty(exports, 'defaults', {get: function () {

exports.types = {
access: [null, 'restricted', 'public'],
'acknowledged-issues': String,
'allow-same-version': Boolean,
'always-auth': Boolean,
also: [null, 'dev', 'development'],
Expand Down
106 changes: 106 additions & 0 deletions test/tap/audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,112 @@ const quickAuditResult = {
}
}

function _runAuditTest (t, params, expectedCode) {
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, quickAuditResult)
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]) => {
const result = JSON.parse(stdout)
t.same(result.audit, quickAuditResult, 'printed quick audit result')
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' }]
}],
metadata: {
vulnerabilities: {
low: 1
}
},
advisories: {
'1316': {
'id': 1316,
'severity': 'high'
}
}
})
return common.npm([
'audit',
'--json',
'--registry', common.registry,
'--cache', path.join(testDir, 'npm-cache')
].concat(params), EXEC_OPTS).then(([code, stdout, stderr]) => {
t.equal(code, expectedCode, 'exited as expected')
})
})
})
}

test('exits with zero exit code when auditing for vulnerability that is marked as known', t => {
return _runAuditTest(t, ['--acknowledged-issues', '1316'], 0)
})

test('exits with non zero exit code when auditing for vulnerabilities that are not marked as known', t => {
return _runAuditTest(t, ['--acknowledged-issues', '6131'], 1)
})

test('exits with zero exit code when auditing for vulnerabilities that are marked as known', t => {
return _runAuditTest(t, ['--acknowledged-issues', '1316,1234,5311'], 0)
})

test('exits with zero exit code when auditing for vulnerability that is marked as known and is below audit-level', t => {
return _runAuditTest(t, ['--acknowledged-issues', '1316', '--audit-level', 'critical'], 0)
})

test('exits with zero exit code when auditing for vulnerabilities that is marked as known and are above audit-level', t => {
return _runAuditTest(t, ['--acknowledged-issues', '1316', '--audit-level', 'low'], 0)
})

test('exits with zero exit code when issue is above audit-level and it is not acknowledged', t => {
return _runAuditTest(t, ['--acknowledged-issues', '6131', '--audit-level', 'low'], 1)
})

test('exits with zero exit code for vulnerabilities below the `audit-level` flag', t => {
const fixture = new Tacks(new Dir({
'package.json': new File({
Expand Down