UNPKG

@chax-at/better-npm-audit

Version:

Reshape into a better npm audit for the community and encourage more people to include security audit into their process.

239 lines (238 loc) 11.5 kB
"use strict"; var __spreadArray = (this && this.__spreadArray) || function (to, from) { for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) to[j] = from[i]; return to; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.processExceptions = exports.getExceptionsIds = exports.processAuditJson = exports.mapLevelToNumber = void 0; var lodash_get_1 = __importDefault(require("lodash.get")); var common_1 = require("./common"); var color_1 = require("./color"); var print_1 = require("./print"); var date_1 = require("./date"); var MAX_PATHS_SIZE = 5; /** * Converts an audit level to a numeric value * @param {String} auditLevel Audit level * @return {Number} Numeric level: the higher the number, the more severe it is */ function mapLevelToNumber(auditLevel) { switch (auditLevel) { case 'info': return 0; case 'low': return 1; case 'moderate': return 2; case 'high': return 3; case 'critical': return 4; default: return 0; } } exports.mapLevelToNumber = mapLevelToNumber; /** * Analyze the JSON string buffer * @param {String} jsonBuffer NPM Audit JSON string buffer * @param {String} auditLevel User's target audit level * @param {Array} exceptionIds User's exception IDs * @param {Array} modulesToIgnore Users modules to ignore * @return {Object} Processed vulnerabilities details */ function processAuditJson(jsonBuffer, auditLevel, exceptionIds, modulesToIgnore) { if (jsonBuffer === void 0) { jsonBuffer = ''; } if (auditLevel === void 0) { auditLevel = 'info'; } if (exceptionIds === void 0) { exceptionIds = []; } if (modulesToIgnore === void 0) { modulesToIgnore = []; } if (!common_1.isJsonString(jsonBuffer)) { return { unhandledIds: [], vulnerabilityIds: [], vulnerabilityUrls: [], vulnerabilityGhsa: [], vulnerabilityModules: [], report: [], failed: true, }; } // NPM v6 uses `advisories` // NPM v7 uses `vulnerabilities` // Refer to the `test/__mocks__` folder for some sample mockups var _a = JSON.parse(jsonBuffer), advisories = _a.advisories, vulnerabilities = _a.vulnerabilities; // NPM v6 handling if (advisories) { return Object.values(advisories).reduce(function (acc, cur) { var shouldAudit = mapLevelToNumber(cur.severity) >= mapLevelToNumber(auditLevel); var isIgnoredModule = modulesToIgnore.includes(cur.module_name); var url = cur.url; // We can only access the GHSA via the URL here var ghsaMatch = url ? url.match(/(GHSA-.*)/) : null; var ghsa = ghsaMatch != null && ghsaMatch.length >= 2 ? ghsaMatch[1] : ''; var isExcepted = exceptionIds.includes(cur.id.toString()) || exceptionIds.includes(url) || exceptionIds.includes(ghsa); // Record this vulnerability into the report, and highlight it using yellow color if it's new acc.report.push([ color_1.color(cur.id, isExcepted || isIgnoredModule ? '' : 'yellow'), color_1.color(cur.module_name, isExcepted || isIgnoredModule ? '' : 'yellow'), color_1.color(cur.title, isExcepted || isIgnoredModule ? '' : 'yellow'), color_1.color(common_1.trimArray(cur.findings.reduce(function (a, c) { return __spreadArray(__spreadArray([], a), c.paths); }, []), MAX_PATHS_SIZE).join('\n'), isExcepted || isIgnoredModule ? '' : 'yellow'), color_1.color(cur.severity, isExcepted || isIgnoredModule ? '' : 'yellow', color_1.getSeverityBgColor(cur.severity)), color_1.color(cur.url, isExcepted || isIgnoredModule ? '' : 'yellow'), isExcepted || isIgnoredModule ? 'y' : color_1.color('n', 'yellow'), ]); acc.vulnerabilityIds.push(Number(cur.id)); acc.vulnerabilityUrls.push(url); acc.vulnerabilityGhsa.push(ghsa); if (!acc.vulnerabilityModules.includes(cur.module_name)) { acc.vulnerabilityModules.push(cur.module_name); } // Found unhandled vulnerabilities if (shouldAudit && !isExcepted && !isIgnoredModule) { acc.unhandledIds.push(Number(cur.id)); } return acc; }, { unhandledIds: [], vulnerabilityIds: [], vulnerabilityUrls: [], vulnerabilityGhsa: [], vulnerabilityModules: [], report: [], }); } // NPM v7 handling if (vulnerabilities) { return Object.values(vulnerabilities).reduce(function (acc, cur) { // Inside `via` array, its either the related module name or the vulnerability source object. lodash_get_1.default(cur, 'via', []).forEach(function (vul) { // The vulnerability ID is labeled as `source` var originalId = lodash_get_1.default(vul, 'source', ''); var moduleName = lodash_get_1.default(vul, 'name', ''); var url = lodash_get_1.default(vul, 'url', ''); // We can only access the GHSA via the URL here var ghsaMatch = url.match(/(GHSA-.*)/); var ghsa = ghsaMatch != null && ghsaMatch.length >= 2 ? ghsaMatch[1] : ''; // Let's skip if ID is a string (module name), and only focus on the root vulnerabilities if (!originalId || typeof originalId === 'string' || typeof vul === 'string') { return; } var id = originalId.toString(); var shouldAudit = mapLevelToNumber(vul.severity) >= mapLevelToNumber(auditLevel); var isExcepted = exceptionIds.includes(id) || exceptionIds.includes(url) || exceptionIds.includes(ghsa); var isIgnoredModule = modulesToIgnore.includes(moduleName); // Record this vulnerability into the report, and highlight it using yellow color if it's new acc.report.push([ color_1.color(String(id), isExcepted || isIgnoredModule ? '' : 'yellow'), color_1.color(vul.name, isExcepted || isIgnoredModule ? '' : 'yellow'), color_1.color(vul.title, isExcepted || isIgnoredModule ? '' : 'yellow'), color_1.color(common_1.trimArray(lodash_get_1.default(cur, 'nodes', []).map(common_1.shortenNodePath), MAX_PATHS_SIZE).join('\n'), isExcepted || isIgnoredModule ? '' : 'yellow'), color_1.color(vul.severity, isExcepted || isIgnoredModule ? '' : 'yellow', color_1.getSeverityBgColor(vul.severity)), color_1.color(vul.url, isExcepted || isIgnoredModule ? '' : 'yellow'), isExcepted || isIgnoredModule ? 'y' : color_1.color('n', 'yellow'), ]); acc.vulnerabilityIds.push(originalId); acc.vulnerabilityUrls.push(url); acc.vulnerabilityGhsa.push(ghsa); if (!acc.vulnerabilityModules.includes(moduleName)) { acc.vulnerabilityModules.push(moduleName); } // Found unhandled vulnerabilities if (shouldAudit && !isExcepted && !isIgnoredModule) { acc.unhandledIds.push(originalId); } }); return acc; }, { unhandledIds: [], vulnerabilityIds: [], vulnerabilityUrls: [], vulnerabilityGhsa: [], vulnerabilityModules: [], report: [], }); } return { unhandledIds: [], vulnerabilityIds: [], vulnerabilityUrls: [], vulnerabilityGhsa: [], vulnerabilityModules: [], report: [], failed: true, }; } exports.processAuditJson = processAuditJson; /** * Process all exceptions and return a list of exception IDs * @param {Object | Boolean} nsprc File content from `.nsprc` * @param {Array} cmdExceptions Exceptions passed in via command line * @return {Array} List of found vulnerabilities */ function getExceptionsIds(nsprc, cmdExceptions) { if (cmdExceptions === void 0) { cmdExceptions = []; } // If file does not exists if (!nsprc || typeof nsprc !== 'object') { // If there are exceptions passed in from command line if (cmdExceptions.length) { // Display simple info console.info("Exception IDs: " + cmdExceptions.join(', ')); return cmdExceptions; } return []; } // Process the content of the file along with the command line exceptions var _a = processExceptions(nsprc, cmdExceptions), exceptionIds = _a.exceptionIds, report = _a.report; print_1.printExceptionReport(report); return exceptionIds; } exports.getExceptionsIds = getExceptionsIds; /** * Filter the given list in the `.nsprc` file for valid exceptions * @param {Object} nsprc The nsprc file content, contains exception info * @param {Array} cmdExceptions Exceptions passed in via command line * @return {Object} Processed vulnerabilities details */ function processExceptions(nsprc, cmdExceptions) { if (cmdExceptions === void 0) { cmdExceptions = []; } return Object.entries(nsprc).reduce(function (acc, _a) { var id = _a[0], details = _a[1]; var isValidId = !isNaN(+id) || id.toString().startsWith('GHSA-') || id.toString().startsWith('http'); var isActive = Boolean(lodash_get_1.default(details, 'active', true)); // default to true var notes = typeof details === 'string' ? details : lodash_get_1.default(details, 'notes', ''); var _b = date_1.analyzeExpiry(lodash_get_1.default(details, 'expiry')), valid = _b.valid, expired = _b.expired, years = _b.years; // Color the status accordingly var status = color_1.color('active', 'green'); if (expired) { status = color_1.color('expired', 'red'); } else if (!isValidId || !valid) { status = color_1.color('invalid', 'red'); } else if (!isActive) { status = color_1.color('inactive', 'yellow'); } // Color the date accordingly var expiryDate = lodash_get_1.default(details, 'expiry') ? new Date(lodash_get_1.default(details, 'expiry')).toUTCString() : ''; // If it was expired for more than 5 years ago, warn by coloring the date in red if (years && years <= -5) { expiryDate = color_1.color(expiryDate, 'red'); } else if (years && years <= -1) { expiryDate = color_1.color(expiryDate, 'yellow'); } acc.report.push([id, status, expiryDate, notes]); if (isValidId && isActive && !expired) { acc.exceptionIds.push(id); } return acc; }, { exceptionIds: cmdExceptions, report: cmdExceptions.map(function (id) { return [String(id), color_1.color('active', 'green'), '', '']; }), }); } exports.processExceptions = processExceptions;