Public Access
generated from Philipp/template-git
feat: show Arch column only when multiple architectures are scanned
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 3m5s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 3m5s
This commit is contained in:
2 files changed
+100
-50
No files matched your search
+3
-29
@@ -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: |
|
||||
|
||||
+97
-21
@@ -122,9 +122,10 @@ function loadReportsForArg(arg) {
|
||||
* Formats a vulnerability record into a Markdown table row.
|
||||
* @param {object} v
|
||||
* @param {Set<string>} [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<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() {
|
||||
const args = process.argv.slice(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);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
Reference in new issue
Block a user