#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
const args = process.argv.slice(2);
// Parse both prod and all audit arguments
const auditArgs = {};
const auditProdArgs = {};
['v1', 'v2', 'v3', 'v4'].forEach((version) => {
const argName = `--audit-${version}`;
const argNameProd = `--audit-prod-${version}`;
const index = args.indexOf(argName);
const indexProd = args.indexOf(argNameProd);
if (index !== -1 && args[index + 1]) {
auditArgs[version] = args[index + 1];
}
if (indexProd !== -1 && args[indexProd + 1]) {
auditProdArgs[version] = args[indexProd + 1];
}
});
const outputIndex = args.indexOf('--output');
const outputPath = outputIndex === -1 ? null : args[outputIndex + 1];
if (!outputPath || Object.keys(auditArgs).length === 0) {
console.error(
'Usage: merge-cve-overview.mjs --output <doc/cve-overview.md> --audit-prod-v1 <audit.json> --audit-v1 <audit.json> --audit-prod-v2 <audit.json> --audit-v2 <audit.json> ...',
);
process.exit(1);
}
const severityOrder = ['critical', 'high', 'moderate', 'low', 'info', 'unknown'];
const versionOrder = ['v4', 'v3', 'v2', 'v1'];
const parseAuditJson = (auditPath) => {
const content = fs.readFileSync(auditPath, 'utf8');
const audit = JSON.parse(content);
// Handle both old format (vulnerabilities) and new format (advisories)
const vulnerabilities = audit.vulnerabilities || audit.advisories || {};
const severityCounts = {};
for (const vuln of Object.values(vulnerabilities)) {
const severity = vuln.severity || 'unknown';
severityCounts[severity] = (severityCounts[severity] || 0) + 1;
}
return {
summary: severityCounts,
vulnerabilities: vulnerabilities,
};
};
const auditsByVersion = {};
const auditsProdByVersion = {};
for (const [version, auditPath] of Object.entries(auditArgs)) {
try {
const audit = parseAuditJson(auditPath);
auditsByVersion[version] = audit;
} catch (error) {
console.error(`Failed to parse audit file for ${version}:`, error.message);
}
}
for (const [version, auditPath] of Object.entries(auditProdArgs)) {
try {
const audit = parseAuditJson(auditPath);
auditsProdByVersion[version] = audit;
} catch (error) {
console.error(`Failed to parse prod audit file for ${version}:`, error.message);
}
}
const lines = ['# CVE Overview', '', '> For more security information, see [SECURITY.md](./SECURITY.md)', ''];
// Helper function to build vulnerability table
const buildVulnTable = (auditsByVer) => {
const tableLines = [];
tableLines.push('| Package | Severity | CVE | Affected Versions | Description |');
tableLines.push('| --- | --- | --- | --- | --- |');
// Collect vulnerabilities
const vulnDetails = [];
for (const [version, audit] of Object.entries(auditsByVer)) {
for (const [_, advisory] of Object.entries(audit.vulnerabilities)) {
vulnDetails.push({
packageName: advisory.module_name || 'unknown',
severity: advisory.severity || 'unknown',
cve: (advisory.cves && advisory.cves[0]) || advisory.github_advisory_id || 'N/A',
title: advisory.title || '',
version: version,
});
}
}
// Group by package + cve (unique)
const vulnMap = new Map();
for (const detail of vulnDetails) {
const key = `${detail.packageName}:::${detail.cve}`;
if (!vulnMap.has(key)) {
vulnMap.set(key, {
packageName: detail.packageName,
severity: detail.severity,
cve: detail.cve,
title: detail.title,
versions: new Set(),
});
}
vulnMap.get(key).versions.add(detail.version);
}
// Sort vulnerabilities by severity (descending) and package name
const sortedVulns = Array.from(vulnMap.values()).sort((a, b) => {
const severityA = severityOrder.indexOf(a.severity);
const severityB = severityOrder.indexOf(b.severity);
if (severityA !== severityB) {
return severityA - severityB;
}
return a.packageName.localeCompare(b.packageName);
});
if (sortedVulns.length === 0) {
tableLines.push('| – | – | – | – | No vulnerabilities found. |');
} else {
for (const vuln of sortedVulns) {
const versions = Array.from(vuln.versions).sort((a, b) => versionOrder.indexOf(a) - versionOrder.indexOf(b));
const title = vuln.title.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').substring(0, 80);
tableLines.push(`| ${vuln.packageName} | ${vuln.severity} | ${vuln.cve} | ${versions.join(', ')} | ${title} |`);
}
}
tableLines.push('');
return tableLines;
};
// 1. Production Dependencies
lines.push('## 1. Production Dependencies');
lines.push('');
lines.push('### Summary');
lines.push('');
lines.push(`| Severity | ${versionOrder.join(' | ')} |`);
lines.push(`| --- | ${versionOrder.map(() => '---:').join(' | ')} |`);
for (const severity of severityOrder) {
const rowCounts = versionOrder.map((version) => {
const audit = auditsProdByVersion[version];
return audit ? (audit.summary[severity] ?? 0) : '-';
});
lines.push(`| ${severity} | ${rowCounts.join(' | ')} |`);
}
lines.push('');
lines.push('### Vulnerabilities');
lines.push('');
lines.push(...buildVulnTable(auditsProdByVersion));
// 2. All Dependencies
lines.push('## 2. All Dependencies');
lines.push('');
lines.push('### Summary');
lines.push('');
lines.push(`| Severity | ${versionOrder.join(' | ')} |`);
lines.push(`| --- | ${versionOrder.map(() => '---:').join(' | ')} |`);
for (const severity of severityOrder) {
const rowCounts = versionOrder.map((version) => {
const audit = auditsByVersion[version];
return audit ? (audit.summary[severity] ?? 0) : '-';
});
lines.push(`| ${severity} | ${rowCounts.join(' | ')} |`);
}
lines.push('');
lines.push('### Vulnerabilities');
lines.push('');
lines.push(...buildVulnTable(auditsByVersion));
const outputDir = path.dirname(outputPath);
fs.mkdirSync(outputDir, { recursive: true });
fs.writeFileSync(outputPath, `${lines.join('\n')}\n`, 'utf8');
console.log(`✅ CVE overview generated: ${outputPath}`);