feat(cra): snapshot/history UI + measure-class (code-fix vs process) UI

Snapshot/history: "Snapshot speichern" + a version list (status, date, coverage)
you can click through — makes the CRA Art. 13 running system visible (backend
endpoints already live). Measure-class: each finding shows a remediation-class
badge from its CRA evidence_type ("Code-nah" = scan-locatable, code-fix in the
ticket possible; otherwise Prozess/Doku), and the measures section is relabelled
as the Sollzustand (process/build) — no auto-fix buttons on process measures.
Backend: MappedFinding now carries evidence_type.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-14 10:02:17 +02:00
parent 05bd0418f8
commit ee1632cd52
7 changed files with 173 additions and 39 deletions
@@ -1,19 +1,24 @@
'use client'
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { CRADemo, CRAFinding, Measure, DEMO_SCENARIO } from './useCRADemo'
// Live CRA assessment: POST the (demo) findings + the customer's priority weights
// to POST /api/v1/cra/assess and merge the live, priority-sorted mapping (CRA
// requirement, risk, measures, NIST/OWASP crosswalk, priority tier + reason +
// quick-win) with the frontend scenario constants (full measure texts +
// cyber->safety cross-links). Falls back to the static scenario if unreachable.
// Live CRA assessment: POST the (demo) findings + customer weights + the project's
// safety functions to /api/v1/cra/assess and merge the live, priority-sorted
// mapping with the frontend scenario constants. Also save/list versioned
// snapshots (the CRA Art. 13 running system). Falls back to the static scenario
// if the backend is unreachable.
export type Weights = Record<string, string> // objective -> high|medium|low
// Demo: CE-risk-assessment safety functions of the Kistenhub (would come from the
// project's CE risk assessment in production). The backend bridge decides which
// cyber findings can defeat them (and flags those safety_impact -> P0).
export interface SnapshotMeta {
id: string
version: number
status: string
generated_at: string
coverage?: { covered?: string[]; total?: number }
}
const SAFETY_FUNCTIONS = [
{
name: 'Zweihandschaltung + trennende Schutzeinrichtung am Hubwerk',
@@ -38,7 +43,6 @@ function merge(live: any): CRADemo {
const meta: Record<string, CRAFinding> = {}
for (const f of DEMO_SCENARIO.findings) meta[f.id] = f
// iterate live.mapped to PRESERVE the backend priority order
const findings: CRAFinding[] = (live.mapped || []).map((m: any) => {
const base = meta[m.finding_id]
return {
@@ -52,6 +56,7 @@ function merge(live: any): CRADemo {
owasp_refs: m.owasp_refs || [],
risk_level: m.risk_level || (base ? base.risk_level : 'LOW'),
measures: m.measures || [],
evidence_type: m.evidence_type,
priority_tier: m.priority_tier,
priority_score: m.priority_score,
quick_win: m.quick_win,
@@ -79,43 +84,56 @@ function merge(live: any): CRADemo {
}
}
export function useCRA() {
export function useCRA(projectId?: string) {
const [data, setData] = useState<CRADemo | null>(null)
const [live, setLive] = useState(false)
const [weights, setWeights] = useState<Weights>({})
const [snapshots, setSnapshots] = useState<SnapshotMeta[]>([])
const buildPayload = useCallback(() => ({
findings: DEMO_SCENARIO.findings.map((f) => ({
id: f.id, title: f.title, cwe: f.cwe, severity: f.scanner_severity, location: f.location,
})),
weights,
safety_functions: SAFETY_FUNCTIONS,
}), [weights])
useEffect(() => {
let cancelled = false
const payload = {
findings: DEMO_SCENARIO.findings.map((f) => ({
id: f.id, title: f.title, cwe: f.cwe, severity: f.scanner_severity, location: f.location,
})),
weights,
// the bridge decides safety_impact from these (no frontend hardcode)
safety_functions: SAFETY_FUNCTIONS,
}
fetch('/api/v1/cra/assess', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(buildPayload()),
})
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
.then((j) => {
if (cancelled) return
setData(merge(j))
setLive(true)
})
.then((j) => { if (!cancelled) { setData(merge(j)); setLive(true) } })
.catch((err) => {
console.error('CRA assess fetch failed, using static scenario:', err)
if (!cancelled) {
setData(DEMO_SCENARIO)
setLive(false)
}
if (!cancelled) { setData(DEMO_SCENARIO); setLive(false) }
})
return () => {
cancelled = true
}
}, [weights])
return () => { cancelled = true }
}, [buildPayload])
return { data, live, weights, setWeights }
const refreshSnapshots = useCallback(() => {
if (!projectId) return
fetch(`/api/v1/cra/projects/${projectId}/assess-snapshots`)
.then((r) => (r.ok ? r.json() : null))
.then((j) => { if (j) setSnapshots(j.snapshots || []) })
.catch(() => {})
}, [projectId])
useEffect(() => { refreshSnapshots() }, [refreshSnapshots])
const saveSnapshot = useCallback(async () => {
if (!projectId) return
await fetch(`/api/v1/cra/projects/${projectId}/assess-snapshot`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(buildPayload()),
})
refreshSnapshots()
}, [projectId, buildPayload, refreshSnapshots])
const viewSnapshot = useCallback(async (id: string) => {
const r = await fetch(`/api/v1/cra/assess-snapshots/${id}`)
return r.ok ? r.json() : null
}, [])
return { data, live, weights, setWeights, snapshots, saveSnapshot, viewSnapshot }
}
@@ -27,6 +27,7 @@ export interface CRAFinding {
owasp_refs: OwaspRef[]
risk_level: string
measures: string[]
evidence_type?: string // code | process | hybrid | document — drives the remediation-class badge
// priority layer (set live by the backend prioritizer; optional in the static fallback)
priority_tier?: string
priority_score?: number