Skip to content

Commit 278e80c

Browse files
committed
Add acknowledged issue list to npm audit
Currently `npm audit` is used in many CI systems. Many of them are blocking, that means that without a positive result from the tool it is not possible to integrate anything. While having a tool that can block CI process based on an external event, like a newly discovered vulnerability, is arguable, it is hard to dismiss lack of options to filter importance of found issues. 'npm audit' already has two options that allows to "ignore" certain vulnerabilities, which is crucial in context of CI. It is "audit-level" and "only". Sadly they do not provide enough granularity. As a result in many cases the only way to dismiss known errors is to disable the audit completely. The change creates an option to add a list of known issues that should not cause `npm audit` to return non-zero exit code. Therefore it is easy to unlock CI and put task of the issue resolution into an adequate, organization/project dependent workflow.
1 parent abdf528 commit 278e80c

File tree

5 files changed

+148
-9
lines changed

5 files changed

+148
-9
lines changed

docs/content/cli-commands/npm-audit.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ description: Run a security audit
1111
### Synopsis
1212

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

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

79+
Do not fail an audit if the only reason for failure would be issues: 1234 and 2314
80+
```bash
81+
$ npm audit --acknowledged-issues 1234,2314
82+
```
83+
7984
### Description
8085

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

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

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

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

132138
### See Also
133139

docs/content/using-npm/config.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@ for [`npm audit`](/cli-commands/audit) for details on what is submitted.
189189
The minimum level of vulnerability for `npm audit` to exit with
190190
a non-zero exit code.
191191

192+
#### acknowledged-issues
193+
194+
* Default: `''`
195+
* Type: String
196+
197+
Comma separated list of acknowledged issues that should not cause `npm audit` to exit with
198+
a non-zero exit code.
199+
192200
#### auth-type
193201

194202
* Default: `'legacy'`

lib/audit.js

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const parseJson = require('json-parse-better-errors')
1717
const readFile = Bluebird.promisify(fs.readFile)
1818

