Last active
February 18, 2022 08:44
-
-
Save KittyGiraudel/37438267cb36448a85d56b8501d91aab to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
Love your blog post which led me here https://hugogiraudel.com/2020/11/19/managing-npm-dependencies
Thanks for writing that up and sharing your code - hopefully all this useful tooling could even find it's way upstream, e.g. directly into
npm audit
and same for other scripts you wrote to check for new versions🚀