From a488121a7b7309dc4604b612c53584b1e9ce5084 Mon Sep 17 00:00:00 2001 From: Remylus Losius Date: Sun, 21 Jun 2026 23:24:44 -0400 Subject: [PATCH] feat(reports): surface attestation PDF + OSCAL SAR downloads in the UI The attestation report detail offered only CSV (primary) + JSON, leaving the OSCAL SAR (B2) and PDF cover (B3b) faces reachable by API only. Add a secondaryFaces list, keyed on the resolved report kind, that renders extra download buttons: - Attestation: PDF (the bounded cover) + OSCAL SAR (the fleet assessment-results), beside the primary CSV and JSON. - Executive: none (its PDF is the primary). The extra controls share the onDownload handler, the in-flight downloading state, and the downloadError surface; downloadReportFace's format union gains 'oscal_sar'. Spec frontend-reports v1.7.0: C-10 + AC-11 (source-inspection test). --- frontend/src/pages/reports/ReportsPage.tsx | 50 ++++++++++++++++++++-- frontend/tests/pages/reports.test.ts | 18 ++++++++ specs/frontend/reports.spec.yaml | 26 ++++++++++- 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/reports/ReportsPage.tsx b/frontend/src/pages/reports/ReportsPage.tsx index c8459490..f2d8e8a4 100644 --- a/frontend/src/pages/reports/ReportsPage.tsx +++ b/frontend/src/pages/reports/ReportsPage.tsx @@ -469,7 +469,10 @@ function LibraryTab({ // session cookie authenticates it (same-origin credentials) and no CSRF // token is needed; the filename comes from the server's // Content-Disposition. Errors are surfaced to the caller. -async function downloadReportFace(id: string, format: 'pdf' | 'json' | 'csv'): Promise { +async function downloadReportFace( + id: string, + format: 'pdf' | 'json' | 'csv' | 'oscal_sar', +): Promise { const res = await fetch(`/api/v1/reports/${id}/export?format=${format}`, { credentials: 'same-origin', }); @@ -595,12 +598,12 @@ function ReportDetail({ id: string; onClose: () => void; }) { - const [downloading, setDownloading] = useState<'pdf' | 'json' | 'csv' | null>(null); + const [downloading, setDownloading] = useState<'pdf' | 'json' | 'csv' | 'oscal_sar' | null>(null); const [downloadError, setDownloadError] = useState(null); const [verifying, setVerifying] = useState(false); const [verifyResult, setVerifyResult] = useState(null); - async function onDownload(format: 'pdf' | 'json' | 'csv') { + async function onDownload(format: 'pdf' | 'json' | 'csv' | 'oscal_sar') { setDownloading(format); setDownloadError(null); try { @@ -655,6 +658,22 @@ function ReportDetail({ ? 'Download the per-host, per-rule CSV evidence' : 'Download the one-page executive PDF'; + // Secondary faces offered beside the primary + JSON. An attestation also + // exposes its bounded PDF cover and the fleet OSCAL SAR (the + // machine-readable assessment-results); an executive report has no extra + // faces (PDF is its primary, JSON is shown for both below). + const secondaryFaces: { face: 'pdf' | 'oscal_sar'; label: string; title: string }[] = + isAttestation + ? [ + { face: 'pdf', label: 'PDF', title: 'Download the one-page attestation cover PDF' }, + { + face: 'oscal_sar', + label: 'OSCAL SAR', + title: 'Download the OSCAL assessment-results (evidence referenced by hash)', + }, + ] + : []; + return (
{downloading === primaryFace ? 'Preparing…' : primaryLabel} + {secondaryFaces.map((f) => ( + + ))}