From e1f89f6226cfba0887d3600bd484752c0f777ff2 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Mon, 15 Jun 2026 00:48:53 +0200 Subject: [PATCH] =?UTF-8?q?feat(cra):=20CRA/Cyber-Tab=20in=203=20Zielgrupp?= =?UTF-8?q?en-Ebenen=20+=20Br=C3=BCcke=20/sdk/cra?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend-Reorganisation (kein Datenmodell-Umbau): - Ebene 1 (Management): CRA-Readiness, offene Risiken (Klartext Kritisch/Hoch/..), Handlungsaufwand nach Evidenz-Typ, betroffene Vorschriften, Top-Risiken, Fristen. - Ebene 2 (Safety × Cyber): "Cyber öffnet CE-Gefährdung erneut" als Hero (USP). - Ebene 3 (Technik): Befund-Tabelle einklappbar, interne IDs (CRA-AI-x/CWE/NIST/ OWASP/ISO) nur im Detail, Maßnahmen-Namen statt M-IDs, größere Schrift. - Brücke: IACE-CRA-Tab ↔ /sdk/cra (Readiness-Check) beidseitig verlinkt. - CRACyberView in Unterkomponenten gesplittet (LOC < 300). scripts/qa/poc_cra_article_assign.py: PoC Artikel/Absatz-Zuordnung (Pfad B2b, zurückgestellt — nicht MVP). Co-Authored-By: Claude Opus 4.7 --- admin-compliance/app/sdk/cra/page.tsx | 14 + .../cra/_components/CRACyberView.tsx | 317 ++---------------- .../cra/_components/CyberSafetyHero.tsx | 57 ++++ .../cra/_components/ManagementSummary.tsx | 158 +++++++++ .../cra/_components/TechFindings.tsx | 162 +++++++++ .../cra/_components/cra-badges.tsx | 58 ++++ .../app/sdk/iace/[projectId]/cra/page.tsx | 10 +- scripts/qa/poc_cra_article_assign.py | 141 ++++++++ 8 files changed, 620 insertions(+), 297 deletions(-) create mode 100644 admin-compliance/app/sdk/iace/[projectId]/cra/_components/CyberSafetyHero.tsx create mode 100644 admin-compliance/app/sdk/iace/[projectId]/cra/_components/ManagementSummary.tsx create mode 100644 admin-compliance/app/sdk/iace/[projectId]/cra/_components/TechFindings.tsx create mode 100644 admin-compliance/app/sdk/iace/[projectId]/cra/_components/cra-badges.tsx create mode 100644 scripts/qa/poc_cra_article_assign.py diff --git a/admin-compliance/app/sdk/cra/page.tsx b/admin-compliance/app/sdk/cra/page.tsx index 41366f96..46314737 100644 --- a/admin-compliance/app/sdk/cra/page.tsx +++ b/admin-compliance/app/sdk/cra/page.tsx @@ -102,6 +102,20 @@ export default function CRAProjectsPage() { setShowModal(true)} /> + {/* Bridge: vom Readiness-Check in die kombinierte CE × Cyber-Analyse */} + +

+ Cyber trifft Safety — kombinierte CE × Cyber-Analyse ansehen +

+

+ Am Beispielprojekt „Kistenhubgerät": wo ein Cyber-Risiko eine bereits gemilderte + CE-Gefährdung wieder öffnet — mit Management-Übersicht, Risiken und Maßnahmen. → +

