audit.js 11 KB

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