Skip to content

Instantly share code, notes, and snippets.

@zoonderkins
Forked from KittyGiraudel/npm-audit.js
Last active May 7, 2021 03:08
Show Gist options
  • Save zoonderkins/fec837829b9037a31d1948902c3dc124 to your computer and use it in GitHub Desktop.
Save zoonderkins/fec837829b9037a31d1948902c3dc124 to your computer and use it in GitHub Desktop.
npm-audit
const { exec } = require('child_process')
const { promisify } = require('util')
const chalk = require('chalk')

// See: https://docs.npmjs.com/about-audit-reports#severity
const SEVERITY_LEVELS = ['low', 'moderate', 'high', 'critical']
const SEVERITY_THRESHOLD = 'critical'
const run = promisify(exec)

// Get the output of a command. If the command exits with a non-zero code, try
// to get the output from the error instead.
// Fair warning: written specifically for the `npm audit` command and might not
// work universally.
// @param {String} command - Command to get the output from
// @return {String}
const getOutput = async command => {
  try {
    const output = await run(command, 'inherit')

    return JSON.parse(output.stdout)
  } catch (error) {
    return JSON.parse(error.stdout.toString())
  }
}

// Determine whether the given severity is considered severe or not based on the
// set threshold.
// @param {String} severity
// @return {Boolean}
const isSevere = severity =>
  SEVERITY_LEVELS.indexOf(severity) >=
  SEVERITY_LEVELS.indexOf(SEVERITY_THRESHOLD)

// Determines whether a `resolve` object from the audit is considered important
// by checking if it is a development dependency, and if its severity reaches
// the set severity threshold.
// @param {Object} data - Dependency audit
// @param {Object} resolve - Resolve object
// @return {Boolean}
const isResolveImportant = data => resolve =>
  !resolve.dev && isSevere(data.advisories[resolve.id].severity)

// Determines whether an `action` object from the audit is considered important
// by checking if some of the issues it resolves are important.
// @param {Object} data - Dependency audit
// @param {Object} action - Action object
// @return {Boolean}
const isActionImportant = data => action =>
  action.resolves.some(isResolveImportant(data))

// Determines whether the script should exit with an error based on whether some
// suggested actions can resolve important issues.
// @param {Object} data - Dependency audit
// @return {Boolean}
const shouldAbort = data => data.actions.some(isActionImportant(data))

// Sorts actions based on their severity level
// @param {Object} data - Dependency audit
// @param {Object} a - Action object
// @param {Object} b - Action object
// @return {Boolean}
const sortBySeverity = data => (a, b) =>
  SEVERITY_LEVELS.indexOf(data.advisories[a.resolves[0].id].severity) -
  SEVERITY_LEVELS.indexOf(data.advisories[b.resolves[0].id].severity)

// Gets some information and suggestion for each action based on the issues they
// resolve.
// @param {Object} data - Dependency audit
// @return {String[]}
const getSuggestions = data =>
  data.actions.sort(sortBySeverity(data)).map(getSuggestion(data))

// Finds the depth of the vulnerability to be able to provide it as `--depth` to
// `npm update`.
// @param {Object[]} findings - Advisory findings
// @return {Number}
const findDepth = findings =>
  Math.max(
    ...findings.map(finding =>
      Math.max(...finding.paths.map(path => path.split('>').length))
    )
  )

// Gets some information and suggestion for the given action based on the issues
// it resolves.
// @param {Object} data - Dependency audit
// @param {Object} action - Action object
// @return {String}
const getSuggestion = data => action => {
  const advisory = data.advisories[action.resolves[0].id]
  const module = chalk.bold.red(advisory.module_name)
  const version = advisory.vulnerable_versions
  const type = advisory.title
  const severity = advisory.severity + ' severity'
  const explanation = chalk.dim(advisory.overview.replace(/\n+$/, ''))
  const url = chalk.dim(advisory.url)
  const recommendation = advisory.recommendation
  const depth = findDepth(advisory.findings)
  const command = chalk.green(
    action.action === 'install'
      ? `npm install ${action.module}@${action.target}`
      : `npm update ${action.module} --depth ${depth}`
  )

  return `${module} (${version})

${type} (${severity})
${explanation}
See: ${url}

${recommendation}
${command}`
}

;(async () => {
  try {
    const output = await getOutput(`npm audit --json`)
    const suggestions = getSuggestions(output)
    const count = suggestions.length

    if (count) {
      const word = count > 1 ? 'vulnerabilities' : 'vulnerability'
      console.log(`⚠️  ${count} ${word} found:\n`)
      suggestions.forEach(suggestion => console.log(suggestion, '\n'))
    }

    process.exit(Number(shouldAbort(output)))
  } catch (error) {
    console.error('Could not run dependency audit', error)
  }
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment