#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
const args = process.argv.slice(2);
const inputIndex = args.indexOf('--input');
const inputProdIndex = args.indexOf('--input-prod');
const inputAllIndex = args.indexOf('--input-all');
const labelIndex = args.indexOf('--label');
const outputIndex = args.indexOf('--output');
if ((inputIndex === -1 && inputProdIndex === -1 && inputAllIndex === -1) || labelIndex === -1 || outputIndex === -1) {
console.error(
'Usage: generate-cve-report.mjs --input <audit.json> --label <version> --output <report.md>\n' +
' or: generate-cve-report.mjs --input-prod <audit.json> --input-all <audit.json> --label <version> --output <report.md>',
);
process.exit(1);
}
const inputPath = inputIndex === -1 ? null : args[inputIndex + 1];
const inputProdPath = inputProdIndex === -1 ? null : args[inputProdIndex + 1];
const inputAllPath = inputAllIndex === -1 ? null : args[inputAllIndex + 1];
const outputPath = args[outputIndex + 1];
const versionLabel = args[labelIndex + 1];
if (!outputPath || !versionLabel) {
console.error('Missing required arguments.');
process.exit(1);
}
if ((inputProdPath && !inputAllPath) || (!inputProdPath && inputAllPath)) {
console.error('Missing required arguments.');
process.exit(1);
}
const severityRank = {
critical: 0,
high: 1,
info: 2,
low: 3,
moderate: 4,
unknown: 5,
};
const severityOrder = Object.keys(severityRank);
const addAdvisory = (list, advisory) => {
const key = `${advisory.module}|${advisory.title}|${advisory.url ?? ''}|${advisory.severity}`;
if (!list.has(key)) {
list.set(key, advisory);
}
};
const parseAuditJson = (content) => {
try {
return JSON.parse(content);
} catch (error) {
const parsedLines = content
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => JSON.parse(line));
return parsedLines.find((entry) => entry.advisories || entry.vulnerabilities) ?? parsedLines.at(-1);
}
};
const parseAuditFile = (auditPath) => {
if (!auditPath) {
return null;
}
const auditRaw = fs.readFileSync(auditPath, 'utf8');
return parseAuditJson(auditRaw);
};
const collectAdvisories = (auditData) => {
const advisories = new Map();
if (auditData?.advisories) {
for (const advisory of Object.values(auditData.advisories)) {
addAdvisory(advisories, {
cves: advisory.cves ?? [],
module: advisory.module_name ?? advisory.moduleName ?? 'unknown',
severity: advisory.severity ?? 'unknown',
title: advisory.title ?? 'Advisory',
url: advisory.url ?? advisory.more_info ?? null,
});
}
}
if (auditData?.vulnerabilities) {
for (const vulnerability of Object.values(auditData.vulnerabilities)) {
for (const viaEntry of vulnerability.via ?? []) {
if (typeof viaEntry === 'string') {
addAdvisory(advisories, {
cves: [],
module: vulnerability.name ?? 'unknown',
severity: vulnerability.severity ?? 'unknown',
title: viaEntry,
url: null,
});
continue;
}
addAdvisory(advisories, {
cves: viaEntry.cves ?? [],
module: vulnerability.name ?? 'unknown',
severity: viaEntry.severity ?? vulnerability.severity ?? 'unknown',
title: viaEntry.title ?? 'Advisory',
url: viaEntry.url ?? null,
});
}
}
}
return Array.from(advisories.values()).map((advisory) => {
const normalizedSeverity = advisory.severity ?? 'unknown';
return {
...advisory,
severity: severityRank[normalizedSeverity] === undefined ? 'unknown' : normalizedSeverity,
};
});
};
const buildReportSection = (auditData, headingPrefix, emptyMessage) => {
const advisoryList = collectAdvisories(auditData);
const summaryCounts = severityOrder.reduce((accumulator, severity) => {
accumulator[severity] = 0;
return accumulator;
}, {});
for (const advisory of advisoryList) {
if (summaryCounts[advisory.severity] === undefined) {
summaryCounts.unknown += 1;
} else {
summaryCounts[advisory.severity] += 1;
}
}
advisoryList.sort((left, right) => {
const severityDiff = severityRank[left.severity] - severityRank[right.severity];
if (severityDiff !== 0) {
return severityDiff;
}
return left.module.localeCompare(right.module) || left.title.localeCompare(right.title);
});
const lines = [];
lines.push(`${headingPrefix} Zusammenfassung`);
lines.push('');
lines.push('| Severity | Count |');
lines.push('| --- | ---: |');
for (const severity of severityOrder) {
lines.push(`| ${severity} | ${summaryCounts[severity]} |`);
}
lines.push('');
lines.push(`${headingPrefix} Details`);
lines.push('');
if (advisoryList.length === 0) {
lines.push(emptyMessage);
lines.push('');
} else {
lines.push('| Package | Severity | CVE | Title | URL |');
lines.push('| --- | --- | --- | --- | --- |');
for (const advisory of advisoryList) {
const cveText = advisory.cves?.length ? advisory.cves.join(', ') : '–';
const urlText = advisory.url ? `[Link](${advisory.url})` : '–';
lines.push(`| ${advisory.module} | ${advisory.severity} | ${cveText} | ${advisory.title} | ${urlText} |`);
}
lines.push('');
}
return lines;
};
const auditData = parseAuditFile(inputPath);
const auditProdData = parseAuditFile(inputProdPath);
const auditAllData = parseAuditFile(inputAllPath);
const lines = [];
lines.push(`# CVE Übersicht (${versionLabel})`);
lines.push('');
lines.push(`Stand: ${new Date().toISOString().slice(0, 10)}`);
lines.push('');
if (auditProdData && auditAllData) {
lines.push('## Produktion (pnpm audit --production)');
lines.push('');
lines.push(
...buildReportSection(
auditProdData,
'###',
'Keine Sicherheitslücken laut `pnpm audit --json --production` gefunden.',
),
);
lines.push('');
lines.push('## Alle Abhängigkeiten (pnpm audit)');
lines.push('');
lines.push(
...buildReportSection(
auditAllData,
'###',
'Keine Sicherheitslücken laut `pnpm audit --json` gefunden.',
),
);
} else {
lines.push(...buildReportSection(auditData, '##', 'Keine Sicherheitslücken laut `pnpm audit --json` gefunden.'));
}
const outputDir = path.dirname(outputPath);
fs.mkdirSync(outputDir, { recursive: true });
fs.writeFileSync(outputPath, `${lines.join('\n')}\n`, 'utf8');