1 | 'use strict'
|
2 |
|
3 | const Bluebird = require('bluebird')
|
4 |
|
5 | const audit = require('./install/audit.js')
|
6 | const fs = require('graceful-fs')
|
7 | const Installer = require('./install.js').Installer
|
8 | const lockVerify = require('lock-verify')
|
9 | const log = require('npmlog')
|
10 | const npa = require('npm-package-arg')
|
11 | const npm = require('./npm.js')
|
12 | const output = require('./utils/output.js')
|
13 | const parseJson = require('json-parse-better-errors')
|
14 |
|
15 | const readFile = Bluebird.promisify(fs.readFile)
|
16 |
|
17 | module.exports = auditCmd
|
18 |
|
19 | const usage = require('./utils/usage')
|
20 | auditCmd.usage = usage(
|
21 | 'audit',
|
22 | '\nnpm audit [--json]' +
|
23 | '\nnpm audit fix ' +
|
24 | '[--force|--package-lock-only|--dry-run|--production|--only=(dev|prod)]'
|
25 | )
|
26 |
|
27 | auditCmd.completion = function (opts, cb) {
|
28 | const argv = opts.conf.argv.remain
|
29 |
|
30 | switch (argv[2]) {
|
31 | case 'audit':
|
32 | return cb(null, [])
|
33 | default:
|
34 | return cb(new Error(argv[2] + ' not recognized'))
|
35 | }
|
36 | }
|
37 |
|
38 | class Auditor extends Installer {
|
39 | constructor (where, dryrun, args, opts) {
|
40 | super(where, dryrun, args, opts)
|
41 | this.deepArgs = (opts && opts.deepArgs) || []
|
42 | this.runId = opts.runId || ''
|
43 | this.audit = false
|
44 | }
|
45 |
|
46 | loadAllDepsIntoIdealTree (cb) {
|
47 | Bluebird.fromNode(cb => super.loadAllDepsIntoIdealTree(cb)).then(() => {
|
48 | if (this.deepArgs && this.deepArgs.length) {
|
49 | this.deepArgs.forEach(arg => {
|
50 | arg.reduce((acc, child, ii) => {
|
51 | if (!acc) {
|
52 |
|
53 |
|
54 | return
|
55 | }
|
56 | const spec = npa(child)
|
57 | const target = (
|
58 | acc.requires.find(n => n.package.name === spec.name) ||
|
59 | acc.requires.find(
|
60 | n => audit.scrub(n.package.name, this.runId) === spec.name
|
61 | )
|
62 | )
|
63 | if (target && ii === arg.length - 1) {
|
64 | target.loaded = false
|
65 |
|
66 | target.package = {
|
67 | name: spec.name,
|
68 | version: spec.fetchSpec,
|
69 | _requested: target.package._requested
|
70 | }
|
71 | delete target.fakeChild
|
72 | let parent = target.parent
|
73 | while (parent) {
|
74 | parent.loaded = false
|
75 | parent = parent.parent
|
76 | }
|
77 | target.requiredBy.forEach(par => {
|
78 | par.loaded = false
|
79 | delete par.fakeChild
|
80 | })
|
81 | }
|
82 | return target
|
83 | }, this.idealTree)
|
84 | })
|
85 | return Bluebird.fromNode(cb => super.loadAllDepsIntoIdealTree(cb))
|
86 | }
|
87 | }).nodeify(cb)
|
88 | }
|
89 |
|
90 |
|
91 | runPreinstallTopLevelLifecycles (cb) { cb() }
|
92 | runPostinstallTopLevelLifecycles (cb) { cb() }
|
93 | }
|
94 |
|
95 | function maybeReadFile (name) {
|
96 | const file = `${npm.prefix}/${name}`
|
97 | return readFile(file)
|
98 | .then((data) => {
|
99 | try {
|
100 | return parseJson(data)
|
101 | } catch (ex) {
|
102 | ex.code = 'EJSONPARSE'
|
103 | throw ex
|
104 | }
|
105 | })
|
106 | .catch({code: 'ENOENT'}, () => null)
|
107 | .catch((ex) => {
|
108 | ex.file = file
|
109 | throw ex
|
110 | })
|
111 | }
|
112 |
|
113 | function filterEnv (action) {
|
114 | const includeDev = npm.config.get('dev') ||
|
115 | (!/^prod(uction)?$/.test(npm.config.get('only')) && !npm.config.get('production')) ||
|
116 | /^dev(elopment)?$/.test(npm.config.get('only')) ||
|
117 | /^dev(elopment)?$/.test(npm.config.get('also'))
|
118 | const includeProd = !/^dev(elopment)?$/.test(npm.config.get('only'))
|
119 | const resolves = action.resolves.filter(({dev}) => {
|
120 | return (dev && includeDev) || (!dev && includeProd)
|
121 | })
|
122 | if (resolves.length) {
|
123 | return Object.assign({}, action, {resolves})
|
124 | }
|
125 | }
|
126 |
|
127 | function auditCmd (args, cb) {
|
128 | if (npm.config.get('global')) {
|
129 | const err = new Error('`npm audit` does not support testing globals')
|
130 | err.code = 'EAUDITGLOBAL'
|
131 | throw err
|
132 | }
|
133 | if (args.length && args[0] !== 'fix') {
|
134 | return cb(new Error('Invalid audit subcommand: `' + args[0] + '`\n\nUsage:\n' + auditCmd.usage))
|
135 | }
|
136 | return Bluebird.all([
|
137 | maybeReadFile('npm-shrinkwrap.json'),
|
138 | maybeReadFile('package-lock.json'),
|
139 | maybeReadFile('package.json')
|
140 | ]).spread((shrinkwrap, lockfile, pkgJson) => {
|
141 | const sw = shrinkwrap || lockfile
|
142 | if (!pkgJson) {
|
143 | const err = new Error('No package.json found: Cannot audit a project without a package.json')
|
144 | err.code = 'EAUDITNOPJSON'
|
145 | throw err
|
146 | }
|
147 | if (!sw) {
|
148 | const err = new Error('Neither npm-shrinkwrap.json nor package-lock.json found: Cannot audit a project without a lockfile')
|
149 | err.code = 'EAUDITNOLOCK'
|
150 | throw err
|
151 | } else if (shrinkwrap && lockfile) {
|
152 | log.warn('audit', 'Both npm-shrinkwrap.json and package-lock.json exist, using npm-shrinkwrap.json.')
|
153 | }
|
154 | const requires = Object.assign(
|
155 | {},
|
156 | (pkgJson && pkgJson.dependencies) || {},
|
157 | (pkgJson && pkgJson.devDependencies) || {}
|
158 | )
|
159 | return lockVerify(npm.prefix).then((result) => {
|
160 | if (result.status) return audit.generate(sw, requires)
|
161 |
|
162 | const lockFile = shrinkwrap ? 'npm-shrinkwrap.json' : 'package-lock.json'
|
163 | const err = new Error(`Errors were found in your ${lockFile}, run npm install to fix them.\n ` +
|
164 | result.errors.join('\n '))
|
165 | err.code = 'ELOCKVERIFY'
|
166 | throw err
|
167 | })
|
168 | }).then((auditReport) => {
|
169 | return audit.submitForFullReport(auditReport)
|
170 | }).catch((err) => {
|
171 | if (err.statusCode === 404 || err.statusCode >= 500) {
|
172 | const ne = new Error(`Your configured registry (${npm.config.get('registry')}) does not support audit requests.`)
|
173 | ne.code = 'ENOAUDIT'
|
174 | ne.wrapped = err
|
175 | throw ne
|
176 | }
|
177 | throw err
|
178 | }).then((auditResult) => {
|
179 | if (args[0] === 'fix') {
|
180 | const actions = (auditResult.actions || []).reduce((acc, action) => {
|
181 | action = filterEnv(action)
|
182 | if (!action) { return acc }
|
183 | if (action.isMajor) {
|
184 | acc.major.add(`${action.module}@${action.target}`)
|
185 | action.resolves.forEach(({id, path}) => acc.majorFixes.add(`${id}::${path}`))
|
186 | } else if (action.action === 'install') {
|
187 | acc.install.add(`${action.module}@${action.target}`)
|
188 | action.resolves.forEach(({id, path}) => acc.installFixes.add(`${id}::${path}`))
|
189 | } else if (action.action === 'update') {
|
190 | const name = action.module
|
191 | const version = action.target
|
192 | action.resolves.forEach(vuln => {
|
193 | acc.updateFixes.add(`${vuln.id}::${vuln.path}`)
|
194 | const modPath = vuln.path.split('>')
|
195 | const newPath = modPath.slice(
|
196 | 0, modPath.indexOf(name)
|
197 | ).concat(`${name}@${version}`)
|
198 | if (newPath.length === 1) {
|
199 | acc.install.add(newPath[0])
|
200 | } else {
|
201 | acc.update.add(newPath.join('>'))
|
202 | }
|
203 | })
|
204 | } else if (action.action === 'review') {
|
205 | action.resolves.forEach(({id, path}) => acc.review.add(`${id}::${path}`))
|
206 | }
|
207 | return acc
|
208 | }, {
|
209 | install: new Set(),
|
210 | installFixes: new Set(),
|
211 | update: new Set(),
|
212 | updateFixes: new Set(),
|
213 | major: new Set(),
|
214 | majorFixes: new Set(),
|
215 | review: new Set()
|
216 | })
|
217 | return Bluebird.try(() => {
|
218 | const installMajor = npm.config.get('force')
|
219 | const installCount = actions.install.size + (installMajor ? actions.major.size : 0) + actions.update.size
|
220 | const vulnFixCount = new Set([...actions.installFixes, ...actions.updateFixes, ...(installMajor ? actions.majorFixes : [])]).size
|
221 | const metavuln = auditResult.metadata.vulnerabilities
|
222 | const total = Object.keys(metavuln).reduce((acc, key) => acc + metavuln[key], 0)
|
223 | if (installCount) {
|
224 | log.verbose(
|
225 | 'audit',
|
226 | 'installing',
|
227 | [...actions.install, ...(installMajor ? actions.major : []), ...actions.update]
|
228 | )
|
229 | }
|
230 | return Bluebird.fromNode(cb => {
|
231 | new Auditor(
|
232 | npm.prefix,
|
233 | !!npm.config.get('dry-run'),
|
234 | [...actions.install, ...(installMajor ? actions.major : [])],
|
235 | {
|
236 | runId: auditResult.runId,
|
237 | deepArgs: [...actions.update].map(u => u.split('>'))
|
238 | }
|
239 | ).run(cb)
|
240 | }).then(() => {
|
241 | const numScanned = auditResult.metadata.totalDependencies
|
242 | if (!npm.config.get('json') && !npm.config.get('parseable')) {
|
243 | output(`fixed ${vulnFixCount} of ${total} vulnerabilit${total === 1 ? 'y' : 'ies'} in ${numScanned} scanned package${numScanned === 1 ? '' : 's'}`)
|
244 | if (actions.review.size) {
|
245 | output(` ${actions.review.size} vulnerabilit${actions.review.size === 1 ? 'y' : 'ies'} required manual review and could not be updated`)
|
246 | }
|
247 | if (actions.major.size) {
|
248 | output(` ${actions.major.size} package update${actions.major.size === 1 ? '' : 's'} for ${actions.majorFixes.size} vuln${actions.majorFixes.size === 1 ? '' : 's'} involved breaking changes`)
|
249 | if (installMajor) {
|
250 | output(' (installed due to `--force` option)')
|
251 | } else {
|
252 | output(' (use `npm audit fix --force` to install breaking changes;' +
|
253 | ' or refer to `npm audit` for steps to fix these manually)')
|
254 | }
|
255 | }
|
256 | }
|
257 | })
|
258 | })
|
259 | } else {
|
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)
|
265 | if (vulns > 0) process.exitCode = 1
|
266 | if (npm.config.get('parseable')) {
|
267 | return audit.printParseableReport(auditResult)
|
268 | } else {
|
269 | return audit.printFullReport(auditResult)
|
270 | }
|
271 | }
|
272 | }).asCallback(cb)
|
273 | }
|