Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 47 additions & 3 deletions frontend/src/pages/reports/ReportsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
async function downloadReportFace(
id: string,
format: 'pdf' | 'json' | 'csv' | 'oscal_sar',
): Promise<void> {
const res = await fetch(`/api/v1/reports/${id}/export?format=${format}`, {
credentials: 'same-origin',
});
Expand Down Expand Up @@ -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<string | null>(null);
const [verifying, setVerifying] = useState(false);
const [verifyResult, setVerifyResult] = useState<VerifyResult | null>(null);

async function onDownload(format: 'pdf' | 'json' | 'csv') {
async function onDownload(format: 'pdf' | 'json' | 'csv' | 'oscal_sar') {
setDownloading(format);
setDownloadError(null);
try {
Expand Down Expand Up @@ -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 (
<div
role="dialog"
Expand Down Expand Up @@ -769,6 +788,31 @@ function ReportDetail({
>
{downloading === primaryFace ? 'Preparing…' : primaryLabel}
</button>
{secondaryFaces.map((f) => (
<button
key={f.face}
type="button"
onClick={() => onDownload(f.face)}
disabled={downloading !== null}
title={f.title}
style={{
height: 32,
padding: '0 12px',
borderRadius: 6,
border: '1px solid var(--ow-line)',
background: 'var(--ow-bg-1)',
color: 'var(--ow-fg-1)',
fontFamily: 'inherit',
fontSize: 12,
fontWeight: 500,
cursor: downloading !== null ? 'default' : 'pointer',
opacity: downloading !== null ? 0.6 : 1,
whiteSpace: 'nowrap',
}}
>
{downloading === f.face ? 'Preparing…' : f.label}
</button>
))}
<button
type="button"
onClick={() => onDownload('json')}
Expand Down
18 changes: 18 additions & 0 deletions frontend/tests/pages/reports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
// AC-10 report-kind selector (executive/attestation) drives the generate
// body; kind-aware primary download (csv for attestation, pdf
// otherwise); kindLabel maps attestation -> "Attestation"
// AC-11 secondaryFaces surfaces the attestation PDF + OSCAL SAR downloads
// (executive none); downloadReportFace accepts 'oscal_sar'

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

// @ac AC-11
test('frontend-reports/AC-11 — secondary attestation faces (PDF + OSCAL SAR)', () => {
// A secondaryFaces list keyed on the attestation kind.
expect(PAGE_SRC).toContain('secondaryFaces');
expect(PAGE_SRC).toMatch(/secondaryFaces[\s\S]*?isAttestation/);
// It offers the PDF cover and the OSCAL SAR faces.
expect(PAGE_SRC).toMatch(/face: 'pdf'/);
expect(PAGE_SRC).toMatch(/face: 'oscal_sar'/);
expect(PAGE_SRC).toContain('OSCAL SAR');
// Rendered as buttons wired to the shared onDownload + downloading state.
expect(PAGE_SRC).toMatch(/secondaryFaces\.map/);
expect(PAGE_SRC).toMatch(/onDownload\(f\.face\)/);
// downloadReportFace's format union includes oscal_sar.
expect(PAGE_SRC).toMatch(/format: 'pdf' \| 'json' \| 'csv' \| 'oscal_sar'/);
});

// @ac AC-04
test('frontend-reports/AC-04 — generate is the only mutation, tokens, no em-dash', () => {
// The only mutating call is the generate POST; no PUT/DELETE.
Expand Down
26 changes: 25 additions & 1 deletion specs/frontend/reports.spec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
spec:
id: frontend-reports
title: Reports page - Library table, Generate action, deferred Templates and Scheduled tabs
version: "1.6.0"
version: "1.7.0"
status: approved
tier: 2

Expand Down Expand Up @@ -173,6 +173,20 @@ spec:
em-dash.
type: technical
enforcement: error
- id: C-10
description: >-
v1.7.0 - The report detail surfaces ALL faces that apply to the
resolved report's kind, not just the primary + JSON. An attestation
report additionally offers a PDF (the bounded cover) and an OSCAL
SAR (the fleet assessment-results) download beside the primary CSV
and JSON; an executive report has no extra faces (its PDF is the
primary). The extra controls are driven by a secondaryFaces list
keyed on the kind, each invoking the same onDownload handler (so
they share the in-flight downloading state and downloadError
surface), and downloadReportFace accepts the oscal_sar format. UI
copy carries no em-dash.
type: technical
enforcement: error
- id: C-03
description: >-
The Templates and Scheduled tabs render an explicit deferred state
Expand Down Expand Up @@ -290,3 +304,13 @@ spec:
"attestation" to "Attestation". The added copy contains no em-dash.
priority: high
references_constraints: [C-09]
- id: AC-11
description: >-
v1.7.0 - Source inspection: a secondaryFaces list is built from the
resolved report kind - an attestation yields a 'pdf' entry and an
'oscal_sar' entry (label "OSCAL SAR"), an executive yields none -
and is rendered as extra download buttons that call onDownload(face)
and reflect the downloading state. downloadReportFace's format union
includes 'oscal_sar'. The added copy contains no em-dash.
priority: high
references_constraints: [C-10]
Loading