feat(cra): live-wire CRA tab to POST /api/v1/cra/assess (proxy + useCRA)

CRA tab now computes the assessment live: useCRA POSTs the scenario findings
through a new /api/v1/cra/* proxy to the backend mapper and merges the live
mapping (CRA requirement, risk, measures, NIST/OWASP crosswalk) with the
frontend scenario constants (full measure texts + cyber->safety cross-links,
until those move server-side in step 2). Falls back to the static scenario if
the backend is unreachable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-14 07:30:00 +02:00
parent 34a678caef
commit ad83b8dc67
4 changed files with 164 additions and 7 deletions
@@ -0,0 +1,90 @@
'use client'
import { useEffect, useState } from 'react'
import { CRADemo, CRAFinding, Measure, DEMO_SCENARIO } from './useCRADemo'
// Live CRA assessment: POST the (demo) findings to the standalone backend
// endpoint POST /api/v1/cra/assess and merge the live mapping (CRA requirement,
// risk, measures, NIST/OWASP crosswalk) with the frontend scenario constants
// (full measure texts + cyber->safety cross-links — until those move server-side
// in step 2). Falls back to the static scenario if the backend is unreachable.
function reqTitle(rationale: string): string {
const i = rationale.indexOf(': ')
return i >= 0 ? rationale.slice(i + 2) : rationale
}
function merge(live: any): CRADemo {
const mapped: Record<string, any> = {}
for (const m of live.mapped || []) mapped[m.finding_id] = m
const findings: CRAFinding[] = DEMO_SCENARIO.findings.map((df) => {
const m = mapped[df.id]
if (!m) return df
return {
...df,
primary_requirement: m.primary_requirement,
requirement_title: reqTitle(m.rationale || ''),
requirement_ids: m.requirement_ids || [],
annex_anchor: m.annex_anchor || '',
iso27001_ref: m.iso27001_ref || [],
nist_refs: m.nist_refs || [],
owasp_refs: m.owasp_refs || [],
risk_level: m.risk_level || df.risk_level,
measures: m.measures || [],
}
})
const open_measures: Measure[] = (live.open_measures || []).map((om: any) => {
const detail = DEMO_SCENARIO.open_measures.find((d) => d.id === om.id)
return detail || { id: om.id, name: om.id, description: om.description || '', norm_refs: [] }
})
return {
scenario: DEMO_SCENARIO.scenario,
findings,
by_risk: live.by_risk || DEMO_SCENARIO.by_risk,
coverage_pct: live.coverage_pct ?? DEMO_SCENARIO.coverage_pct,
requirements_touched: live.requirements_touched || DEMO_SCENARIO.requirements_touched,
open_measures,
cross_links: DEMO_SCENARIO.cross_links,
deadlines: live.deadlines || DEMO_SCENARIO.deadlines,
}
}
export function useCRA() {
const [data, setData] = useState<CRADemo | null>(null)
const [live, setLive] = useState(false)
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,
})),
}
fetch('/api/v1/cra/assess', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
.then((j) => {
if (cancelled) return
setData(merge(j))
setLive(true)
})
.catch((err) => {
console.error('CRA assess fetch failed, using static scenario:', err)
if (!cancelled) {
setData(DEMO_SCENARIO)
setLive(false)
}
})
return () => {
cancelled = true
}
}, [])
return { data, live }
}
@@ -73,7 +73,11 @@ const MEASURES: Measure[] = [
norm_refs: ['Verordnung (EU) 2024/2847 (CRA), Anhang II', 'DIN EN 40000-1-2 (Entwurf)', 'IEC 62443-3-3'] },
]
const DEMO: CRADemo = {
// Scenario constants: the invented Kistenhub IoT findings (input) + the full
// curated measure texts + the cyber->safety cross-links. The live useCRA hook
// POSTs these findings to the backend and merges the real assessment; this also
// serves as the offline fallback.
export const DEMO_SCENARIO: CRADemo = {
scenario:
'Kistenhubgeraet mit (angenommenem) IoT-Modul / Internetanschluss — Fernsteuerung, Telemetrie und Remote-Updates.',
findings: [
@@ -139,6 +143,3 @@ const DEMO: CRADemo = {
],
}
export function useCRADemo() {
return { data: DEMO }
}
@@ -1,9 +1,21 @@
'use client'
import { useCRADemo } from './_hooks/useCRADemo'
import { useCRA } from './_hooks/useCRA'
import { CRACyberView } from './_components/CRACyberView'
export default function CRAPage() {
const { data } = useCRADemo()
return <CRACyberView data={data} />
const { data, live } = useCRA()
if (!data) {
return <p className="text-sm text-gray-500">CRA-Risikobeurteilung wird geladen </p>
}
return (
<div>
{!live && (
<p className="mb-3 text-[11px] text-amber-600 dark:text-amber-400">
Backend nicht erreichbar statisches Szenario angezeigt.
</p>
)}
<CRACyberView data={data} />
</div>
)
}