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