UNPKG

npmc

Version:

a package manager for JavaScript

222 lines (208 loc) 7.46 kB
'use strict' const BB = require('bluebird') const audit = require('./install/audit.js') const fs = require('graceful-fs') const inflateShrinkwrap = BB.promisify(require('./install/inflate-shrinkwrap.js')) const Installer = require('./install.js').Installer const lockVerify = require('lock-verify') const log = require('npmlog') const npa = require('npm-package-arg') const npm = require('./npm.js') const parseJson = require('json-parse-better-errors') const readShrinkwrap = BB.promisify(require('./install/read-shrinkwrap.js')) const validate = require('aproba') const readFile = BB.promisify(fs.readFile) module.exports = auditCmd auditCmd.usage = 'npm audit\n' + 'npm audit fix\n' 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) || [] } loadShrinkwrap (cb) { return BB.try(() => { validate('F', arguments) log.silly('install', 'loadShrinkwrap') return readShrinkwrap(this.idealTree) }).then(() => { if (this.idealTree.package._shrinkwrap) { this._deepArgs.forEach(arg => { arg.reduce((acc, child, i) => { if (i === this._deepArgs.length - 1) { const spec = npa(child) const target = acc.requires.find(n => n.name === spec.name) target.version = spec.fetchSpec delete target.from delete target.resolved delete target.requires delete target.integrity } return acc.requires.find(n => n.name === child) }, this.idealTree) }) return inflateShrinkwrap( this.idealTree, this.idealTree.package._shrinkwrap || {} ) } }).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 auditCmd (args, cb) { if (npm.config.get('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 BB.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) || {}, (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 === 404 || err.statusCode >= 500) { const ne = new Error(`Your configured registry (${npm.config.get('registry')}) does not support audit requests.`) ne.code = 'ENOAUDIT' ne.wrapped = err throw ne } throw err }).then((auditResult) => { if (args[0] === 'fix') { const actions = (auditResult.actions || []).reduce((acc, action) => { if (action.isMajor) { acc.major.add(`${action.module}@${action.target}`) } else if (action.action === 'install') { acc.install.add(`${action.module}@${action.target}`) } else if (action.action === 'update') { const name = action.module const version = action.target action.resolves.forEach(vuln => { const modPath = vuln.path.split('>') acc.update.add( modPath.slice( 0, modPath.indexOf(name) ).concat(`${name}@${version}`).join('>') ) }) } else if (action.action === 'review') { acc.review.add(action) } return acc }, { install: new Set(), update: new Set(), major: new Set(), review: new Set() }) return BB.try(() => { const installMajor = npm.config.get('force') if (actions.major.size) { log.warn('audit', 'some security updates involve breaking changes') if (installMajor) { log.warn('audit', 'and will be upgraded due to `--force` option:') log.warn('audit', ' ', [...actions.major].join('\n')) } else { log.warn('audit', 'and will not be updated automatically.') log.warn('audit', 'To update them yourself, run:') log.warn('audit', '') log.warn('audit', `npm install ${[...actions.major].join(' ')}`) log.warn('audit', '') log.warn('audit', '(`npm audit fix --force` to make it automatic)') } } if (actions.review.size) { log.warn('audit', 'some vulnerabilities require manual review') log.warn('audit', 'run `npm audit` to view the full report') } const installCount = actions.install.size + (installMajor ? actions.major.size : 0) if (actions.update.size || installCount) { log.notice( 'audit', 'installing', installCount + actions.update.size, 'updated packages with patched vulnerabilities' ) return BB.fromNode(cb => { new Auditor( npm.prefix, !!npm.config.get('dry-run'), [...actions.install, ...(installMajor ? actions.major : [])], {deepArgs: [...actions.update].map(u => u.split('>'))} ).run(cb) }) } }) } else { const vulns = auditResult.metadata.vulnerabilities.low + auditResult.metadata.vulnerabilities.moderate + auditResult.metadata.vulnerabilities.high + auditResult.metadata.vulnerabilities.critical if (vulns > 0) process.exitCode = 1 return audit.printFullReport(auditResult) } }).asCallback(cb) }