diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/_components/CRACyberView.tsx b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/CRACyberView.tsx index 7512a94a..211add54 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/_components/CRACyberView.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/CRACyberView.tsx @@ -34,6 +34,31 @@ function TierBadge({ tier, reason }: { tier?: string; reason?: string }) { ) } +const EVIDENCE_LABEL: Record = { + code: 'Code-nah', + hybrid: 'Code + Prozess', + process: 'Prozess', + document: 'Dokumentation', +} + +// "Code-nah" = der Scan kann es im Quellcode verorten → Code-Fix im Ticket möglich. +// Sonst = Prozess/Organisation: wir benennen den Sollzustand, kein Auto-Fix. +function EvidenceTag({ et }: { et?: string }) { + if (!et || !EVIDENCE_LABEL[et]) return null + const codeish = et === 'code' || et === 'hybrid' + return ( + + {EVIDENCE_LABEL[et]} + + ) +} + function FindingsTable({ findings }: { findings: CRAFinding[] }) { const [open, setOpen] = useState>({}) const toggle = (id: string) => setOpen((o) => ({ ...o, [id]: !o[id] })) @@ -58,6 +83,7 @@ function FindingsTable({ findings }: { findings: CRAFinding[] }) {
{f.title}
{f.id} · {f.cwe} · {f.location}
+
{f.primary_requirement} {f.requirement_title} @@ -205,8 +231,12 @@ export function CRACyberView({ data }: { data: CRADemo }) { {/* Recommended measures — full curated text + norm references */}
-

Empfohlene Maßnahmen

-

Kuratierte CRA-Maßnahmen aus der BreakPilot-Bibliothek — mit Normverweisen.

+

Empfohlene Maßnahmen (Sollzustand)

+

+ Kuratierte CRA-Maßnahmen mit Normverweisen — sie beschreiben den umzubauenden Prozess / das Sollziel, + kein Auto-Fix. Konkrete Code-Fixes entstehen separat, wenn der Repo-Scan ein Source-Code-Risiko an einer + Stelle sieht (Findings mit „Code-nah"). +

{data.open_measures.map((me) => (
diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/_components/SnapshotPanel.tsx b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/SnapshotPanel.tsx new file mode 100644 index 00000000..252c87f5 --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/SnapshotPanel.tsx @@ -0,0 +1,73 @@ +'use client' + +import { useState } from 'react' +import { SnapshotMeta } from '../_hooks/useCRA' + +export function SnapshotPanel({ snapshots, onSave, onView }: { + snapshots: SnapshotMeta[] + onSave: () => Promise + onView: (id: string) => Promise +}) { + const [saving, setSaving] = useState(false) + const [detail, setDetail] = useState<{ version: number; content_md: string } | null>(null) + + const save = async () => { + setSaving(true) + try { await onSave() } finally { setSaving(false) } + } + const view = async (id: string) => { + const d = await onView(id) + if (d) setDetail({ version: d.version, content_md: d.content_md }) + } + + return ( +
+
+
+

Verlauf / Snapshots

+

+ Versionierte CRA-Risikobeurteilung über die Zeit — die fortlaufende Dokumentation (CRA Art. 13). +

+
+ +
+ + {snapshots.length === 0 ? ( +

Noch kein Snapshot gespeichert.

+ ) : ( +
    + {snapshots.map((s) => ( +
  • + + v{s.version} + {s.status} + {new Date(s.generated_at).toLocaleString('de-DE')} + · {s.coverage?.covered?.length ?? 0}/{s.coverage?.total ?? 40} Anforderungen + + +
  • + ))} +
+ )} + + {detail && ( +
+
+ Snapshot v{detail.version} + +
+
{detail.content_md}
+
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts index 255e7b36..272f6297 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts @@ -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 // 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 = {} 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(null) const [live, setLive] = useState(false) const [weights, setWeights] = useState({}) + const [snapshots, setSnapshots] = useState([]) + + 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 } } 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 22e8a03f..67219211 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRADemo.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRADemo.ts @@ -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 diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx index 07cfcab7..757d7c4c 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx @@ -1,11 +1,15 @@ 'use client' +import { useParams } from 'next/navigation' import { useCRA } from './_hooks/useCRA' import { CRACyberView } from './_components/CRACyberView' import { WeightsControl } from './_components/WeightsControl' +import { SnapshotPanel } from './_components/SnapshotPanel' export default function CRAPage() { - const { data, live, weights, setWeights } = useCRA() + const params = useParams() + const projectId = params?.projectId as string | undefined + const { data, live, weights, setWeights, snapshots, saveSnapshot, viewSnapshot } = useCRA(projectId) if (!data) { return

CRA-Risikobeurteilung wird geladen …

} @@ -18,6 +22,7 @@ export default function CRAPage() { )} +
) } diff --git a/backend-compliance/compliance/services/cra_finding_mapper.py b/backend-compliance/compliance/services/cra_finding_mapper.py index 9b8d72f8..b135575c 100644 --- a/backend-compliance/compliance/services/cra_finding_mapper.py +++ b/backend-compliance/compliance/services/cra_finding_mapper.py @@ -94,6 +94,7 @@ class MappedFinding: primary_requirement: str = "" annex_anchor: str = "" iso27001_ref: list = field(default_factory=list) + evidence_type: str = "" # code | process | hybrid | document (from the requirement) risk_level: str = "LOW" measures: list = field(default_factory=list) nist_refs: list = field(default_factory=list) # NIST 800-53 control IDs (golden-set crosswalk) @@ -189,6 +190,7 @@ def map_finding(f: ScannerFinding) -> MappedFinding: primary_requirement=primary["req_id"], annex_anchor=primary.get("annex_anchor", ""), iso27001_ref=list(primary.get("iso27001_ref", [])), + evidence_type=primary.get("evidence_type", ""), risk_level=_SEV_BY_RANK.get(risk_rank, "LOW"), measures=measures, nist_refs=refs["nist"], diff --git a/backend-compliance/tests/test_cra_finding_mapper.py b/backend-compliance/tests/test_cra_finding_mapper.py index 47de765a..7ea64772 100644 --- a/backend-compliance/tests/test_cra_finding_mapper.py +++ b/backend-compliance/tests/test_cra_finding_mapper.py @@ -14,6 +14,11 @@ def test_hardcoded_credentials_cwe_maps_to_credential_requirement(): assert m.annex_anchor # spine carries the Annex anchor +def test_mapped_finding_carries_evidence_type(): + m = map_finding(ScannerFinding(id="e", title="default password", cwe="CWE-259", severity="high")) + assert m.evidence_type == "code" # CRA-AI-8 is code-checkable + + def test_default_password_is_critical_and_carries_measure_M542(): m = map_finding(ScannerFinding(id="f2", title="Universal default password", cwe="CWE-259", severity="critical")) assert m.primary_requirement == "CRA-AI-8"