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 d202d4c9..6e2b1be7 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/_components/CRACyberView.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/CRACyberView.tsx @@ -1,7 +1,7 @@ 'use client' import { Fragment, useState } from 'react' -import { CRADemo, CRAFinding } from '../_hooks/useCRADemo' +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', @@ -59,7 +59,7 @@ function EvidenceTag({ et }: { et?: string }) { ) } -function FindingsTable({ findings }: { findings: CRAFinding[] }) { +function FindingsTable({ findings, measuresById }: { findings: CRAFinding[]; measuresById: Record }) { const [open, setOpen] = useState>({}) const toggle = (id: string) => setOpen((o) => ({ ...o, [id]: !o[id] })) return ( @@ -101,50 +101,79 @@ function FindingsTable({ findings }: { findings: CRAFinding[] }) { onClick={() => toggle(f.id)} className="text-[11px] text-purple-600 hover:underline whitespace-nowrap" > - NIST/OWASP {open[f.id] ? '▲' : '▼'} + Standard & Maßnahmen {open[f.id] ? '▲' : '▼'} {open[f.id] && ( - -

Best-Practice-Tiefe (Golden-Set-Crosswalk)

-
- 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} - ))} - - )} + + {/* 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} + ))} + + )} +
- {f.regulatory_breadth && f.regulatory_breadth.length > 0 && ( -
-

- Regulatorische Breite{f.sub_topic ? ` — ${f.sub_topic}` : ''} (CRA + NIST/OWASP/ENISA-Quellen) -

+ + {/* 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 && ( - {c.use_case} - )} + {c.use_case || 'Option'} {c.control_id} {c.title} - · {c.source_regulation} + · {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. +

)} @@ -157,6 +186,9 @@ function FindingsTable({ findings }: { findings: CRAFinding[] }) { } 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 */} @@ -224,8 +256,15 @@ export function CRACyberView({ data }: { data: CRADemo }) {

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) */} @@ -247,33 +286,6 @@ export function CRACyberView({ data }: { data: CRADemo }) {
)} - {/* Recommended measures — full curated text + norm references */} -
-

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) => ( -
-

- {me.id} — {me.name} -

-

{me.description}

-
- {me.norm_refs.map((nr) => ( - - {nr} - - ))} -
-
- ))} -
-
- {/* CRA deadlines */}

CRA-Fristen

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 55ddcd33..edb22d91 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRADemo.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRADemo.ts @@ -30,7 +30,7 @@ export interface CRAFinding { evidence_type?: string // code | process | hybrid | document — drives the remediation-class badge // regulatory breadth (atom-grain shared Controls-API: cra + code/network_security), live only sub_topic?: string - regulatory_breadth?: { control_id: string; title: string; source_regulation: string; severity?: string; use_case?: string }[] + regulatory_breadth?: { control_id: string; title: string; source_regulation: string; source_article?: string; severity?: string; use_case?: string }[] // priority layer (set live by the backend prioritizer; optional in the static fallback) priority_tier?: string priority_score?: number diff --git a/backend-compliance/compliance/services/cra_use_case_controls.py b/backend-compliance/compliance/services/cra_use_case_controls.py index 03910ca0..6bfcde22 100644 --- a/backend-compliance/compliance/services/cra_use_case_controls.py +++ b/backend-compliance/compliance/services/cra_use_case_controls.py @@ -76,6 +76,7 @@ def enrich_findings_with_breadth(mapped: list, db, per_use_case: int = 3) -> Non cache[key] = [ {"control_id": c.get("control_id"), "title": c.get("title"), "source_regulation": c.get("source_regulation"), + "source_article": c.get("source_article"), "severity": c.get("severity"), "use_case": uc} for c in res.get("controls", []) ] diff --git a/backend-compliance/compliance/services/use_case_controls.py b/backend-compliance/compliance/services/use_case_controls.py index 6db8efb9..a9695a08 100644 --- a/backend-compliance/compliance/services/use_case_controls.py +++ b/backend-compliance/compliance/services/use_case_controls.py @@ -78,10 +78,14 @@ _LIST_SQL = text(""" _ATOM_LIST_SQL = text(""" SELECT ac.control_uuid, ac.sub_topic, ac.canonical_obligation, cc.control_id, cc.title, cc.objective, cc.severity, - (SELECT cpl.source_regulation FROM control_parent_links cpl - WHERE cpl.control_uuid = ac.control_uuid LIMIT 1) AS source_regulation + cpl.source_regulation, cpl.source_article FROM atom_classification ac JOIN canonical_controls cc ON cc.id = ac.control_uuid + LEFT JOIN LATERAL ( + SELECT cpl.source_regulation, cpl.source_article + FROM control_parent_links cpl + WHERE cpl.control_uuid = ac.control_uuid LIMIT 1 + ) cpl ON true WHERE ac.use_case = :uc AND ac.relevant = true AND (:sub IS NULL OR ac.sub_topic = :sub) ORDER BY ac.sub_topic NULLS LAST, @@ -228,6 +232,7 @@ class UseCaseControlsService: "sub_topic": r.sub_topic, "canonical_obligation": r.canonical_obligation, "source_regulation": r.source_regulation, + "source_article": r.source_article, } for r in rows ] diff --git a/backend-compliance/tests/test_cra_use_case_controls.py b/backend-compliance/tests/test_cra_use_case_controls.py index 31ec396d..984fc5dc 100644 --- a/backend-compliance/tests/test_cra_use_case_controls.py +++ b/backend-compliance/tests/test_cra_use_case_controls.py @@ -1,5 +1,9 @@ """Pin the CRA-AI -> network_security sub_topic map (DB enrichment verified live).""" -from compliance.services.cra_use_case_controls import subtopic_for +from compliance.services import cra_use_case_controls +from compliance.services.cra_use_case_controls import ( + enrich_findings_with_breadth, + subtopic_for, +) from compliance.api.cra_annex_i_data import ANNEX_I_REQUIREMENTS # Exact atom-grain sub_topic keys (verified against the live atom_classification). @@ -14,3 +18,32 @@ def test_every_requirement_maps_to_a_valid_subtopic(): for req in ANNEX_I_REQUIREMENTS: st = subtopic_for(req["req_id"]) assert st in _VALID, "{} -> {}".format(req["req_id"], st) + + +class _FakeControlsService: + """Stands in for UseCaseControlsService: returns one atom control per call, + carrying the legal anchor (source_article) the real atom query now selects.""" + + def __init__(self, db): + pass + + def controls_for_use_case(self, use_case, sub_topic=None, limit=3): + return {"controls": [{ + "control_id": "AI-{}-{}".format(use_case, sub_topic), + "title": "Test obligation", + "source_regulation": "Cyber Resilience Act (CRA)", + "source_article": "Artikel 13", + "severity": "high", + }]} + + +def test_breadth_carries_source_article(monkeypatch): + monkeypatch.setattr( + cra_use_case_controls, "UseCaseControlsService", _FakeControlsService, + ) + mapped = [{"primary_requirement": "CRA-AI-8"}] # -> authentication sub_topic + enrich_findings_with_breadth(mapped, db=None) + breadth = mapped[0]["regulatory_breadth"] + assert breadth, "expected breadth controls" + assert all("source_article" in c for c in breadth) + assert any(c["source_article"] == "Artikel 13" for c in breadth)