automate-improved-yarn-audit
Version:
A utility for running automate improved yarn audit.
229 lines (228 loc) • 10.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.YarnAuditCheck = void 0;
const node_child_process_1 = require("node:child_process");
const cli_table_1 = __importDefault(require("cli-table"));
class YarnAuditCheck {
AUDIT_ADVISORY = "auditAdvisory";
AUDIT_SUMMARY = "auditSummary";
NO_PATCH_PATCHED_VERSION = "<0.0.0";
NO_PATCH_AVAILABLE = "No patch available";
SEVERITY_LEVELS = [
"critical",
"high",
"moderate",
"low",
"info"
];
// If you want to add default packages
excludePackageList = [];
constructor(excludePackageList) {
this.excludePackageList =
excludePackageList.length === 0
? this.excludePackageList
: excludePackageList;
}
showTable(tableRows) {
const table = new cli_table_1.default({
style: { head: ["reset"] },
colAligns: ["left", "left", "left"],
head: []
});
tableRows.forEach((row) => table.push(row));
console.log(table.toString());
}
formatStatus = (status) => {
// Check if status is either true, false, or warn
switch (status) {
case true:
return "\u001B[32m" /* Colors.FG_GREEN */ + "PASS" + "\u001B[0m" /* Colors.RESET_COLOR */;
case false:
return "\u001B[31m" /* Colors.FG_RED */ + "FAIL" + "\u001B[0m" /* Colors.RESET_COLOR */;
case "warn":
return "\u001B[33m" /* Colors.FG_YELLOW */ + "WARNING" + "\u001B[0m" /* Colors.RESET_COLOR */;
default:
return String(status);
}
};
formatSeverity(severity) {
let formattedSeverity;
switch (severity) {
case "critical":
case "high":
formattedSeverity = `${"\u001B[31m" /* Colors.FG_RED */}${severity}${"\u001B[0m" /* Colors.RESET_COLOR */}`;
break;
case "moderate":
formattedSeverity = `${"\u001B[33m" /* Colors.FG_YELLOW */}${severity}${"\u001B[0m" /* Colors.RESET_COLOR */}`;
break;
default:
formattedSeverity = severity;
break;
}
return formattedSeverity;
}
// Parse the yarn audit output and exclude packages
excludeAndDisplayPackageInfo(auditOutput) {
// filter only auditAdvisories and sort based on severity levels
const auditAdvisories = auditOutput
.filter((i) => i.type === this.AUDIT_ADVISORY)
.sort((a, b) => this.SEVERITY_LEVELS.indexOf(a.data.advisory.severity) -
this.SEVERITY_LEVELS.indexOf(b.data.advisory.severity));
const finalAuditPackages = auditAdvisories
.filter((advisory) => !this.excludePackageList.includes(advisory.data.advisory.module_name))
.map((advisory) => {
const patchedIn = advisory.data.advisory.patched_versions.replace(" ", "") ===
this.NO_PATCH_PATCHED_VERSION
? this.NO_PATCH_AVAILABLE
: advisory.data.advisory.patched_versions;
const auditPackage = {
Package: advisory.data.advisory.module_name,
Severity: advisory.data.advisory.severity,
Title: advisory.data.advisory.title,
"Patched In": patchedIn,
"Dependency of": `${advisory.data.resolution.path.split(">")[0]} ${advisory.data.resolution.dev ? "[dev]" : ""}`,
Path: advisory.data.resolution.path.split(">").join(" > "),
"More info": `https://www.npmjs.com/advisories/${advisory.data.resolution.id}`
};
// Display table for packages with vulnerabilities
const tableRows = Object.entries(auditPackage).map(([key, value]) => ({
[key]: key === "Severity" ? this.formatSeverity(value) : value
}));
this.showTable(tableRows);
return auditPackage;
});
const vulnerablePackages = finalAuditPackages.filter((i) => this.SEVERITY_LEVELS.slice(0, 2).includes(i.Severity));
const pass = vulnerablePackages.length === 0;
if (this.excludePackageList.length) {
// eslint-disable-next-line no-console
console.info(`\n ${this.excludePackageList.length} - Package excluded:`);
const excludePackagesList = this.excludePackageList.map((value) => ({
[value]: `${"\u001B[32m" /* Colors.FG_GREEN */} EXCLUDED ${"\u001B[0m" /* Colors.RESET_COLOR */}`
}));
this.showTable(excludePackagesList);
}
this.printAuditSummary(auditOutput, finalAuditPackages);
return pass;
}
printAuditSummary(auditOutput, finalAuditPackages) {
// audit summary
const auditSummary = auditOutput.filter((i) => i.type === this.AUDIT_SUMMARY);
const stringifyAuditSummary = JSON.stringify(auditSummary[0].data);
const auditSummaryObject = JSON.parse(stringifyAuditSummary);
const severity = this.SEVERITY_LEVELS.reduce((acc, level) => ({ ...acc, [level]: 0 }), {});
const severityInfo = this.SEVERITY_LEVELS.map((level) => ({
severity: level,
label: `${level.charAt(0)}${level.slice(1)}`
}));
// eslint-disable-next-line no-console
console.info(`\n${finalAuditPackages.length} vulnerabilities found - Packages audited: ${auditSummaryObject.totalDependencies}`);
if (finalAuditPackages.length) {
const severityCounts = finalAuditPackages.reduce((acc, item) => {
const severity = item.Severity;
acc[severity]++;
return acc;
}, severity);
const displayedCounts = severityInfo
.map(({ severity, label }) => severityCounts[severity] > 0
? `${severityCounts[severity]} ${label}`
: null)
.filter((severity) => severity !== null)
.join(" | ");
// eslint-disable-next-line no-console
console.info(`Severity: ${displayedCounts}`);
}
}
parsingAndFilteringOutput(auditOutput) {
// Split the output into lines and parse each line as JSON
const auditLines = auditOutput.trim().split("\n");
return auditLines.map((line) => JSON.parse(line));
}
runYarnAudit() {
return new Promise((resolve, reject) => {
const yarnAuditProcess = (0, node_child_process_1.spawn)("yarn", ["audit", "--json"]);
let auditOutput = "";
yarnAuditProcess.stdout.on("data", (data) => {
auditOutput += data.toString();
});
yarnAuditProcess.stderr.on("data", (data) => {
const stringifyError = data.toString();
if (stringifyError) {
const trimAndSplittedArray = stringifyError.trim().split("\n");
const auditResult = [];
trimAndSplittedArray.forEach((line) => {
try {
auditResult.push(JSON.parse(line));
}
catch (error) {
/* eslint-disable no-console */
console.error("Error parsing JSON:", error.message);
console.error("Raw output:", line);
/* eslint-enable no-console */
}
});
auditResult.forEach((result) => {
// eslint-disable-next-line no-console
console.info(`${"\u001B[33m" /* Colors.FG_YELLOW */}${result.type}${"\u001B[0m" /* Colors.RESET_COLOR */} ${result.data}`);
});
}
});
yarnAuditProcess.on("close", (code) => {
// Code '0' indicates there are no vulnerabilities found in project's dependencies
if (code === 0) {
const auditResult = this.parsingAndFilteringOutput(auditOutput);
this.printAuditSummary(auditResult, []);
resolve(true);
}
else if (code === 12) {
// Code '12' indicates that there are vulnerabilities found in project's dependencies
try {
const auditResult = this.parsingAndFilteringOutput(auditOutput);
const response = this.excludeAndDisplayPackageInfo(auditResult);
resolve(response);
}
catch (parseError) {
// eslint-disable-next-line no-console
console.error("Error parsing JSON:", parseError.message);
reject(new Error(`Failed to parse JSON. See raw output above.`));
}
}
else {
// eslint-disable-next-line no-console
console.error(auditOutput);
reject(new Error(`Yarn audit process exited with code ${code}. See output above.`));
}
});
});
}
}
exports.YarnAuditCheck = YarnAuditCheck;
// Check if command line arguments are provided
if (process.argv.length <= 2) {
console.log("Usage: yarn run automate-improved-yarn-audit <project-path> <arg1> <arg2> ...");
process.exit(1); // Exit the script with error code 1
}
// Extract command line arguments (excluding the first two elements which are 'node' and the script file name)
const excludePackages = process.argv.slice(3);
const rootPath = process.argv.slice(2, 3)[0];
process.chdir(rootPath);
// Instantiate and run the class
const yarnAuditCheck = new YarnAuditCheck(excludePackages);
yarnAuditCheck
.runYarnAudit()
.then((status) => {
yarnAuditCheck.showTable([
{ "Yarn Audit Check Status": yarnAuditCheck.formatStatus(status) }
]);
if (!status) {
process.exit(1);
}
process.exit(0);
})
.catch((error) => {
console.error(`ERROR: ${error.message}`);
process.exit(1);
});