feat(security): export scan results as SARIF 2.1.0 (#652)

Adds a new SarifExporter service that builds a SARIF 2.1.0 document
from the stored scan findings (vulnerabilities, secrets, misconfigs).
Rule IDs are namespaced to avoid collisions in a flat result list.
Suppressions carry through as SARIF suppressions[] entries so GitHub
code scanning and Defender for Cloud see the same accepted status
shown in the UI.

Exposed via GET /api/security/scans/:id/sarif, admin + paid-tier
gated to match the SBOM export precedent. A SARIF button appears
in the scan sheet next to SBOM and CSV for paid tiers.
This commit is contained in:
Anso
2026-04-17 08:17:36 -04:00
committed by GitHub
parent a95bf1ff33
commit 12bbf86dc4
5 changed files with 618 additions and 0 deletions
@@ -345,6 +345,30 @@ export function VulnerabilityScanSheet({
URL.revokeObjectURL(url);
}, [scan, details]);
const exportSarif = useCallback(async () => {
if (!scan) return;
try {
const res = await apiFetch(`/security/scans/${scan.id}/sarif`);
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body?.error || 'Failed to generate SARIF');
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${scan.image_ref.replace(/[^a-z0-9]+/gi, '_')}.sarif.json`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
toast.success('SARIF downloaded');
} catch (err) {
const error = err as { message?: string; error?: string; data?: { error?: string } };
toast.error(error?.message || error?.error || error?.data?.error || 'SARIF export failed');
}
}, [scan]);
return (
<Sheet open={scanId != null} onOpenChange={(open) => !open && onClose()}>
<SheetContent className="sm:max-w-2xl flex flex-col p-0">
@@ -459,6 +483,18 @@ export function VulnerabilityScanSheet({
<Download className="w-3.5 h-3.5 mr-1.5" strokeWidth={1.5} />
CSV
</Button>
{canGenerateSbom && (
<Button
variant="outline"
size="sm"
onClick={exportSarif}
disabled={scan.status !== 'completed'}
title="Export findings as SARIF 2.1.0 for GitHub code scanning"
>
<Download className="w-3.5 h-3.5 mr-1.5" strokeWidth={1.5} />
SARIF
</Button>
)}
{canCompare && (
<Button
variant="outline"