• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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