123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- 'use strict'
- const Bluebird = require('bluebird')
- const audit = require('./install/audit.js')
- const figgyPudding = require('figgy-pudding')
- const fs = require('graceful-fs')
- const Installer = require('./install.js').Installer
- const lockVerify = require('lock-verify')
- const log = require('npmlog')
- const npa = require('libnpm/parse-arg')
- const npm = require('./npm.js')
- const npmConfig = require('./config/figgy-config.js')
- const output = require('./utils/output.js')
- const parseJson = require('json-parse-better-errors')
- const readFile = Bluebird.promisify(fs.readFile)
- const AuditConfig = figgyPudding({
- also: {},
- 'audit-level': {},
- deepArgs: 'deep-args',
- 'deep-args': {},
- dev: {},
- force: {},
- 'dry-run': {},
- global: {},
- json: {},
- only: {},
- parseable: {},
- prod: {},
- production: {},
- registry: {},
- runId: {}
- })
- module.exports = auditCmd
- const usage = require('./utils/usage')
- auditCmd.usage = usage(
- 'audit',
- '\nnpm audit [--json] [--production]' +
- '\nnpm audit fix ' +
- '[--force|--package-lock-only|--dry-run|--production|--only=(dev|prod)]'
- )
- auditCmd.completion = function (opts, cb) {
- const argv = opts.conf.argv.remain
- switch (argv[2]) {
- case 'audit':
- return cb(null, [])
- default:
- return cb(new Error(argv[2] + ' not recognized'))
- }
- }
- class Auditor extends Installer {
- constructor (where, dryrun, args, opts) {
- super(where, dryrun, args, opts)
- this.deepArgs = (opts && opts.deepArgs) || []
- this.runId = opts.runId || ''
- this.audit = false
- }
- loadAllDepsIntoIdealTree (cb) {
- Bluebird.fromNode(cb => super.loadAllDepsIntoIdealTree(cb)).then(() => {
- if (this.deepArgs && this.deepArgs.length) {
- this.deepArgs.forEach(arg => {
- arg.reduce((acc, child, ii) => {
- if (!acc) {
- // We might not always be able to find `target` through the given
- // path. If we can't we'll just ignore it.
- return
- }
- const spec = npa(child)
- const target = (
- acc.requires.find(n => n.package.name === spec.name) ||
- acc.requires.find(
- n => audit.scrub(n.package.name, this.runId) === spec.name
- )
- )
- if (target && ii === arg.length - 1) {
- target.loaded = false
- // This kills `hasModernMeta()` and forces a re-fetch
- target.package = {
- name: spec.name,
- version: spec.fetchSpec,
- _requested: target.package._requested
- }
- delete target.fakeChild
- let parent = target.parent
- while (parent) {
- parent.loaded = false
- parent = parent.parent
- }
- target.requiredBy.forEach(par => {
- par.loaded = false
- delete par.fakeChild
- })
- }
- return target
- }, this.idealTree)
- })
- return Bluebird.fromNode(cb => super.loadAllDepsIntoIdealTree(cb))
- }
- }).nodeify(cb)
- }
- // no top level lifecycles on audit
- runPreinstallTopLevelLifecycles (cb) { cb() }
- runPostinstallTopLevelLifecycles (cb) { cb() }
- }
- function maybeReadFile (name) {
- const file = `${npm.prefix}/${name}`
- return readFile(file)
- .then((data) => {
- try {
- return parseJson(data)
- } catch (ex) {
- ex.code = 'EJSONPARSE'
- throw ex
- }
- })
- .catch({code: 'ENOENT'}, () => null)
- .catch((ex) => {
- ex.file = file
- throw ex
- })
- }
- function filterEnv (action, opts) {
- const includeDev = opts.dev ||
- (!/^prod(uction)?$/.test(opts.only) && !opts.production) ||
- /^dev(elopment)?$/.test(opts.only) ||
- /^dev(elopment)?$/.test(opts.also)
- const includeProd = !/^dev(elopment)?$/.test(opts.only)
- const resolves = action.resolves.filter(({dev}) => {
- return (dev && includeDev) || (!dev && includeProd)
- })
- if (resolves.length) {
- return Object.assign({}, action, {resolves})
- }
- }
- function auditCmd (args, cb) {
- const opts = AuditConfig(npmConfig())
- if (opts.global) {
- const err = new Error('`npm audit` does not support testing globals')
- err.code = 'EAUDITGLOBAL'
- throw err
- }
- if (args.length && args[0] !== 'fix') {
- return cb(new Error('Invalid audit subcommand: `' + args[0] + '`\n\nUsage:\n' + auditCmd.usage))
- }
- return Bluebird.all([
- maybeReadFile('npm-shrinkwrap.json'),
- maybeReadFile('package-lock.json'),
- maybeReadFile('package.json')
- ]).spread((shrinkwrap, lockfile, pkgJson) => {
- const sw = shrinkwrap || lockfile
- if (!pkgJson) {
- const err = new Error('No package.json found: Cannot audit a project without a package.json')
- err.code = 'EAUDITNOPJSON'
- throw err
- }
- if (!sw) {
- const err = new Error('Neither npm-shrinkwrap.json nor package-lock.json found: Cannot audit a project without a lockfile')
- err.code = 'EAUDITNOLOCK'
- throw err
- } else if (shrinkwrap && lockfile) {
- log.warn('audit', 'Both npm-shrinkwrap.json and package-lock.json exist, using npm-shrinkwrap.json.')
- }
- const requires = Object.assign(
- {},
- (pkgJson && pkgJson.dependencies) || {},
- (!opts.production && pkgJson && pkgJson.devDependencies) || {}
- )
- return lockVerify(npm.prefix).then((result) => {
- if (result.status) return audit.generate(sw, requires)
- const lockFile = shrinkwrap ? 'npm-shrinkwrap.json' : 'package-lock.json'
- const err = new Error(`Errors were found in your ${lockFile}, run npm install to fix them.\n ` +
- result.errors.join('\n '))
- err.code = 'ELOCKVERIFY'
- throw err
- })
- }).then((auditReport) => {
- return audit.submitForFullReport(auditReport)
- }).catch((err) => {
- if (err.statusCode >= 400) {
- let msg
- if (err.statusCode === 401) {
- msg = `Either your login credentials are invalid or your registry (${opts.registry}) does not support audit.`
- } else if (err.statusCode === 404) {
- msg = `Your configured registry (${opts.registry}) does not support audit requests.`
- } else {
- msg = `Your configured registry (${opts.registry}) may not support audit requests, or the audit endpoint may be temporarily unavailable.`
- }
- if (err.body.length) {
- msg += '\nThe server said: ' + err.body
- }
- const ne = new Error(msg)
- ne.code = 'ENOAUDIT'
- ne.wrapped = err
- throw ne
- }
- throw err
- }).then((auditResult) => {
- if (args[0] === 'fix') {
- const actions = (auditResult.actions || []).reduce((acc, action) => {
- action = filterEnv(action, opts)
- if (!action) { return acc }
- if (action.isMajor) {
- acc.major.add(`${action.module}@${action.target}`)
- action.resolves.forEach(({id, path}) => acc.majorFixes.add(`${id}::${path}`))
- } else if (action.action === 'install') {
- acc.install.add(`${action.module}@${action.target}`)
- action.resolves.forEach(({id, path}) => acc.installFixes.add(`${id}::${path}`))
- } else if (action.action === 'update') {
- const name = action.module
- const version = action.target
- action.resolves.forEach(vuln => {
- acc.updateFixes.add(`${vuln.id}::${vuln.path}`)
- const modPath = vuln.path.split('>')
- const newPath = modPath.slice(
- 0, modPath.indexOf(name)
- ).concat(`${name}@${version}`)
- if (newPath.length === 1) {
- acc.install.add(newPath[0])
- } else {
- acc.update.add(newPath.join('>'))
- }
- })
- } else if (action.action === 'review') {
- action.resolves.forEach(({id, path}) => acc.review.add(`${id}::${path}`))
- }
- return acc
- }, {
- install: new Set(),
- installFixes: new Set(),
- update: new Set(),
- updateFixes: new Set(),
- major: new Set(),
- majorFixes: new Set(),
- review: new Set()
- })
- return Bluebird.try(() => {
- const installMajor = opts.force
- const installCount = actions.install.size + (installMajor ? actions.major.size : 0) + actions.update.size
- const vulnFixCount = new Set([...actions.installFixes, ...actions.updateFixes, ...(installMajor ? actions.majorFixes : [])]).size
- const metavuln = auditResult.metadata.vulnerabilities
- const total = Object.keys(metavuln).reduce((acc, key) => acc + metavuln[key], 0)
- if (installCount) {
- log.verbose(
- 'audit',
- 'installing',
- [...actions.install, ...(installMajor ? actions.major : []), ...actions.update]
- )
- }
- return Bluebird.fromNode(cb => {
- new Auditor(
- npm.prefix,
- !!opts['dry-run'],
- [...actions.install, ...(installMajor ? actions.major : [])],
- opts.concat({
- runId: auditResult.runId,
- deepArgs: [...actions.update].map(u => u.split('>'))
- }).toJSON()
- ).run(cb)
- }).then(() => {
- const numScanned = auditResult.metadata.totalDependencies
- if (!opts.json && !opts.parseable) {
- output(`fixed ${vulnFixCount} of ${total} vulnerabilit${total === 1 ? 'y' : 'ies'} in ${numScanned} scanned package${numScanned === 1 ? '' : 's'}`)
- if (actions.review.size) {
- output(` ${actions.review.size} vulnerabilit${actions.review.size === 1 ? 'y' : 'ies'} required manual review and could not be updated`)
- }
- if (actions.major.size) {
- 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`)
- if (installMajor) {
- output(' (installed due to `--force` option)')
- } else {
- output(' (use `npm audit fix --force` to install breaking changes;' +
- ' or refer to `npm audit` for steps to fix these manually)')
- }
- }
- }
- })
- })
- } else {
- const levels = ['low', 'moderate', 'high', 'critical']
- const minLevel = levels.indexOf(opts['audit-level'])
- const vulns = levels.reduce((count, level, i) => {
- return i < minLevel ? count : count + (auditResult.metadata.vulnerabilities[level] || 0)
- }, 0)
- if (vulns > 0) process.exitCode = 1
- if (opts.parseable) {
- return audit.printParseableReport(auditResult)
- } else {
- return audit.printFullReport(auditResult)
- }
- }
- }).asCallback(cb)
- }
|