const test = require('node:test'); const assert = require('node:assert/strict'); const path = require('node:path'); const { loadReportsForArg, formatVulnRow, renderMarkdownDiff, renderFullSummary, } = require('../cve-diff.js'); const FIXTURES_DIR = path.join(__dirname, 'fixtures'); test('loadReportsForArg loads valid reports and extracts multi-arch metadata', () => { const targetDir = path.join(FIXTURES_DIR, 'target'); const cves = loadReportsForArg(targetDir); assert.equal(cves.size, 2); assert.ok(cves.has('CVE-2023-0001:::glibc')); assert.ok(cves.has('CVE-2023-0002:::openssl')); const glibc = cves.get('CVE-2023-0001:::glibc'); assert.equal(glibc.Severity, 'CRITICAL'); assert.deepEqual(Array.from(glibc._archs).sort(), ['amd64', 'arm64']); assert.deepEqual(Array.from(cves.scannedArchs).sort(), ['amd64', 'arm64']); const openssl = cves.get('CVE-2023-0002:::openssl'); assert.equal(openssl.Severity, 'HIGH'); assert.deepEqual(Array.from(openssl._archs), ['amd64']); }); test('loadReportsForArg handles reports with missing or null Results array without crashing', () => { const emptyDir = path.join(FIXTURES_DIR, 'empty'); const cves = loadReportsForArg(emptyDir); assert.equal(cves.size, 0); }); test('renderMarkdownDiff produces correct introduced, resolved, and unchanged sets', () => { const targetDir = path.join(FIXTURES_DIR, 'target'); const headDir = path.join(FIXTURES_DIR, 'head'); const baseCves = loadReportsForArg(targetDir); const branchCves = loadReportsForArg(headDir); const diff = renderMarkdownDiff(baseCves, branchCves, 'main'); // Introduced: CVE-2023-0003 (curl) assert.ok(diff.includes('New Vulnerabilities Introduced (1)')); assert.ok(diff.includes('CVE-2023-0003')); assert.ok(diff.includes('`curl`')); // Resolved: CVE-2023-0001 (glibc) assert.ok(diff.includes('Resolved / Removed Vulnerabilities (1)')); assert.ok(diff.includes('CVE-2023-0001')); assert.ok(diff.includes('`glibc`')); // Unchanged count assert.ok(diff.includes('| ⚪ **Unchanged Vulnerabilities** | 1 |')); // Multi-arch is present in fixtures (amd64, arm64), so Arch column should be shown assert.ok(diff.includes('| Arch |')); }); test('renderMarkdownDiff hides Arch column when single architecture is scanned', () => { const targetSingle = path.join(FIXTURES_DIR, 'target', 'cve-amd64.json'); const headSingle = path.join(FIXTURES_DIR, 'head', 'cve-amd64.json'); const baseCves = loadReportsForArg(targetSingle); const branchCves = loadReportsForArg(headSingle); const diff = renderMarkdownDiff(baseCves, branchCves, 'main'); assert.ok(!diff.includes('| Arch |')); assert.ok(diff.includes('New Vulnerabilities Introduced (1)')); assert.ok(diff.includes('Resolved / Removed Vulnerabilities (1)')); }); test('renderFullSummary outputs summary table and lists all vulnerabilities', () => { const headDir = path.join(FIXTURES_DIR, 'head'); const headCves = loadReportsForArg(headDir); const summary = renderFullSummary(headCves); assert.ok(summary.includes('### 🛡️ Security Scan Report')); assert.ok(summary.includes('| Total Vulnerabilities | **2** |')); assert.ok(summary.includes('CVE-2023-0002')); assert.ok(summary.includes('CVE-2023-0003')); }); test('formatVulnRow properly formats badge, architecture tag, and escape pipes', () => { const vuln = { Severity: 'CRITICAL', VulnerabilityID: 'CVE-2023-0001', PrimaryURL: 'https://avd.aquasec.com/nvd/cve-2023-0001', PkgName: 'glibc', InstalledVersion: '2.31', FixedVersion: '2.32', Title: 'A vulnerability with | pipe symbol', _archs: new Set(['amd64']), }; const allArchs = new Set(['amd64', 'arm64']); const rowWithArch = formatVulnRow(vuln, allArchs, true); assert.ok(rowWithArch.includes('🔴 CRITICAL')); assert.ok(rowWithArch.includes('`amd64`')); assert.ok(rowWithArch.includes('\\|')); // Pipe escaped const rowWithoutArch = formatVulnRow(vuln, allArchs, false); assert.ok(!rowWithoutArch.includes('`amd64`')); });