From ad83b8dc67a58b9a596264672c1fee796ee2ac83 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sun, 14 Jun 2026 07:30:00 +0200 Subject: [PATCH] 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 --- .../app/api/v1/cra/[...path]/route.ts | 54 +++++++++++ .../sdk/iace/[projectId]/cra/_hooks/useCRA.ts | 90 +++++++++++++++++++ .../iace/[projectId]/cra/_hooks/useCRADemo.ts | 9 +- .../app/sdk/iace/[projectId]/cra/page.tsx | 18 +++- 4 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 admin-compliance/app/api/v1/cra/[...path]/route.ts create mode 100644 admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts diff --git a/admin-compliance/app/api/v1/cra/[...path]/route.ts b/admin-compliance/app/api/v1/cra/[...path]/route.ts new file mode 100644 index 00000000..cedf2870 --- /dev/null +++ b/admin-compliance/app/api/v1/cra/[...path]/route.ts @@ -0,0 +1,54 @@ +/** + * CRA API proxy — catch-all. Proxies /api/v1/cra/* to the Python backend + * (e.g. POST /api/v1/cra/assess, the standalone CRA risk-assessment endpoint). + */ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000' + +async function forward(request: NextRequest, path: string[], method: 'GET' | 'POST') { + const pathStr = path.join('/') + const search = request.nextUrl.searchParams.toString() + const url = `${BACKEND_URL}/api/v1/cra/${pathStr}${search ? `?${search}` : ''}` + + const init: RequestInit = { + method, + headers: { 'Content-Type': 'application/json' }, + signal: AbortSignal.timeout(30000), + } + if (method === 'POST') { + try { + init.body = JSON.stringify(await request.json()) + } catch { + init.body = '{}' + } + } + + try { + const response = await fetch(url, init) + const text = await response.text() + if (!response.ok) { + return NextResponse.json( + { error: `Backend Error: ${response.status}`, details: text }, + { status: response.status }, + ) + } + return new NextResponse(text, { + status: response.status, + headers: { 'Content-Type': 'application/json' }, + }) + } catch (error) { + console.error('CRA API proxy error:', error) + return NextResponse.json({ error: 'Verbindung zum Backend fehlgeschlagen' }, { status: 503 }) + } +} + +export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { + const { path } = await params + return forward(request, path, 'GET') +} + +export async function POST(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { + const { path } = await params + return forward(request, path, 'POST') +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts new file mode 100644 index 00000000..c9f0188a --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts @@ -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 = {} + 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(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 } +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRADemo.ts b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRADemo.ts index d789313e..faae5720 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRADemo.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRADemo.ts @@ -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 } -} diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx index 3af75447..316edb21 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx @@ -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 + const { data, live } = useCRA() + if (!data) { + return

CRA-Risikobeurteilung wird geladen …

+ } + return ( +
+ {!live && ( +

+ Backend nicht erreichbar — statisches Szenario angezeigt. +

+ )} + +
+ ) }