Skip to content

Commit 792c8c7

Browse files
lennymzkat
authored andcommittedAug 3, 2018
audit: configurable audit level for non-zero exit (#31)
`npm audit` currently exits with exit code 1 if any vulnerabilities are found of any level. Add a flag of `--audit-level` to `npm audit` to allow it to pass if only vulnerabilities below a certain level are found. Example: `npm audit --audit-level=high` will exit with 0 if only low or moderate level vulns are detected. Fixes: https://npm.community/t/245 PR-URL: #31 Credit: @lennym Reviewed-By: @zkat
1 parent 32e6947 commit 792c8c7

File tree

4 files changed

+283
-5
lines changed

4 files changed

+283
-5
lines changed
 

‎doc/misc/npm-config.md

+8
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,14 @@ When "true" submit audit reports alongside `npm install` runs to the default
164164
registry and all registries configured for scopes. See the documentation
165165
for npm-audit(1) for details on what is submitted.
166166

167+
### audit-level
168+
169+
* Default: `"low"`
170+
* Type: `'low'`, `'moderate'`, `'high'`, `'critical'`
171+
172+
The minimum level of vulnerability for `npm audit` to exit with
173+
a non-zero exit code.
174+
167175
### auth-type
168176

169177
* Default: `'legacy'`

‎lib/audit.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -257,11 +257,11 @@ function auditCmd (args, cb) {
257257
})
258258
})
259259
} else {
260-
const vulns =
261-
auditResult.metadata.vulnerabilities.low +
262-
auditResult.metadata.vulnerabilities.moderate +
263-
auditResult.metadata.vulnerabilities.high +
264-
auditResult.metadata.vulnerabilities.critical
260+
const levels = ['low', 'moderate', 'high', 'critical']
261+
const minLevel = levels.indexOf(npm.config.get('audit-level'))
262+
const vulns = levels.reduce((count, level, i) => {
263+
return i < minLevel ? count : count + (auditResult.metadata.vulnerabilities[level] || 0)
264+
}, 0)
265265
if (vulns > 0) process.exitCode = 1
266266
if (npm.config.get('parseable')) {
267267
return audit.printParseableReport(auditResult)

‎lib/config/defaults.js

+2
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Object.defineProperty(exports, 'defaults', {get: function () {
110110
'always-auth': false,
111111
also: null,
112112
audit: true,
113+
'audit-level': 'low',
113114
'auth-type': 'legacy',
114115

115116
'bin-links': true,
@@ -257,6 +258,7 @@ exports.types = {
257258
'always-auth': Boolean,
258259
also: [null, 'dev', 'development'],
259260
audit: Boolean,
261+
'audit-level': ['low', 'moderate', 'high', 'critical'],
260262
'auth-type': ['legacy', 'sso', 'saml', 'oauth'],
261263
'bin-links': Boolean,
262264
browser: [null, String],

‎test/tap/audit.js

+268
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
'use strict'
2+
3+
const BB = require('bluebird')
4+
5+
const common = BB.promisifyAll(require('../common-tap.js'))
6+
const mr = BB.promisify(require('npm-registry-mock'))
7+
const path = require('path')
8+
const rimraf = BB.promisify(require('rimraf'))
9+
const Tacks = require('tacks')
10+
const tap = require('tap')
11+
const test = tap.test
12+
13+
const Dir = Tacks.Dir
14+
const File = Tacks.File
15+
const testDir = path.join(__dirname, path.basename(__filename, '.js'))
16+
17+
const EXEC_OPTS = { cwd: testDir }
18+
19+
tap.tearDown(function () {
20+
process.chdir(__dirname)
21+
try {
22+
rimraf.sync(testDir)
23+
} catch (e) {
24+
if (process.platform !== 'win32') {
25+
throw e
26+
}
27+
}
28+
})
29+
30+
function tmock (t) {
31+
return mr({port: common.port}).then(s => {
32+
t.tearDown(function () {
33+
s.done()
34+
s.close()
35+
rimraf.sync(testDir)
36+
})
37+
return s
38+
})
39+
}
40+
41+
test('exits with zero exit code for vulnerabilities below the `audit-level` flag', t => {
42+
const fixture = new Tacks(new Dir({
43+
'package.json': new File({
44+
name: 'foo',
45+
version: '1.0.0',
46+
dependencies: {
47+
baddep: '1.0.0'
48+
}
49+
})
50+
}))
51+
fixture.create(testDir)
52+
return tmock(t).then(srv => {
53+
srv.filteringRequestBody(req => 'ok')
54+
srv.post('/-/npm/v1/security/audits/quick', 'ok').reply(200, 'yeah')
55+
srv.get('/baddep').twice().reply(200, {
56+
name: 'baddep',
57+
'dist-tags': {
58+
'latest': '1.2.3'
59+
},
60+
versions: {
61+
'1.0.0': {
62+
name: 'baddep',
63+
version: '1.0.0',
64+
_hasShrinkwrap: false,
65+
dist: {
66+
shasum: 'deadbeef',
67+
tarball: common.registry + '/idk/-/idk-1.0.0.tgz'
68+
}
69+
},
70+
'1.2.3': {
71+
name: 'baddep',
72+
version: '1.2.3',
73+
_hasShrinkwrap: false,
74+
dist: {
75+
shasum: 'deadbeef',
76+
tarball: common.registry + '/idk/-/idk-1.2.3.tgz'
77+
}
78+
}
79+
}
80+
})
81+
return common.npm([
82+
'install',
83+
'--audit',
84+
'--json',
85+
'--package-lock-only',
86+
'--registry', common.registry,
87+
'--cache', path.join(testDir, 'npm-cache')
88+
], EXEC_OPTS).then(([code, stdout, stderr]) => {
89+
srv.filteringRequestBody(req => 'ok')
90+
srv.post('/-/npm/v1/security/audits', 'ok').reply(200, {
91+
actions: [{
92+
action: 'update',
93+
module: 'baddep',
94+
target: '1.2.3',
95+
resolves: [{path: 'baddep'}]
96+
}],
97+
metadata: {
98+
vulnerabilities: {
99+
low: 1
100+
}
101+
}
102+
})
103+
return common.npm([
104+
'audit',
105+
'--audit-level', 'high',
106+
'--json',
107+
'--registry', common.registry,
108+
'--cache', path.join(testDir, 'npm-cache')
109+
], EXEC_OPTS).then(([code, stdout, stderr]) => {
110+
t.equal(code, 0, 'exited OK')
111+
})
112+
})
113+
})
114+
})
115+
116+
test('exits with non-zero exit code for vulnerabilities at the `audit-level` flag', t => {
117+
const fixture = new Tacks(new Dir({
118+
'package.json': new File({
119+
name: 'foo',
120+
version: '1.0.0',
121+
dependencies: {
122+
baddep: '1.0.0'
123+
}
124+
})
125+
}))
126+
fixture.create(testDir)
127+
return tmock(t).then(srv => {
128+
srv.filteringRequestBody(req => 'ok')
129+
srv.post('/-/npm/v1/security/audits/quick', 'ok').reply(200, 'yeah')
130+
srv.get('/baddep').twice().reply(200, {
131+
name: 'baddep',
132+
'dist-tags': {
133+
'latest': '1.2.3'
134+
},
135+
versions: {
136+
'1.0.0': {
137+
name: 'baddep',
138+
version: '1.0.0',
139+
_hasShrinkwrap: false,
140+
dist: {
141+
shasum: 'deadbeef',
142+
tarball: common.registry + '/idk/-/idk-1.0.0.tgz'
143+
}
144+
},
145+
'1.2.3': {
146+
name: 'baddep',
147+
version: '1.2.3',
148+
_hasShrinkwrap: false,
149+
dist: {
150+
shasum: 'deadbeef',
151+
tarball: common.registry + '/idk/-/idk-1.2.3.tgz'
152+
}
153+
}
154+
}
155+
})
156+
return common.npm([
157+
'install',
158+
'--audit',
159+
'--json',
160+
'--package-lock-only',
161+
'--registry', common.registry,
162+
'--cache', path.join(testDir, 'npm-cache')
163+
], EXEC_OPTS).then(([code, stdout, stderr]) => {
164+
srv.filteringRequestBody(req => 'ok')
165+
srv.post('/-/npm/v1/security/audits', 'ok').reply(200, {
166+
actions: [{
167+
action: 'update',
168+
module: 'baddep',
169+
target: '1.2.3',
170+
resolves: [{path: 'baddep'}]
171+
}],
172+
metadata: {
173+
vulnerabilities: {
174+
high: 1
175+
}
176+
}
177+
})
178+
return common.npm([
179+
'audit',
180+
'--audit-level', 'high',
181+
'--json',
182+
'--registry', common.registry,
183+
'--cache', path.join(testDir, 'npm-cache')
184+
], EXEC_OPTS).then(([code, stdout, stderr]) => {
185+
t.equal(code, 1, 'exited OK')
186+
})
187+
})
188+
})
189+
})
190+
191+
test('exits with non-zero exit code for vulnerabilities at the `audit-level` flag', t => {
192+
const fixture = new Tacks(new Dir({
193+
'package.json': new File({
194+
name: 'foo',
195+
version: '1.0.0',
196+
dependencies: {
197+
baddep: '1.0.0'
198+
}
199+
})
200+
}))
201+
fixture.create(testDir)
202+
return tmock(t).then(srv => {
203+
srv.filteringRequestBody(req => 'ok')
204+
srv.post('/-/npm/v1/security/audits/quick', 'ok').reply(200, 'yeah')
205+
srv.get('/baddep').twice().reply(200, {
206+
name: 'baddep',
207+
'dist-tags': {
208+
'latest': '1.2.3'
209+
},
210+
versions: {
211+
'1.0.0': {
212+
name: 'baddep',
213+
version: '1.0.0',
214+
_hasShrinkwrap: false,
215+
dist: {
216+
shasum: 'deadbeef',
217+
tarball: common.registry + '/idk/-/idk-1.0.0.tgz'
218+
}
219+
},
220+
'1.2.3': {
221+
name: 'baddep',
222+
version: '1.2.3',
223+
_hasShrinkwrap: false,
224+
dist: {
225+
shasum: 'deadbeef',
226+
tarball: common.registry + '/idk/-/idk-1.2.3.tgz'
227+
}
228+
}
229+
}
230+
})
231+
return common.npm([
232+
'install',
233+
'--audit',
234+
'--json',
235+
'--package-lock-only',
236+
'--registry', common.registry,
237+
'--cache', path.join(testDir, 'npm-cache')
238+
], EXEC_OPTS).then(([code, stdout, stderr]) => {
239+
srv.filteringRequestBody(req => 'ok')
240+
srv.post('/-/npm/v1/security/audits', 'ok').reply(200, {
241+
actions: [{
242+
action: 'update',
243+
module: 'baddep',
244+
target: '1.2.3',
245+
resolves: [{path: 'baddep'}]
246+
}],
247+
metadata: {
248+
vulnerabilities: {
249+
high: 1
250+
}
251+
}
252+
})
253+
return common.npm([
254+
'audit',
255+
'--audit-level', 'moderate',
256+
'--json',
257+
'--registry', common.registry,
258+
'--cache', path.join(testDir, 'npm-cache')
259+
], EXEC_OPTS).then(([code, stdout, stderr]) => {
260+
t.equal(code, 1, 'exited OK')
261+
})
262+
})
263+
})
264+
})
265+
266+
test('cleanup', t => {
267+
return rimraf(testDir)
268+
})

0 commit comments

Comments
 (0)
Please sign in to comment.