Skip to content

Commit ae8a719

Browse files
feat(reports): surface attestation PDF + OSCAL SAR downloads in the UI (#645)
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).
1 parent b6b880d commit ae8a719

3 files changed

Lines changed: 90 additions & 4 deletions

File tree

frontend/src/pages/reports/ReportsPage.tsx

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,10 @@ function LibraryTab({
469469
// session cookie authenticates it (same-origin credentials) and no CSRF
470470
// token is needed; the filename comes from the server's
471471
// Content-Disposition. Errors are surfaced to the caller.
472-
async function downloadReportFace(id: string, format: 'pdf' | 'json' | 'csv'): Promise<void> {
472+
async function downloadReportFace(
473+
id: string,
474+
format: 'pdf' | 'json' | 'csv' | 'oscal_sar',
475+
): Promise<void> {
473476
const res = await fetch(`/api/v1/reports/${id}/export?format=${format}`, {
474477
credentials: 'same-origin',
475478
});
@@ -595,12 +598,12 @@ function ReportDetail({
595598
id: string;
596599
onClose: () => void;
597600
}) {
598-
const [downloading, setDownloading] = useState<'pdf' | 'json' | 'csv' | null>(null);
601+
const [downloading, setDownloading] = useState<'pdf' | 'json' | 'csv' | 'oscal_sar' | null>(null);
599602
const [downloadError, setDownloadError] = useState<string | null>(null);
600603
const [verifying, setVerifying] = useState(false);
601604
const [verifyResult, setVerifyResult] = useState<VerifyResult | null>(null);
602605

603-
async function onDownload(format: 'pdf' | 'json' | 'csv') {
606+
async function onDownload(format: 'pdf' | 'json' | 'csv' | 'oscal_sar') {
604607
setDownloading(format);
605608
setDownloadError(null);
606609
try {
@@ -655,6 +658,22 @@ function ReportDetail({
655658
? 'Download the per-host, per-rule CSV evidence'
656659
: 'Download the one-page executive PDF';
657660

661+
// Secondary faces offered beside the primary + JSON. An attestation also
662+
// exposes its bounded PDF cover and the fleet OSCAL SAR (the
663+
// machine-readable assessment-results); an executive report has no extra
664+
// faces (PDF is its primary, JSON is shown for both below).
665+
const secondaryFaces: { face: 'pdf' | 'oscal_sar'; label: string; title: string }[] =
666+
isAttestation
667+
? [
668+
{ face: 'pdf', label: 'PDF', title: 'Download the one-page attestation cover PDF' },
669+
{
670+
face: 'oscal_sar',
671+
label: 'OSCAL SAR',
672+
title: 'Download the OSCAL assessment-results (evidence referenced by hash)',
673+
},
674+
]
675+
: [];
676+
658677
return (
659678
<div
660679
role="dialog"
@@ -769,6 +788,31 @@ function ReportDetail({
769788
>
770789
{downloading === primaryFace ? 'Preparing…' : primaryLabel}
771790
</button>
791+
{secondaryFaces.map((f) => (
792+
<button
793+
key={f.face}
794+
type="button"
795+
onClick={() => onDownload(f.face)}
796+
disabled={downloading !== null}
797+
title={f.title}
798+
style={{
799+
height: 32,
800+
padding: '0 12px',
801+
borderRadius: 6,
802+
border: '1px solid var(--ow-line)',
803+
background: 'var(--ow-bg-1)',
804+
color: 'var(--ow-fg-1)',
805+
fontFamily: 'inherit',
806+
fontSize: 12,
807+
fontWeight: 500,
808+
cursor: downloading !== null ? 'default' : 'pointer',
809+
opacity: downloading !== null ? 0.6 : 1,
810+
whiteSpace: 'nowrap',
811+
}}
812+
>
813+
{downloading === f.face ? 'Preparing…' : f.label}
814+
</button>
815+
))}
772816
<button
773817
type="button"
774818
onClick={() => onDownload('json')}

frontend/tests/pages/reports.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// AC-10 report-kind selector (executive/attestation) drives the generate
1414
// body; kind-aware primary download (csv for attestation, pdf
1515
// otherwise); kindLabel maps attestation -> "Attestation"
16+
// AC-11 secondaryFaces surfaces the attestation PDF + OSCAL SAR downloads
17+
// (executive none); downloadReportFace accepts 'oscal_sar'
1618

1719
import { describe, expect, test } from 'vitest';
1820
import { readFileSync } from 'node:fs';
@@ -182,6 +184,22 @@ describe('frontend-reports — reports library page', () => {
182184
expect(PAGE_SRC).toMatch(/kind === 'attestation'\) return 'Attestation'/);
183185
});
184186

187+
// @ac AC-11
188+
test('frontend-reports/AC-11 — secondary attestation faces (PDF + OSCAL SAR)', () => {
189+
// A secondaryFaces list keyed on the attestation kind.
190+
expect(PAGE_SRC).toContain('secondaryFaces');
191+
expect(PAGE_SRC).toMatch(/secondaryFaces[\s\S]*?isAttestation/);
192+
// It offers the PDF cover and the OSCAL SAR faces.
193+
expect(PAGE_SRC).toMatch(/face: 'pdf'/);
194+
expect(PAGE_SRC).toMatch(/face: 'oscal_sar'/);
195+
expect(PAGE_SRC).toContain('OSCAL SAR');
196+
// Rendered as buttons wired to the shared onDownload + downloading state.
197+
expect(PAGE_SRC).toMatch(/secondaryFaces\.map/);
198+
expect(PAGE_SRC).toMatch(/onDownload\(f\.face\)/);
199+
// downloadReportFace's format union includes oscal_sar.
200+
expect(PAGE_SRC).toMatch(/format: 'pdf' \| 'json' \| 'csv' \| 'oscal_sar'/);
201+
});
202+
185203
// @ac AC-04
186204
test('frontend-reports/AC-04 — generate is the only mutation, tokens, no em-dash', () => {
187205
// The only mutating call is the generate POST; no PUT/DELETE.

specs/frontend/reports.spec.yaml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
spec:
22
id: frontend-reports
33
title: Reports page - Library table, Generate action, deferred Templates and Scheduled tabs
4-
version: "1.6.0"
4+
version: "1.7.0"
55
status: approved
66
tier: 2
77

@@ -173,6 +173,20 @@ spec:
173173
em-dash.
174174
type: technical
175175
enforcement: error
176+
- id: C-10
177+
description: >-
178+
v1.7.0 - The report detail surfaces ALL faces that apply to the
179+
resolved report's kind, not just the primary + JSON. An attestation
180+
report additionally offers a PDF (the bounded cover) and an OSCAL
181+
SAR (the fleet assessment-results) download beside the primary CSV
182+
and JSON; an executive report has no extra faces (its PDF is the
183+
primary). The extra controls are driven by a secondaryFaces list
184+
keyed on the kind, each invoking the same onDownload handler (so
185+
they share the in-flight downloading state and downloadError
186+
surface), and downloadReportFace accepts the oscal_sar format. UI
187+
copy carries no em-dash.
188+
type: technical
189+
enforcement: error
176190
- id: C-03
177191
description: >-
178192
The Templates and Scheduled tabs render an explicit deferred state
@@ -290,3 +304,13 @@ spec:
290304
"attestation" to "Attestation". The added copy contains no em-dash.
291305
priority: high
292306
references_constraints: [C-09]
307+
- id: AC-11
308+
description: >-
309+
v1.7.0 - Source inspection: a secondaryFaces list is built from the
310+
resolved report kind - an attestation yields a 'pdf' entry and an
311+
'oscal_sar' entry (label "OSCAL SAR"), an executive yields none -
312+
and is rendered as extra download buttons that call onDownload(face)
313+
and reflect the downloading state. downloadReportFace's format union
314+
includes 'oscal_sar'. The added copy contains no em-dash.
315+
priority: high
316+
references_constraints: [C-10]

0 commit comments

Comments
 (0)