1919
const AuditConfig = figgyPudding({
20+
'acknowledged-issues': {},
2021
also: {},
2122
'audit-level': {},
2223
deepArgs: 'deep-args',
@@ -41,7 +42,7 @@ auditCmd.usage = usage(
4142
'audit',
4243
'\nnpm audit [--json] [--production]' +
4344
'\nnpm audit fix ' +
44-
'[--force|--package-lock-only|--dry-run|--production|--only=(dev|prod)]'
45+
'[--force|--package-lock-only|--dry-run|--production|--only=(dev|prod)|--acknowledged-issues=(comma,separated,list,of,issues)]'
4546
)
4647

4748
auditCmd.completion = function (opts, cb) {
@@ -291,10 +292,26 @@ function auditCmd (args, cb) {
291292
} else {
292293
const levels = ['low', 'moderate', 'high', 'critical']
293294
const minLevel = levels.indexOf(opts['audit-level'])
294-
const vulns = levels.reduce((count, level, i) => {
295-
return i < minLevel ? count : count + (auditResult.metadata.vulnerabilities[level] || 0)
296-
}, 0)
297-
if (vulns > 0) process.exitCode = 1
295+
const acknowledgedIssues = opts['acknowledged-issues'] ? opts['acknowledged-issues'].split(',') : []
296+
if (auditResult.advisories) {
297+
for (const [issueId, advisory] of Object.entries(auditResult.advisories)) {
298+
if (levels.indexOf(advisory.severity || 0) < minLevel) {
299+
continue
300+
}
301+
if (acknowledgedIssues.indexOf(issueId) >= 0) {
302+
continue
303+
}
304+
process.exitCode = 1
305+
break
306+
}
307+
} else {
308+
if (levels.reduce((count, level, i) => {
309+
return i < minLevel ? count : count + (auditResult.metadata.vulnerabilities[level] || 0)
310+
}, 0) > 0) {
311+
process.exitCode = 1
312+
}
313+
}
314+
298315
if (opts.parseable) {
299316
return audit.printParseableReport(auditResult)
300317
} else {

lib/config/defaults.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ Object.defineProperty(exports, 'defaults', {get: function () {
106106

107107
defaults = {
108108
access: null,
109+
'acknowledged-issues': '',
109110
'allow-same-version': false,
110111
'always-auth': false,
111112
also: null,
@@ -259,6 +260,7 @@ Object.defineProperty(exports, 'defaults', {get: function () {
259260

260261
exports.types = {
261262
access: [null, 'restricted', 'public'],
263+
'acknowledged-issues': String,
262264
'allow-same-version': Boolean,
263265
'always-auth': Boolean,
264266
also: [null, 'dev', 'development'],

test/tap/audit.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,112 @@ const quickAuditResult = {
8787
}
8888
}
8989

90+
function _runAuditTest (t, params, expectedCode) {
91+
const fixture = new Tacks(new Dir({
92+
'package.json': new File({
93+
name: 'foo',
94+
version: '1.0.0',
95+
dependencies: {
96+
baddep: '1.0.0'
97+
}
98+
})
99+
}))
100+
fixture.create(testDir)
101+
return tmock(t).then(srv => {
102+
srv.filteringRequestBody(req => 'ok')
103+
srv.post('/-/npm/v1/security/audits/quick', 'ok').reply(200, quickAuditResult)
104+
srv.get('/baddep').twice().reply(200, {
105+
name: 'baddep',
106+
'dist-tags': {
107+
'latest': '1.2.3'
108+
},
109+
versions: {
110+
'1.0.0': {
111+
name: 'baddep',
112+
version: '1.0.0',
113+
_hasShrinkwrap: false,
114+
dist: {
115+
shasum: 'deadbeef',
116+
tarball: common.registry + '/idk/-/idk-1.0.0.tgz'
117+
}
118+
},
119+
'1.2.3': {
120+
name: 'baddep',
121+
version: '1.2.3',
122+
_hasShrinkwrap: false,
123+
dist: {
124+
shasum: 'deadbeef',
125+
tarball: common.registry + '/idk/-/idk-1.2.3.tgz'
126+
}
127+
}
128+
}
129+
})
130+
return common.npm([
131+
'install',
132+
'--audit',
133+
'--json',
134+
'--package-lock-only',
135+
'--registry', common.registry,
136+
'--cache', path.join(testDir, 'npm-cache')
137+
], EXEC_OPTS).then(([code, stdout, stderr]) => {
138+
const result = JSON.parse(stdout)
139+
t.same(result.audit, quickAuditResult, 'printed quick audit result')
140+
srv.filteringRequestBody(req => 'ok')
141+
srv.post('/-/npm/v1/security/audits', 'ok').reply(200, {
142+
actions: [{
143+
action: 'update',
144+
module: 'baddep',
145+
target: '1.2.3',
146+
resolves: [{ path: 'baddep' }]
147+
}],
148+
metadata: {
149+
vulnerabilities: {
150+
low: 1
151+
}
152+
},
153+
advisories: {
154+
'1316': {
155+
'id': 1316,
156+
'severity': 'high'
157+
}
158+
}
159+
})
160+
return common.npm([
161+
'audit',
162+
'--json',
163+
'--registry', common.registry,
164+
'--cache', path.join(testDir, 'npm-cache')
165+
].concat(params), EXEC_OPTS).then(([code, stdout, stderr]) => {
166+
t.equal(code, expectedCode, 'exited as expected')
167+
})
168+
})
169+
})
170+
}
171+
172+
test('exits with zero exit code when auditing for vulnerability that is marked as known', t => {
173+
return _runAuditTest(t, ['--acknowledged-issues', '1316'], 0)
174+
})
175+
176+
test('exits with non zero exit code when auditing for vulnerabilities that are not marked as known', t => {
177+
return _runAuditTest(t, ['--acknowledged-issues', '6131'], 1)
178+
})
179+
180+
test('exits with zero exit code when auditing for vulnerabilities that are marked as known', t => {
181+
return _runAuditTest(t, ['--acknowledged-issues', '1316,1234,5311'], 0)
182+
})
183+
184+
test('exits with zero exit code when auditing for vulnerability that is marked as known and is below audit-level', t => {
185+
return _runAuditTest(t, ['--acknowledged-issues', '1316', '--audit-level', 'critical'], 0)
186+
})
187+
188+
test('exits with zero exit code when auditing for vulnerabilities that is marked as known and are above audit-level', t => {
189+
return _runAuditTest(t, ['--acknowledged-issues', '1316', '--audit-level', 'low'], 0)
190+
})
191+
192+
test('exits with zero exit code when issue is above audit-level and it is not acknowledged', t => {
193+
return _runAuditTest(t, ['--acknowledged-issues', '6131', '--audit-level', 'low'], 1)
194+
})
195+
90196
test('exits with zero exit code for vulnerabilities below the `audit-level` flag', t => {
91197
const fixture = new Tacks(new Dir({
92198
'package.json': new File({

0 commit comments

Comments
 (0)