+
+
Quellen & Lizenz: 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 6e2b1be7..ac8fffad 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/_components/CRACyberView.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/CRACyberView.tsx @@ -1,312 +1,37 @@ 'use client' -import { Fragment, useState } from 'react' -import { CRADemo, CRAFinding, Measure } from '../_hooks/useCRADemo' - -const RISK_BADGE: Record = { - CRITICAL: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300', - HIGH: 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300', - MEDIUM: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300', - LOW: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300', -} - -function RiskBadge({ level }: { level: string }) { - return ( - - {level} - - ) -} - -const TIER_BADGE: Record = { - P0: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300', - P1: 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300', - P2: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300', - P3: 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-300', -} - -function TierBadge({ tier, reason }: { tier?: string; reason?: string }) { - if (!tier) return - return ( - - {tier} - - ) -} - -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, measuresById }: { findings: CRAFinding[]; measuresById: Record }) { - const [open, setOpen] = useState>({}) - const toggle = (id: string) => setOpen((o) => ({ ...o, [id]: !o[id] })) - return ( -
- - - - - - - - - - - - - {findings.map((f) => ( - - - - - - - - - - {open[f.id] && ( - - - - )} - - ))} - -
PrioCyber-BefundCRA-AnforderungRisikoMaßnahmenBest Practice
-
{f.title}
-
{f.id} · {f.cwe} · {f.location}
-
-
- {f.primary_requirement} {f.requirement_title} - {f.requirement_ids.length > 1 && ( - +{f.requirement_ids.length - 1} - )} -
{f.annex_anchor}
-
- {f.measures.length ? f.measures.join(', ') : } - - -
- {/* Best-Practice-Standard — der Maßstab (kein Code-Rezept) */} -
-

- Best-Practice-Standard - {' '}— woran „erfüllt" gemessen wird (Kontroll-Frameworks, noch kein Code-Rezept): -

-
- NIST 800-53: - {f.nist_refs.map((n) => ( - {n} - ))} - OWASP: - {f.owasp_refs.map((o) => ( - {o.code} · {o.label} - ))} - {f.iso27001_ref.length > 0 && ( - <> - ISO 27001: - {f.iso27001_ref.map((iso) => ( - {iso} - ))} - - )} -
-
- - {/* Maßnahmen (wählbar) — kuratierte Kern-Maßnahme + belegte Optionen, zusammengeführt */} -
-

- Maßnahmen (wählbar) - {' '}— passend kombinieren, nicht alle abhaken. Das Risiko ist geschlossen, wenn die Pflicht real erfüllt ist. -

- {f.measures.length > 0 && ( -
    - {f.measures.map((mid) => { - const m = measuresById[mid] - return ( -
  • - kuratiert - {m ? m.name : mid} - {m && m.description ? — {m.description} : null} - {m && m.norm_refs && m.norm_refs.length > 0 ? · {m.norm_refs.join(', ')} : null} -
  • - ) - })} -
- )} - {f.regulatory_breadth && f.regulatory_breadth.length > 0 && ( -
    - {f.regulatory_breadth.map((c) => ( -
  • - {c.use_case || 'Option'} - {c.control_id} {c.title} - · {c.source_regulation}{c.source_article ? `, ${c.source_article}` : ''} -
  • - ))} -
- )} - {f.measures.length === 0 && (!f.regulatory_breadth || f.regulatory_breadth.length === 0) && ( -

Keine kuratierte Maßnahme hinterlegt — Standard (oben) + Code-Fix aus dem Scan nutzen.

- )} -
- -

- Konkreter Code-Fix (Patch, z. B. Verschlüsselungsverfahren/Schlüssel) folgt aus dem Repo-Scan, sobald das Repository angebunden ist. -

-
-
- ) -} +import { CRADemo, Measure } from '../_hooks/useCRADemo' +import { ManagementSummary } from './ManagementSummary' +import { CyberSafetyHero } from './CyberSafetyHero' +import { TechFindings } from './TechFindings' export function CRACyberView({ data }: { data: CRADemo }) { const measuresById: Record = Object.fromEntries( data.open_measures.map((m) => [m.id, m]), ) + return ( -
+
{/* Co-Pilot framing — advisory, not alarmist */} -
-

CRA / Cyber-Risiko

-

{data.scenario}

-

- Wir verknüpfen die Cyber-Befunde Ihres Repo-Scans mit den CRA-Anforderungen (Annex I) und mit Ihrer - bestehenden CE-Risikobeurteilung. Die Punkte sind Handlungsfelder zur gemeinsamen Klärung mit DSB/Anwalt — - keine automatische Verstoßfeststellung. Demo: erfundene Findings, echtes CRA-Mapping. +

+

CRA / Cyber-Risiko

+

{data.scenario}

+

+ Wir verknüpfen die Cyber-Befunde Ihres Repo-Scans mit den CRA-Anforderungen und mit Ihrer + bestehenden CE-Risikobeurteilung. Die Punkte sind Handlungsfelder zur gemeinsamen Klärung mit + DSB/Anwalt — keine automatische Verstoßfeststellung.{' '} + Demo: erfundene Befunde, echtes CRA-Mapping.

-
+ - {/* Summary tiles */} -
- - - -
-

Risiko-Verteilung

-
- {(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] as const).map((lvl) => - data.by_risk[lvl] ? ( - - {data.by_risk[lvl]} {lvl} - - ) : null, - )} -
-
-
+ {/* Ebene 1 — Management: Risiko & Aufwand auf einen Blick */} + - {/* Cyber meets Safety — the core integration idea */} -
-
-

Cyber trifft Safety

-

- Wo ein Cyber-Risiko eine bereits mechanisch gemilderte Gefährdung Ihrer - CE-Risikobeurteilung wieder öffnet (CRA × Maschinen-VO). -

-
-
- {data.cross_links.map((cl, i) => ( -
-

{cl.safety_hazard}

-

{cl.safety_ref}

-
-
- Bisherige Maßnahme: {cl.original_measure} -
-
- Cyber-Befunde: {cl.cyber_finding_ids.join(', ')} -
-
-

{cl.cyber_breaks_it}

- - Restrisiko: {cl.residual} - -
- ))} -
-
+ {/* Ebene 2 — Safety × Cyber: das Alleinstellungsmerkmal */} + - {/* Findings -> CRA requirement */} -
-
-

Befunde → CRA-Anforderung

-

- So liest du einen Befund: Cyber-Befund (was der Scan sah) - {' → '}CRA-Anforderung (was das Gesetz verlangt) - {' → '}Best-Practice-Standard (woran „erfüllt" gemessen wird) - {' → '}Maßnahmen (mögliche Umsetzungen — passend wählen, nicht alle). - Klick „Standard & Maßnahmen" für die Details je Befund. -

-
- -
- - {/* Quick wins — high impact, low effort (second view) */} - {data.findings.some((f) => f.quick_win) && ( -
-

Quick Wins

-

Hohe Wirkung bei geringem Aufwand — gut für den Einstieg.

-
    - {data.findings.filter((f) => f.quick_win).map((f) => ( -
  • - - - {f.title} → {f.primary_requirement} - {f.measures.length > 0 && · {f.measures.join(', ')}} - -
  • - ))} -
-
- )} - - {/* CRA deadlines */} -
-

CRA-Fristen

-
    - {data.deadlines.map((d) => ( -
  • - {d.date} {d.label} -
  • - ))} -
-
-
- ) -} - -function Tile({ label, value, sub }: { label: string; value: string; sub?: string }) { - return ( -
-

{label}

-

{value}

- {sub &&

{sub}

} + {/* Ebene 3 — Technik: Controls, Standards, Maßnahmen (einklappbar) */} +
) } diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/_components/CyberSafetyHero.tsx b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/CyberSafetyHero.tsx new file mode 100644 index 00000000..b3b8a06c --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/CyberSafetyHero.tsx @@ -0,0 +1,57 @@ +'use client' + +import { CRADemo } from '../_hooks/useCRADemo' + +export function CyberSafetyHero({ data }: { data: CRADemo }) { + if (!data.cross_links.length) return null + return ( +
+
+

+ Cyber öffnet eine bestehende CE-Gefährdung erneut +

+

+ Wo ein Cyber-Risiko eine in Ihrer CE-Risikobeurteilung bereits{' '} + mechanisch gemilderte Gefährdung wieder aufreißt + — der Schnittpunkt von Maschinenverordnung und Cyber Resilience Act. +

+
+
+ {data.cross_links.map((cl, i) => ( +
+

{cl.safety_hazard}

+

{cl.safety_ref}

+ +
+ + CE: gemindert + + + + Cyber: wieder offen + +
+ +
+
+ Bisherige Schutzmaßnahme:
+ {cl.original_measure} +
+
+ Auslösende Cyber-Befunde:
+ {cl.cyber_finding_ids.join(', ')} +
+
+ +

+ Warum: {cl.cyber_breaks_it} +

+ + Restrisiko: {cl.residual} + +
+ ))} +
+
+ ) +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/_components/ManagementSummary.tsx b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/ManagementSummary.tsx new file mode 100644 index 00000000..c89115f8 --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/ManagementSummary.tsx @@ -0,0 +1,158 @@ +'use client' + +import { CRADemo, CRAFinding, Measure } from '../_hooks/useCRADemo' +import { RISK_BADGE, RISK_LABEL } from './cra-badges' + +const TOTAL_ANNEX_I = 40 // CRA Annex I — Anzahl grundlegender Anforderungen +const EFFORT_LABEL: Record = { + code: 'Entwicklung (Code)', hybrid: 'Entwicklung + Prozess', + process: 'Prozess / Organisation', document: 'Dokumentation', +} + +function measureNames(ids: string[], byId: Record): string[] { + return ids.map((m) => byId[m]?.name || m) +} + +// Top-Risiken in Klartext: zuerst die Cyber-trifft-Safety-Fälle (höchste Wirkung), +// danach mit den höchstpriorisierten Einzelbefunden auffüllen. +function topRisks(data: CRADemo, byId: Record) { + const fById: Record = Object.fromEntries(data.findings.map((f) => [f.id, f])) + const risks: { title: string; impact: string; systems: string[]; measures: string[]; level: string }[] = [] + for (const cl of data.cross_links) { + const fs = cl.cyber_finding_ids.map((id) => fById[id]).filter(Boolean) + risks.push({ + title: cl.safety_hazard, + impact: cl.cyber_breaks_it, + systems: Array.from(new Set(fs.map((f) => f.location).filter(Boolean))), + measures: measureNames(Array.from(new Set(fs.flatMap((f) => f.measures))), byId), + level: 'CRITICAL', + }) + } + const usedIds = new Set(data.cross_links.flatMap((c) => c.cyber_finding_ids)) + const rest = data.findings + .filter((f) => !usedIds.has(f.id) && (f.priority_tier === 'P0' || f.priority_tier === 'P1')) + .sort((a, b) => (b.priority_score || 0) - (a.priority_score || 0)) + for (const f of rest) { + if (risks.length >= 4) break + risks.push({ + title: f.requirement_title || f.title, + impact: f.title, + systems: f.location ? [f.location] : [], + measures: measureNames(f.measures, byId), + level: f.risk_level, + }) + } + return risks.slice(0, 4) +} + +export function ManagementSummary({ data, measuresById }: { data: CRADemo; measuresById: Record }) { + const readiness = Math.round( + (100 * Math.max(0, TOTAL_ANNEX_I - data.requirements_touched.length)) / TOTAL_ANNEX_I, + ) + const effort: Record = {} + for (const f of data.findings) { + const k = f.evidence_type || 'process' + effort[k] = (effort[k] || 0) + 1 + } + const regulations = Array.from(new Set([ + 'Cyber Resilience Act (CRA)', + 'Maschinenverordnung (EU) 2023/1230', + ...data.findings.flatMap((f) => (f.regulatory_breadth || []).map((c) => c.source_regulation)), + ].filter(Boolean))).slice(0, 6) + const risks = topRisks(data, measuresById) + + return ( +
+
+ {/* Readiness */} +
+

CRA-Readiness

+

{readiness}%

+

+ Anteil der {TOTAL_ANNEX_I} CRA-Anforderungen ohne offenen Befund (indikativ) +

+
+ {/* Offene Risiken */} +
+

Offene Risiken

+
+ {(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] as const).map((lvl) => + data.by_risk[lvl] ? ( +
+ + {data.by_risk[lvl]} + + {RISK_LABEL[lvl]} +
+ ) : null, + )} +
+
+ {/* Handlungsaufwand */} +
+

