UNPKG

10 kBJavaScriptView Raw
1'use strict'
2
3const Bluebird = require('bluebird')
4
5const audit = require('./install/audit.js')
6const fs = require('graceful-fs')
7const Installer = require('./install.js').Installer
8const lockVerify = require('lock-verify')
9const log = require('npmlog')
10const npa = require('npm-package-arg')
11const npm = require('./npm.js')
12const output = require('./utils/output.js')
13const parseJson = require('json-parse-better-errors')
14
15const readFile = Bluebird.promisify(fs.readFile)
16
17module.exports = auditCmd
18
19const usage = require('./utils/usage')
20auditCmd.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
27auditCmd.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
38class 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 // We might not always be able to find `target` through the given
53 // path. If we can't we'll just ignore it.
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 // This kills `hasModernMeta()` and forces a re-fetch
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 // no top level lifecycles on audit
91 runPreinstallTopLevelLifecycles (cb) { cb() }
92 runPostinstallTopLevelLifecycles (cb) { cb() }
93}
94
95function 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
113function 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
127function 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}