feat: show Arch column only when multiple architectures are scanned
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m46s

This commit is contained in:
Philipp committed 2026-09-13 22:01:33 +02:00
1 parent 44dade1825
commit 257e67ff81
2 files changed
+100 -46

No files matched your search

+3 -25
View File
@@ -228,34 +228,12 @@ runs:
trivy sbom --format json --output "cve-reports/target/cve-${arch}.json" "$f" || true trivy sbom --format json --output "cve-reports/target/cve-${arch}.json" "$f" || true
done 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" \ node "${{ github.action_path }}/cve-diff.js" \
"cve-reports/target" \ "cve-reports/target" \
"cve-reports/head" \ "cve-reports/head" \
"${{ inputs.target-branch }}" > "${{ inputs.output-delta }}" || true "${{ inputs.target-branch }}" \
"${{ inputs.output-summary }}" > "${{ 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
- name: Publish Reports to Step Summary - name: Publish Reports to Step Summary
shell: bash shell: bash
+97 -21
View File
@@ -103,9 +103,10 @@ function loadReportsForArg(arg) {
* Formats a vulnerability record into a Markdown table row. * Formats a vulnerability record into a Markdown table row.
* @param {object} v * @param {object} v
* @param {Set<string>} [allScannedArchs] * @param {Set<string>} [allScannedArchs]
* @param {boolean} [showArch]
* @returns {string} * @returns {string}
*/ */
function formatVulnRow(v, allScannedArchs = new Set()) { function formatVulnRow(v, allScannedArchs = new Set(), showArch = true) {
const sev = v.Severity || 'UNKNOWN'; const sev = v.Severity || 'UNKNOWN';
const badge = SEVERITY_BADGES[sev] || `⚪ ${sev}`; const badge = SEVERITY_BADGES[sev] || `⚪ ${sev}`;
const vid = v.VulnerabilityID || ''; const vid = v.VulnerabilityID || '';
@@ -113,18 +114,6 @@ function formatVulnRow(v, allScannedArchs = new Set()) {
const link = url ? `[${vid}](${url})` : vid; const link = url ? `[${vid}](${url})` : vid;
const pkg = v.PkgName || ''; 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 installed = v.InstalledVersion || '-';
const fixed = v.FixedVersion || '-'; const fixed = v.FixedVersion || '-';
let title = (v.Title || v.Description || '').replace(/\n/g, ' ').trim(); 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) + '...'; 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 || []), ...(baseCves.scannedArchs || []),
...(branchCves.scannedArchs || []), ...(branchCves.scannedArchs || []),
]); ]);
const showArch = allScannedArchs.size > 1;
const newVulns = newKeys.map((k) => branchCves.get(k)).sort(sortVulns); const newVulns = newKeys.map((k) => branchCves.get(k)).sort(sortVulns);
const resolvedVulns = resolvedKeys.map((k) => baseCves.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) { if (newVulns.length > 0) {
lines.push('| Severity | CVE | Package | Arch | Installed | Fixed In | Title |'); if (showArch) {
lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |'); 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) { for (const v of newVulns) {
lines.push(formatVulnRow(v, allScannedArchs)); lines.push(formatVulnRow(v, allScannedArchs, showArch));
} }
} else { } else {
lines.push('✅ No new vulnerabilities introduced in this build.'); 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})`); lines.push(`#### ✅ Resolved / Removed Vulnerabilities (${resolvedVulns.length})`);
if (resolvedVulns.length > 0) { if (resolvedVulns.length > 0) {
lines.push('| Severity | CVE | Package | Arch | Previously Installed | Fixed In | Title |'); if (showArch) {
lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |'); 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) { for (const v of resolvedVulns) {
lines.push(formatVulnRow(v, allScannedArchs)); lines.push(formatVulnRow(v, allScannedArchs, showArch));
} }
} else { } else {
lines.push('⚪ No existing vulnerabilities were resolved.'); lines.push('⚪ No existing vulnerabilities were resolved.');
@@ -230,22 +245,82 @@ function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') {
return lines.join('\n'); return lines.join('\n');
} }
/**
* Renders a full markdown summary report of all vulnerabilities in a scan.
* @param {Map<string, object>} 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() { function main() {
const args = process.argv.slice(2); const args = process.argv.slice(2);
if (args.length < 2) { if (args.length < 2) {
console.error('Usage: cve-diff.js <base_report(s)> <branch_report(s)> [base_name]'); console.error('Usage: cve-diff.js <base_report(s)> <branch_report(s)> [base_name] [summary_output_path]');
process.exit(1); process.exit(1);
} }
const baseArg = args[0]; const baseArg = args[0];
const branchArg = args[1]; const branchArg = args[1];
const baseName = args[2] || 'main'; const baseName = args[2] || 'main';
const summaryOutputPath = args[3];
const baseCves = loadReportsForArg(baseArg); const baseCves = loadReportsForArg(baseArg);
const branchCves = loadReportsForArg(branchArg); const branchCves = loadReportsForArg(branchArg);
const output = renderMarkdownDiff(baseCves, branchCves, baseName); const output = renderMarkdownDiff(baseCves, branchCves, baseName);
console.log(output); console.log(output);
if (summaryOutputPath) {
const summaryOutput = renderFullSummary(branchCves);
fs.writeFileSync(summaryOutputPath, summaryOutput, 'utf8');
}
} }
if (require.main === module) { if (require.main === module) {
@@ -256,4 +331,5 @@ module.exports = {
loadReportsForArg, loadReportsForArg,
formatVulnRow, formatVulnRow,
renderMarkdownDiff, renderMarkdownDiff,
renderFullSummary,
}; };