@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
JavaScript
;
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;