Handlungsaufwand

+

{data.findings.length}

+

offene Befunde

+
    + {Object.entries(effort).map(([k, n]) => ( +
  • + {n}× {EFFORT_LABEL[k] || k} +
  • + ))} +
+
+ {/* Betroffene Vorschriften */} +
+

Betroffene Vorschriften

+
    + {regulations.map((r) => ( +
  • + {r} +
  • + ))} +
+
+
+ + {/* Top-Risiken in Klartext */} +
+

Wichtigste Risiken

+
+ {risks.map((r, i) => ( +
+
+ + {RISK_LABEL[r.level] || r.level} + + {r.title} +
+

{r.impact}

+ {r.systems.length > 0 && ( +

+ Betroffene Systeme: {r.systems.join(', ')} +

+ )} + {r.measures.length > 0 && ( +

+ Empfohlene Maßnahmen: {r.measures.join(' · ')} +

+ )} +
+ ))} +
+
+ + {/* CRA-Fristen */} +
+

CRA-Fristen

+
    + {data.deadlines.map((d) => ( +
  • + {d.date} {d.label} +
  • + ))} +
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/_components/TechFindings.tsx b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/TechFindings.tsx new file mode 100644 index 00000000..db2b31b8 --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/TechFindings.tsx @@ -0,0 +1,162 @@ +'use client' + +import { Fragment, useState } from 'react' +import { CRADemo, CRAFinding, Measure } from '../_hooks/useCRADemo' +import { EvidenceTag, RiskBadge, TierBadge } from './cra-badges' + +function FindingDetail({ f, measuresById }: { f: CRAFinding; measuresById: Record }) { + return ( + + {/* Best-Practice-Standard — der Maßstab (kein Code-Rezept) */} +
+

+ Best-Practice-Standard — woran „erfüllt" gemessen wird +

+
+ Gesetz: + + {f.primary_requirement} · {f.annex_anchor} + + NIST: + {f.nist_refs.map((n) => ( + {n} + ))} + OWASP: + {f.owasp_refs.map((o) => ( + {o.code} · {o.label} + ))} + {f.iso27001_ref.length > 0 && ( + <> + ISO 27001: + {f.iso27001_ref.map((iso) => ( + {iso} + ))} + + )} +
+
+ + {/* Maßnahmen (wählbar) — kuratiert + belegte Optionen */} +
+

+ Maßnahmen (wählbar — passend kombinieren, nicht alle abhaken) +

+ {f.measures.length > 0 && ( +
    + {f.measures.map((mid) => { + const m = measuresById[mid] + return ( +
  • + {m ? m.name : mid} + {m?.description ? — {m.description} : null} + {m?.norm_refs?.length ? · {m.norm_refs.join(', ')} : null} +
  • + ) + })} +
+ )} + {f.regulatory_breadth && f.regulatory_breadth.length > 0 && ( +
    + {f.regulatory_breadth.map((c) => ( +
  • + {c.use_case || 'Option'} + {c.title} · {c.source_regulation} +
  • + ))} +
+ )} +
+ +

+ Befund-Kennung {f.id} · {f.cwe}. Konkreter Code-Fix folgt aus dem Repo-Scan, sobald das Repository angebunden ist. +

+ + ) +} + +export function TechFindings({ data, measuresById }: { data: CRADemo; measuresById: Record }) { + const [show, setShow] = useState(false) + const [open, setOpen] = useState>({}) + const toggle = (id: string) => setOpen((o) => ({ ...o, [id]: !o[id] })) + const mNames = (ids: string[]) => ids.map((m) => measuresById[m]?.name || m) + + return ( +
+ + + {show && ( +
+ + + + + + + + + + + + {data.findings.map((f) => ( + + + + + + + + + {open[f.id] && ( + + + + )} + + ))} + +
PrioCyber-Befund & PflichtRisikoMaßnahmenDetails
+
{f.title}
+
Pflicht: {f.requirement_title}
+
+
+ {f.measures.length ? mNames(f.measures).join(' · ') : } + + +
+ + {data.findings.some((f) => f.quick_win) && ( +
+

Quick Wins

+

Hohe Wirkung bei geringem Aufwand — gut für den Einstieg.

+
    + {data.findings.filter((f) => f.quick_win).map((f) => ( +
  • + + {f.title} + {f.measures.length > 0 && · {mNames(f.measures).join(' · ')}} + +
  • + ))} +
+
+ )} +
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/_components/cra-badges.tsx b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/cra-badges.tsx new file mode 100644 index 00000000..b1e71a7b --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/cra-badges.tsx @@ -0,0 +1,58 @@ +'use client' + +export const RISK_BADGE: Record = { + CRITICAL: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300', + HIGH: 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300', + MEDIUM: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300', + LOW: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300', +} + +export const RISK_LABEL: Record = { + CRITICAL: 'Kritisch', HIGH: 'Hoch', MEDIUM: 'Mittel', LOW: 'Niedrig', +} + +export function RiskBadge({ level }: { level: string }) { + return ( + + {RISK_LABEL[level] || level} + + ) +} + +const TIER_BADGE: Record = { + P0: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300', + P1: 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300', + P2: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300', + P3: 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-300', +} + +export function TierBadge({ tier, reason }: { tier?: string; reason?: string }) { + if (!tier) return + return ( + + {tier} + + ) +} + +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. +export function EvidenceTag({ et }: { et?: string }) { + if (!et || !EVIDENCE_LABEL[et]) return null + const codeish = et === 'code' || et === 'hybrid' + return ( + + {EVIDENCE_LABEL[et]} + + ) +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx index 757d7c4c..456e8822 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx @@ -15,8 +15,16 @@ export default function CRAPage() { } return (
+
+ + Projektgebundene CE × Cyber-Analyse + + + Allgemeiner CRA-Readiness-Check → + +
{!live && ( -

+

Backend nicht erreichbar — statisches Szenario angezeigt.

)} diff --git a/scripts/qa/poc_cra_article_assign.py b/scripts/qa/poc_cra_article_assign.py new file mode 100644 index 00000000..089f9512 --- /dev/null +++ b/scripts/qa/poc_cra_article_assign.py @@ -0,0 +1,141 @@ +"""PoC: Artikel/Absatz-Zuordnung für CRA-Controls (Pfad B2b). + +Pro Control: semantische Suche (Go-SDK /rag/search, nomic-embed + Qdrant) holt +die besten artikel-getaggten CRA-Chunks; Haiku wählt die passende Fundstelle und +gibt {article, paragraph, confidence}. Schreibt NICHTS in die DB — nur Report zur +Validierung (50er-Stichprobe). Lauf: + + ssh macmini 'docker exec -i -e POC_N=8 bp-compliance-backend python3 -' \ + < scripts/qa/poc_cra_article_assign.py +""" +import json +import os +import re + +import httpx +from sqlalchemy import create_engine, text + +N = int(os.environ.get("POC_N", "50")) +DB = os.environ.get("COMPLIANCE_DATABASE_URL") or os.environ["DATABASE_URL"] +SDK = os.environ.get("SDK_URL", "http://ai-compliance-sdk:8090") +AKEY = (os.environ.get("ANTHROPIC_API_KEY") or "").strip() +MODEL = os.environ.get("POC_HAIKU_MODEL", "claude-haiku-4-5-20251001") + +_MARKER = re.compile(r"^\[([^\]]+)\]") +_BINDING = ("artikel", "anhang", "annex", "art.", "teil") +_JSON = re.compile(r"\{.*\}", re.DOTALL) + + +def is_binding(marker: str) -> bool: + m = marker.lower() + return any(k in m for k in _BINDING) + + +def retrieve(query: str) -> list: + """Top binding (article/annex) CRA chunks for the control text.""" + try: + r = httpx.post( + f"{SDK}/sdk/v1/rag/search", + json={"query": query, "collection": "bp_compliance_ce", + "top_k": 12, "regulations": ["cra_2024"]}, + timeout=20.0, + ) + res = r.json().get("results", []) + except Exception as e: # noqa: BLE001 + return [{"_err": str(e)}] + cands = [] + for x in res: + t = x.get("text") or "" + m = _MARKER.match(t) + if m and is_binding(m.group(1)): + cands.append({"marker": m.group(1).strip(), + "text": t[:400], "score": x.get("score", 0.0)}) + if len(cands) >= 3: + break + return cands + + +def haiku(control_text: str, cands: list) -> dict: + block = "\n".join( + f"[{i+1}] {c['marker']}: {c['text'][:300]}" for i, c in enumerate(cands) + ) + prompt = ( + "Eine CRA-Compliance-Pflicht soll der korrekten Fundstelle im Cyber " + "Resilience Act (Verordnung (EU) 2024/2847) zugeordnet werden.\n\n" + f"PFLICHT:\n{control_text}\n\n" + f"KANDIDATEN-FUNDSTELLEN (aus dem CRA-Volltext):\n{block}\n\n" + "Wähle die Fundstelle, die die Pflicht am genauesten verankert. " + "Antworte NUR mit JSON: " + '{"article":"Artikel N|Anhang X","paragraph":"Absatz N|","candidate":N,' + '"confidence":0.0}. Wenn keine passt: ' + '{"article":"","paragraph":"","candidate":0,"confidence":0.0}' + ) + r = httpx.post( + "https://api.anthropic.com/v1/messages", + headers={"x-api-key": AKEY, "anthropic-version": "2023-06-01", + "content-type": "application/json"}, + json={"model": MODEL, "max_tokens": 200, + "messages": [{"role": "user", "content": prompt}]}, + timeout=60.0, + ) + data = r.json() + if "content" not in data: + return {"_err": str(data)[:200]} + m = _JSON.search(data["content"][0]["text"]) + return json.loads(m.group(0)) if m else {"_err": "no json"} + + +def main() -> None: + eng = create_engine(DB) + with eng.connect() as c: + c.execute(text("SET search_path TO compliance, core, public")) + rows = c.execute(text(""" + SELECT cc.id::text uid, cc.control_id, + trim(coalesce(cc.title,'') || '. ' || coalesce(cc.objective,'')) ctext, + cpl.source_article existing + FROM atom_classification ac + JOIN canonical_controls cc ON cc.id = ac.control_uuid + JOIN control_parent_links cpl ON cpl.control_uuid = ac.control_uuid + WHERE ac.use_case = 'cra' AND ac.relevant = true + ORDER BY md5(cc.control_id) + LIMIT :n + """), {"n": N}).fetchall() + + print(f"PoC CRA Artikel-Zuordnung — {len(rows)} Controls, Modell {MODEL}\n") + n_assigned = n_conf = n_changed = n_nocand = 0 + for row in rows: + cands = retrieve(row.ctext or row.control_id) + if cands and cands[0].get("_err"): + print(f"[{row.control_id}] RAG-ERR {cands[0]['_err'][:80]}") + continue + if not cands: + n_nocand += 1 + print(f"[{row.control_id}] alt={row.existing!r:30} → KEINE Artikel-Kandidaten") + continue + v = haiku(row.ctext, cands) + if v.get("_err"): + print(f"[{row.control_id}] HAIKU-ERR {v['_err'][:80]}") + continue + art = v.get("article", "") + para = v.get("paragraph", "") + conf = v.get("confidence", 0.0) + if art: + n_assigned += 1 + if conf >= 0.7: + n_conf += 1 + if art and art.lower().replace("artikel", "art").strip() not in (row.existing or "").lower(): + n_changed += 1 + newref = f"{art}{(' ' + para) if para else ''}" + print(f"[{row.control_id}] conf={conf:.2f} NEU={newref!r:24} ALT={row.existing!r:30} " + f"| top-cand={cands[0]['marker'][:18]!r}") + print(f" pflicht: {(row.ctext or '')[:95]}") + + print(f"\n--- Summe ({len(rows)}) ---") + print(f" Artikel zugeordnet : {n_assigned}") + print(f" confidence >= 0.70 : {n_conf}") + print(f" abweichend von ALT : {n_changed}") + print(f" keine Kandidaten : {n_nocand}") + + +if __name__ == "__main__": + main()