Public Access
generated from Philipp/template-git
feat: dynamic multi-architecture discovery, iteration, and conditional reporting #11
No files matched your search
+3
-29
@@ -407,43 +407,17 @@ runs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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..."
|
echo "Both head and target scans succeeded. Computing set-based vulnerability delta..."
|
||||||
if ! node "${{ github.action_path }}/cve-diff.js" \
|
if ! 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 }}"; then
|
"${{ inputs.target-branch }}" \
|
||||||
|
"${{ inputs.output-summary }}" > "${{ inputs.output-delta }}"; then
|
||||||
echo "::error::Failed to generate CVE differential report"
|
echo "::error::Failed to generate CVE differential report"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
- name: Publish Reports to Step Summary
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+97
-21
@@ -122,9 +122,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 || '';
|
||||||
@@ -132,18 +133,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();
|
||||||
@@ -151,7 +140,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} |`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -205,6 +209,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);
|
||||||
@@ -237,10 +242,15 @@ function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') {
|
|||||||
lines.push(`#### 🚨 New Vulnerabilities Introduced (${newVulns.length})`);
|
lines.push(`#### 🚨 New Vulnerabilities Introduced (${newVulns.length})`);
|
||||||
|
|
||||||
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.');
|
||||||
@@ -249,10 +259,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.');
|
||||||
@@ -262,22 +277,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) {
|
||||||
@@ -288,4 +363,5 @@ module.exports = {
|
|||||||
loadReportsForArg,
|
loadReportsForArg,
|
||||||
formatVulnRow,
|
formatVulnRow,
|
||||||
renderMarkdownDiff,
|
renderMarkdownDiff,
|
||||||
|
renderFullSummary,
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user