From e394966a967603c22493e8f3cc2d9810a0b1146d 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 | 32 ++------------ cve-diff.js | 118 ++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 100 insertions(+), 50 deletions(-) diff --git a/action.yaml b/action.yaml index f576ef2..2257cd4 100644 --- a/action.yaml +++ b/action.yaml @@ -407,43 +407,17 @@ runs: exit 1 fi - # 5. Run multi-arch set-based diff (both are guaranteed present and valid) + # 5. Run multi-arch set-based diff and generate consolidated full summary report echo "Both head and target scans succeeded. Computing set-based vulnerability delta..." if ! node "${{ github.action_path }}/cve-diff.js" \ "cve-reports/target" \ "cve-reports/head" \ - "${{ inputs.target-branch }}" > "${{ inputs.output-delta }}"; then + "${{ inputs.target-branch }}" \ + "${{ inputs.output-summary }}" > "${{ inputs.output-delta }}"; then echo "::error::Failed to generate CVE differential report" exit 1 fi - # 6. 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 - if ! trivy sbom \ - --skip-version-check \ - --format template \ - --template "@${{ github.action_path }}/markdown.tpl" \ - --output "${{ inputs.output-summary }}" \ - "$primary_sbom"; then - echo "::error::Trivy summary generation failed for primary SBOM: $primary_sbom" - exit 1 - fi - fi - - name: Publish Reports to Step Summary shell: bash run: | diff --git a/cve-diff.js b/cve-diff.js index 1f28134..f0de854 100755 --- a/cve-diff.js +++ b/cve-diff.js @@ -122,9 +122,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 || ''; @@ -132,18 +133,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(); @@ -151,7 +140,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} |`; } /** @@ -205,6 +209,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); @@ -237,10 +242,15 @@ function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') { lines.push(`#### 🚨 New Vulnerabilities Introduced (${newVulns.length})`); 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.'); @@ -249,10 +259,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.'); @@ -262,22 +277,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) { @@ -288,4 +363,5 @@ module.exports = { loadReportsForArg, formatVulnRow, renderMarkdownDiff, + renderFullSummary, };