From 257e67ff81cfc7ba5dab0be131b4541dd6d9eaf3 Mon Sep 17 00:00:00 2001 From: Philipp Horstenkamp Date: Sun, 13 Sep 2026 22:01:33 +0200 Subject: [PATCH] feat: show Arch column only when multiple architectures are scanned --- action.yaml | 28 ++----------- cve-diff.js | 118 ++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 100 insertions(+), 46 deletions(-) diff --git a/action.yaml b/action.yaml index b3d59d7..1cfd52e 100644 --- a/action.yaml +++ b/action.yaml @@ -228,34 +228,12 @@ runs: trivy sbom --format json --output "cve-reports/target/cve-${arch}.json" "$f" || true done - # Run multi-arch set-based diff + # Run multi-arch set-based diff and generate consolidated full summary report node "${{ github.action_path }}/cve-diff.js" \ "cve-reports/target" \ "cve-reports/head" \ - "${{ inputs.target-branch }}" > "${{ inputs.output-delta }}" || true - - # Generate full markdown summary report for primary platform - primary_sbom="" - if [ -f sboms/head/sbom-amd64.spdx.json ]; then - primary_sbom="sboms/head/sbom-amd64.spdx.json" - elif [ -f sboms/head/sbom-arm64.spdx.json ]; then - primary_sbom="sboms/head/sbom-arm64.spdx.json" - else - for f in sboms/head/sbom-*.spdx.json; do - if [ -f "$f" ]; then - primary_sbom="$f" - break - fi - done - fi - - if [ -n "$primary_sbom" ]; then - trivy sbom \ - --format template \ - --template "@${{ github.action_path }}/markdown.tpl" \ - --output "${{ inputs.output-summary }}" \ - "$primary_sbom" || true - fi + "${{ inputs.target-branch }}" \ + "${{ inputs.output-summary }}" > "${{ inputs.output-delta }}" || true - name: Publish Reports to Step Summary shell: bash diff --git a/cve-diff.js b/cve-diff.js index f6efb2b..64c702f 100755 --- a/cve-diff.js +++ b/cve-diff.js @@ -103,9 +103,10 @@ function loadReportsForArg(arg) { * Formats a vulnerability record into a Markdown table row. * @param {object} v * @param {Set} [allScannedArchs] + * @param {boolean} [showArch] * @returns {string} */ -function formatVulnRow(v, allScannedArchs = new Set()) { +function formatVulnRow(v, allScannedArchs = new Set(), showArch = true) { const sev = v.Severity || 'UNKNOWN'; const badge = SEVERITY_BADGES[sev] || `⚪ ${sev}`; const vid = v.VulnerabilityID || ''; @@ -113,18 +114,6 @@ function formatVulnRow(v, allScannedArchs = new Set()) { const link = url ? `[${vid}](${url})` : vid; const pkg = v.PkgName || ''; - const archs = v._archs || new Set(['all']); - let archBadge; - if ( - archs.has('all') || - (allScannedArchs && allScannedArchs.size > 1 && archs.size === allScannedArchs.size) || - (archs.has('amd64') && archs.has('arm64')) - ) { - archBadge = '`all`'; - } else { - archBadge = `\`${Array.from(archs).sort().join(', ')}\``; - } - const installed = v.InstalledVersion || '-'; const fixed = v.FixedVersion || '-'; let title = (v.Title || v.Description || '').replace(/\n/g, ' ').trim(); @@ -132,7 +121,22 @@ function formatVulnRow(v, allScannedArchs = new Set()) { title = title.slice(0, 77) + '...'; } - return `| ${badge} | ${link} | \`${pkg}\` | ${archBadge} | ${installed} | ${fixed} | ${title} |`; + if (showArch) { + const archs = v._archs || new Set(['all']); + let archBadge; + if ( + archs.has('all') || + (allScannedArchs && allScannedArchs.size > 1 && archs.size === allScannedArchs.size) || + (archs.has('amd64') && archs.has('arm64')) + ) { + archBadge = '`all`'; + } else { + archBadge = `\`${Array.from(archs).sort().join(', ')}\``; + } + return `| ${badge} | ${link} | \`${pkg}\` | ${archBadge} | ${installed} | ${fixed} | ${title} |`; + } + + return `| ${badge} | ${link} | \`${pkg}\` | ${installed} | ${fixed} | ${title} |`; } /** @@ -188,6 +192,7 @@ function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') { ...(baseCves.scannedArchs || []), ...(branchCves.scannedArchs || []), ]); + const showArch = allScannedArchs.size > 1; const newVulns = newKeys.map((k) => branchCves.get(k)).sort(sortVulns); const resolvedVulns = resolvedKeys.map((k) => baseCves.get(k)).sort(sortVulns); @@ -205,10 +210,15 @@ function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') { ]; if (newVulns.length > 0) { - lines.push('| Severity | CVE | Package | Arch | Installed | Fixed In | Title |'); - lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |'); + if (showArch) { + lines.push('| Severity | CVE | Package | Arch | Installed | Fixed In | Title |'); + lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |'); + } else { + lines.push('| Severity | CVE | Package | Installed | Fixed In | Title |'); + lines.push('| :--- | :--- | :--- | :--- | :--- | :--- |'); + } for (const v of newVulns) { - lines.push(formatVulnRow(v, allScannedArchs)); + lines.push(formatVulnRow(v, allScannedArchs, showArch)); } } else { lines.push('✅ No new vulnerabilities introduced in this build.'); @@ -217,10 +227,15 @@ function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') { lines.push(`#### ✅ Resolved / Removed Vulnerabilities (${resolvedVulns.length})`); if (resolvedVulns.length > 0) { - lines.push('| Severity | CVE | Package | Arch | Previously Installed | Fixed In | Title |'); - lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |'); + if (showArch) { + lines.push('| Severity | CVE | Package | Arch | Previously Installed | Fixed In | Title |'); + lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |'); + } else { + lines.push('| Severity | CVE | Package | Previously Installed | Fixed In | Title |'); + lines.push('| :--- | :--- | :--- | :--- | :--- | :--- |'); + } for (const v of resolvedVulns) { - lines.push(formatVulnRow(v, allScannedArchs)); + lines.push(formatVulnRow(v, allScannedArchs, showArch)); } } else { lines.push('⚪ No existing vulnerabilities were resolved.'); @@ -230,22 +245,82 @@ function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') { return lines.join('\n'); } +/** + * Renders a full markdown summary report of all vulnerabilities in a scan. + * @param {Map} cves + * @returns {string} + */ +function renderFullSummary(cves) { + const allScannedArchs = cves.scannedArchs || new Set(); + const showArch = allScannedArchs.size > 1; + + const lines = [ + '### 🛡️ Security Scan Report', + '', + ]; + + if (cves.size === 0) { + lines.push('✅ No vulnerabilities found.'); + lines.push(''); + return lines.join('\n'); + } + + function sortVulns(a, b) { + const aSev = a.Severity || 'UNKNOWN'; + const bSev = b.Severity || 'UNKNOWN'; + const aIdx = SEVERITY_ORDER.indexOf(aSev) !== -1 ? SEVERITY_ORDER.indexOf(aSev) : 99; + const bIdx = SEVERITY_ORDER.indexOf(bSev) !== -1 ? SEVERITY_ORDER.indexOf(bSev) : 99; + if (aIdx !== bIdx) return aIdx - bIdx; + const aPkg = a.PkgName || ''; + const bPkg = b.PkgName || ''; + return aPkg.localeCompare(bPkg); + } + + const sortedVulns = Array.from(cves.values()).sort(sortVulns); + + lines.push(`| Metric | Count |`); + lines.push(`| :--- | :--- |`); + lines.push(`| Total Vulnerabilities | **${sortedVulns.length}** |`); + lines.push(''); + + if (showArch) { + lines.push('| Severity | CVE | Package | Arch | Installed | Fixed In | Title |'); + lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |'); + } else { + lines.push('| Severity | CVE | Package | Installed | Fixed In | Title |'); + lines.push('| :--- | :--- | :--- | :--- | :--- | :--- |'); + } + + for (const v of sortedVulns) { + lines.push(formatVulnRow(v, allScannedArchs, showArch)); + } + lines.push(''); + + return lines.join('\n'); +} + function main() { const args = process.argv.slice(2); if (args.length < 2) { - console.error('Usage: cve-diff.js [base_name]'); + console.error('Usage: cve-diff.js [base_name] [summary_output_path]'); process.exit(1); } const baseArg = args[0]; const branchArg = args[1]; const baseName = args[2] || 'main'; + const summaryOutputPath = args[3]; const baseCves = loadReportsForArg(baseArg); const branchCves = loadReportsForArg(branchArg); const output = renderMarkdownDiff(baseCves, branchCves, baseName); console.log(output); + + if (summaryOutputPath) { + const summaryOutput = renderFullSummary(branchCves); + fs.writeFileSync(summaryOutputPath, summaryOutput, 'utf8'); + } } if (require.main === module) { @@ -256,4 +331,5 @@ module.exports = { loadReportsForArg, formatVulnRow, renderMarkdownDiff, + renderFullSummary, };