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 20a32af5..745bfc2c 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/_components/CRACyberView.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/CRACyberView.tsx @@ -92,7 +92,7 @@ export function CRACyberView({ data }: { data: CRADemo }) { Cyber-Befund CRA-Anforderung - Annex I + Best Practice (NIST / OWASP) Risiko Maßnahmen @@ -109,11 +109,25 @@ export function CRACyberView({ data }: { data: CRADemo }) { {f.requirement_ids.length > 1 && ( +{f.requirement_ids.length - 1} )} +
{f.annex_anchor}
+ + +
+ {f.nist_refs.map((n) => ( + + {n} + + ))} + {f.owasp_refs.map((o) => ( + + {o.code} + + ))} +
- {f.annex_anchor} - {f.measures.length ? f.measures.map((me) => me.id).join(', ') : } + {f.measures.length ? f.measures.join(', ') : } ))} @@ -122,29 +136,40 @@ export function CRACyberView({ data }: { data: CRADemo }) { - {/* Measures + deadlines */} -
-
-

Empfohlene Maßnahmen

-
    - {data.open_measures.map((me) => ( -
  • - {me.id} — {me.description} -
  • - ))} -
-
-
-

CRA-Fristen

-
    - {data.deadlines.map((d) => ( -
  • - {d.date} {d.label} -
  • - ))} -
+ {/* Recommended measures — full curated text + norm references */} +
+

Empfohlene Maßnahmen

+

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

+
+ {data.open_measures.map((me) => ( +
+

+ {me.id} — {me.name} +

+

{me.description}

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

CRA-Fristen

+
    + {data.deadlines.map((d) => ( +
  • + {d.date} {d.label} +
  • + ))} +
+
) } 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 681fcf89..d789313e 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRADemo.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRADemo.ts @@ -2,11 +2,15 @@ // DEMO data layer for the CRA / Cyber tab. The Kistenhubgeraet (crate lift) CE // project is treated AS IF it had an internet-connected IoT module. The cyber -// findings are invented, but the CRA mapping below is the REAL output of the -// deterministic backend mapper (compliance/services/cra_finding_mapper.py) run -// on these findings — so the integration concept is faithful. The cross_links -// (cyber re-opens a mechanically-mitigated safety hazard) are the core idea we -// want to validate visually. Live wiring replaces this fixture later. +// findings are invented, but the CRA mapping + the NIST 800-53 / OWASP Top 10 +// crosswalk below are the REAL output of the deterministic backend +// (compliance/services/cra_finding_mapper.py + cra_security_crosswalk.py) run on +// these findings — so the integration concept is faithful. Measure texts + +// norm references are the real curated entries from measures_library_cra.go. +// The cross_links (cyber re-opens a mechanically-mitigated safety hazard) are +// the core idea we validate visually. Live wiring replaces this fixture later. + +export interface OwaspRef { code: string; label: string } export interface CRAFinding { id: string @@ -19,8 +23,17 @@ export interface CRAFinding { requirement_ids: string[] annex_anchor: string iso27001_ref: string[] + nist_refs: string[] + owasp_refs: OwaspRef[] risk_level: string - measures: { id: string; description: string }[] + measures: string[] +} + +export interface Measure { + id: string + name: string + description: string + norm_refs: string[] } export interface CrossLink { @@ -38,19 +51,27 @@ export interface CRADemo { by_risk: Record coverage_pct: number requirements_touched: string[] - open_measures: { id: string; description: string }[] + open_measures: Measure[] cross_links: CrossLink[] deadlines: { date: string; label: string }[] } -const MEASURE_DESC: Record = { - M541: 'Signierte Software- und Firmware-Updates mit Rollback-Schutz', - M542: 'Initiale Default-Passwoerter beim ersten Start erzwungen aendern', - M545: 'Cybersecurity-Hardening-Guide fuer den Anwender beilegen', - M547: 'Updates ueber authentisierten Kanal mit Integritaetspruefung', -} +const ow = (code: string, label: string): OwaspRef => ({ code, label }) -const m = (...ids: string[]) => ids.map((id) => ({ id, description: MEASURE_DESC[id] || '' })) +const MEASURES: Measure[] = [ + { id: 'M542', name: 'Initiale Default-Passwoerter beim ersten Start erzwungen aendern', + description: 'Die Maschine fordert beim ersten Hochfahren zwingend die Aenderung aller werkseitigen Default-Passwoerter (Bediener, Wartung, Admin). Default-Credentials werden nicht in Klartext in der Dokumentation veroeffentlicht, sondern dem Betreiber separat (versiegelt) uebergeben. Eine Wiederherstellung auf Default-Credentials ist nur ueber physischen Zugriff moeglich.', + norm_refs: ['Verordnung (EU) 2024/2847 (CRA), Anhang I', 'DIN EN 40000-1-2 (Entwurf)', 'ETSI EN 303 645'] }, + { id: 'M547', name: 'Updates ueber authentisierten Kanal mit Integritaetspruefung', + description: 'Der Update-Kanal (Online-Pull oder USB-Push) ist gegen Manipulation gesichert: TLS-1.3 mit Zertifikatspruefung bei Online-Updates, Hash-Pruefung der Update-Datei vor dem Anwenden. Der Update-Prozess ist atomar: bei Abbruch bleibt die alte Version lauffaehig.', + norm_refs: ['Verordnung (EU) 2024/2847 (CRA), Anhang I', 'DIN EN 40000-1-2 (Entwurf)', 'IEC 62443-4-2'] }, + { id: 'M541', name: 'Signierte Software- und Firmware-Updates mit Rollback-Schutz', + description: 'Updates der Steuerungs-, Roboter- und Visualisierungssoftware werden ausschliesslich kryptographisch signiert ausgeliefert. Die Steuerung prueft die Signatur vor der Installation und verweigert das Einspielen unsignierter Pakete. Ein Rollback-Schutz verhindert das Downgraden auf nachweislich verwundbare Versionen.', + norm_refs: ['Verordnung (EU) 2024/2847 (CRA), Anhang I', 'DIN EN 40000-1-2 (Entwurf)', 'IEC 62443-4-1'] }, + { id: 'M545', name: 'Cybersecurity-Hardening-Guide fuer den Anwender beilegen', + description: 'Die mit der Maschine ausgelieferte Dokumentation enthaelt einen Cybersecurity-Hardening-Guide: empfohlene Netzwerk-Segmentierung, deaktivierbare Dienste, sichere Konfiguration aller eingebauten Komponenten (PLC, HMI, Roboter), Empfehlungen zur Benutzer-Verwaltung. Der Guide wird bei Update-Releases gepflegt.', + norm_refs: ['Verordnung (EU) 2024/2847 (CRA), Anhang II', 'DIN EN 40000-1-2 (Entwurf)', 'IEC 62443-3-3'] }, +] const DEMO: CRADemo = { scenario: @@ -59,32 +80,38 @@ const DEMO: CRADemo = { { id: 'KH-CY-1', title: 'Fernsteuer-Weboberflaeche mit universellem Default-Passwort', location: 'remote-ui/login', scanner_severity: 'critical', cwe: 'CWE-259', primary_requirement: 'CRA-AI-8', requirement_title: 'Keine Default-Passwoerter', requirement_ids: ['CRA-AI-8'], - annex_anchor: 'Annex I, 1(3)(d)', iso27001_ref: ['A.8.5'], risk_level: 'CRITICAL', measures: m('M542') }, + annex_anchor: 'Annex I, 1(3)(d)', iso27001_ref: ['A.8.5'], nist_refs: ['IA-5', 'IA-5(1)'], + owasp_refs: [ow('A07:2021', 'Identification and Authentication Failures')], risk_level: 'CRITICAL', measures: ['M542'] }, { id: 'KH-CY-2', title: 'IoT-Telemetrie unverschluesselt ueber MQTT', location: 'telemetry/mqtt', scanner_severity: 'high', cwe: 'CWE-319', primary_requirement: 'CRA-AI-15', requirement_title: 'Transport-Schutz (Data in Transit)', requirement_ids: ['CRA-AI-15', 'CRA-AI-13'], - annex_anchor: 'Annex I, 1(3)(e)', iso27001_ref: ['A.8.24'], risk_level: 'HIGH', measures: [] }, + annex_anchor: 'Annex I, 1(3)(e)', iso27001_ref: ['A.8.24'], nist_refs: ['SC-8', 'SC-8(1)', 'SC-13', 'SC-28'], + owasp_refs: [ow('A02:2021', 'Cryptographic Failures')], risk_level: 'HIGH', measures: [] }, { id: 'KH-CY-3', title: 'Firmware-Updates ohne Signaturpruefung', location: 'updater', scanner_severity: 'high', cwe: 'CWE-494', primary_requirement: 'CRA-AI-30', requirement_title: 'Update-Integritaet', requirement_ids: ['CRA-AI-30', 'CRA-AI-28', 'CRA-AI-6'], - annex_anchor: 'Annex I, 1(4)', iso27001_ref: ['A.8.24'], risk_level: 'HIGH', measures: m('M547', 'M541') }, + annex_anchor: 'Annex I, 1(4)', iso27001_ref: ['A.8.24'], nist_refs: ['SI-7', 'SI-2'], + owasp_refs: [ow('A08:2021', 'Software and Data Integrity Failures')], risk_level: 'HIGH', measures: ['M547', 'M541'] }, { id: 'KH-CY-4', title: 'Offener Debug-Port (telnet) am Controller', location: 'controller:23', scanner_severity: 'medium', cwe: 'CWE-1188', primary_requirement: 'CRA-AI-1', requirement_title: 'Secure-by-Default-Konfiguration', requirement_ids: ['CRA-AI-1'], - annex_anchor: 'Annex I, 1(1)', iso27001_ref: ['A.8.9'], risk_level: 'HIGH', measures: m('M545') }, + annex_anchor: 'Annex I, 1(1)', iso27001_ref: ['A.8.9'], nist_refs: ['CM-6', 'CM-7'], + owasp_refs: [ow('A05:2021', 'Security Misconfiguration')], risk_level: 'HIGH', measures: ['M545'] }, { id: 'KH-CY-5', title: 'Gebundelte libmodbus mit bekannter CVE (veraltet)', location: 'deps/libmodbus', scanner_severity: 'high', cwe: 'CWE-1104', primary_requirement: 'CRA-AI-22', requirement_title: 'Dependency-Monitoring', requirement_ids: ['CRA-AI-22'], - annex_anchor: 'Annex I, 1(5)', iso27001_ref: ['A.8.8', 'A.8.25'], risk_level: 'HIGH', measures: [] }, + annex_anchor: 'Annex I, 1(5)', iso27001_ref: ['A.8.8', 'A.8.25'], nist_refs: ['RA-5', 'SI-2', 'SR-4'], + owasp_refs: [ow('A06:2021', 'Vulnerable and Outdated Components')], risk_level: 'HIGH', measures: [] }, { id: 'KH-CY-6', title: 'Keine Sicherheits-Protokollierung der Remote-Befehle', location: 'remote-ui', scanner_severity: 'medium', cwe: 'CWE-778', primary_requirement: 'CRA-AI-24', requirement_title: 'Security-Logging', requirement_ids: ['CRA-AI-24'], - annex_anchor: 'Annex I, 1(3)(g)', iso27001_ref: ['A.8.15'], risk_level: 'MEDIUM', measures: [] }, + annex_anchor: 'Annex I, 1(3)(g)', iso27001_ref: ['A.8.15'], nist_refs: ['AU-2', 'AU-3'], + owasp_refs: [ow('A09:2021', 'Security Logging and Monitoring Failures')], risk_level: 'MEDIUM', measures: [] }, ], by_risk: { CRITICAL: 1, HIGH: 4, MEDIUM: 1, LOW: 0 }, coverage_pct: 100.0, requirements_touched: ['CRA-AI-1', 'CRA-AI-6', 'CRA-AI-8', 'CRA-AI-13', 'CRA-AI-15', 'CRA-AI-22', 'CRA-AI-24', 'CRA-AI-28', 'CRA-AI-30'], - open_measures: m('M542', 'M547', 'M541', 'M545'), + open_measures: MEASURES, cross_links: [ { cyber_finding_ids: ['KH-CY-1', 'KH-CY-3'], diff --git a/backend-compliance/compliance/services/cra_finding_mapper.py b/backend-compliance/compliance/services/cra_finding_mapper.py index 3985fbae..28f2b647 100644 --- a/backend-compliance/compliance/services/cra_finding_mapper.py +++ b/backend-compliance/compliance/services/cra_finding_mapper.py @@ -14,6 +14,7 @@ from dataclasses import dataclass, field, asdict from typing import Optional from compliance.api.cra_annex_i_data import ANNEX_I_REQUIREMENTS, MEASURES, DEADLINES +from compliance.services.cra_security_crosswalk import security_refs_for _REQ_INDEX = {r["req_id"]: r for r in ANNEX_I_REQUIREMENTS} _SEV_ORDER = {"LOW": 1, "MEDIUM": 2, "HIGH": 3, "CRITICAL": 4} @@ -89,6 +90,8 @@ class MappedFinding: iso27001_ref: list = field(default_factory=list) 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) + owasp_refs: list = field(default_factory=list) # [{code, label}] OWASP Top 10:2021 rationale: str = "" unmapped: bool = False @@ -162,6 +165,7 @@ def map_finding(f: ScannerFinding) -> MappedFinding: for m in _REQ_INDEX[rid].get("mapped_measures", []): if m not in measures: measures.append(m) + refs = security_refs_for(reqs) return MappedFinding( finding_id=f.id, requirement_ids=reqs, @@ -170,6 +174,8 @@ def map_finding(f: ScannerFinding) -> MappedFinding: iso27001_ref=list(primary.get("iso27001_ref", [])), risk_level=_SEV_BY_RANK.get(risk_rank, "LOW"), measures=measures, + nist_refs=refs["nist"], + owasp_refs=refs["owasp"], rationale="{}: {}".format(primary["req_id"], primary.get("title", "")), ) diff --git a/backend-compliance/compliance/services/cra_security_crosswalk.py b/backend-compliance/compliance/services/cra_security_crosswalk.py new file mode 100644 index 00000000..71fa6904 --- /dev/null +++ b/backend-compliance/compliance/services/cra_security_crosswalk.py @@ -0,0 +1,92 @@ +"""CRA Annex I -> NIST 800-53 Rev5 + OWASP Top 10:2021 crosswalk (Security Golden Set). + +Deterministic, hand-curated mapping. It is the AUTHORITATIVE reference set that +the standalone CRA assessment uses to attach best-practice control IDs to each +CRA essential requirement. NOT a RAG search — semantic breadth comes later via +the shared Controls-API of the mapping session, NOT a second retrieval here. + +Each entry maps a CRA-AI requirement to the well-established NIST 800-53 Rev5 +control identifiers and the OWASP Top 10:2021 category that govern the same +control objective. Process-only requirements (disclosure/reporting) carry NIST +refs where defensible and no OWASP code (OWASP Top 10 addresses code-level +weaknesses, not disclosure processes) — left empty rather than invented. +""" + +# OWASP Top 10:2021 category labels (for display). +OWASP_2021 = { + "A01:2021": "Broken Access Control", + "A02:2021": "Cryptographic Failures", + "A04:2021": "Insecure Design", + "A05:2021": "Security Misconfiguration", + "A06:2021": "Vulnerable and Outdated Components", + "A07:2021": "Identification and Authentication Failures", + "A08:2021": "Software and Data Integrity Failures", + "A09:2021": "Security Logging and Monitoring Failures", +} + +# req_id -> {"nist": [800-53 control IDs], "owasp": [Top 10:2021 codes]} +CRA_SECURITY_CROSSWALK = { + "CRA-AI-1": {"nist": ["CM-6", "CM-7"], "owasp": ["A05:2021"]}, + "CRA-AI-2": {"nist": ["CM-7", "SC-7"], "owasp": ["A05:2021"]}, + "CRA-AI-3": {"nist": ["SA-8", "SC-7", "SC-39"], "owasp": ["A04:2021"]}, + "CRA-AI-4": {"nist": ["AC-6"], "owasp": ["A01:2021"]}, + "CRA-AI-5": {"nist": ["SI-7", "CM-5"], "owasp": ["A08:2021"]}, + "CRA-AI-6": {"nist": ["SI-7"], "owasp": ["A08:2021"]}, + "CRA-AI-7": {"nist": ["IA-2", "IA-2(1)"], "owasp": ["A07:2021"]}, + "CRA-AI-8": {"nist": ["IA-5", "IA-5(1)"], "owasp": ["A07:2021"]}, + "CRA-AI-9": {"nist": ["IA-5"], "owasp": ["A07:2021"]}, + "CRA-AI-10": {"nist": ["AC-12", "SC-23"], "owasp": ["A07:2021"]}, + "CRA-AI-11": {"nist": ["AC-7"], "owasp": ["A07:2021"]}, + "CRA-AI-12": {"nist": ["AC-2", "AC-3", "AC-6"], "owasp": ["A01:2021"]}, + "CRA-AI-13": {"nist": ["SC-13", "SC-28"], "owasp": ["A02:2021"]}, + "CRA-AI-14": {"nist": ["SC-28"], "owasp": ["A02:2021"]}, + "CRA-AI-15": {"nist": ["SC-8", "SC-8(1)"], "owasp": ["A02:2021"]}, + "CRA-AI-16": {"nist": ["SC-12"], "owasp": ["A02:2021"]}, + "CRA-AI-17": {"nist": ["SI-12"], "owasp": []}, + "CRA-AI-18": {"nist": ["SA-3", "SA-8", "SA-15"], "owasp": ["A04:2021"]}, + "CRA-AI-19": {"nist": ["SA-11", "SA-15"], "owasp": ["A04:2021"]}, + "CRA-AI-20": {"nist": ["SA-11", "SA-11(1)"], "owasp": ["A04:2021"]}, + "CRA-AI-21": {"nist": ["SR-3", "SR-5", "SR-6"], "owasp": ["A06:2021"]}, + "CRA-AI-22": {"nist": ["RA-5", "SI-2", "SR-4"], "owasp": ["A06:2021"]}, + "CRA-AI-23": {"nist": ["SR-4", "SR-3"], "owasp": ["A06:2021"]}, + "CRA-AI-24": {"nist": ["AU-2", "AU-3"], "owasp": ["A09:2021"]}, + "CRA-AI-25": {"nist": ["AU-6", "SI-4"], "owasp": ["A09:2021"]}, + "CRA-AI-26": {"nist": ["SI-4"], "owasp": ["A09:2021"]}, + "CRA-AI-27": {"nist": ["AU-9"], "owasp": ["A09:2021"]}, + "CRA-AI-28": {"nist": ["SI-2", "SI-7"], "owasp": ["A08:2021"]}, + "CRA-AI-29": {"nist": ["SI-7(15)", "SC-12"], "owasp": ["A08:2021"]}, + "CRA-AI-30": {"nist": ["SI-7"], "owasp": ["A08:2021"]}, + "CRA-AI-31": {"nist": ["SA-22"], "owasp": ["A06:2021"]}, + "CRA-AI-32": {"nist": ["RA-5"], "owasp": ["A06:2021"]}, + "CRA-AI-33": {"nist": ["RA-5", "SR-4"], "owasp": ["A06:2021"]}, + "CRA-AI-34": {"nist": ["RA-5", "RA-7"], "owasp": []}, + "CRA-AI-35": {"nist": ["SI-5"], "owasp": []}, + "CRA-AI-36": {"nist": ["IR-4", "IR-6"], "owasp": []}, + "CRA-AI-37": {"nist": ["IR-6"], "owasp": []}, + "CRA-AI-38": {"nist": ["IR-6", "IR-8"], "owasp": []}, + "CRA-AI-39": {"nist": ["SI-2"], "owasp": ["A06:2021"]}, + "CRA-AI-40": {"nist": ["IR-5", "AU-11"], "owasp": []}, +} + + +def security_refs_for(req_ids: list) -> dict: + """Union of NIST + OWASP refs across the given CRA-AI requirement ids. + + Returns {"nist": [...], "owasp": [{"code": .., "label": ..}]}, deduped, + order-stable. The golden set is the single source — no retrieval. + """ + nist: list = [] + owasp: list = [] + seen_owasp = set() + for rid in req_ids: + entry = CRA_SECURITY_CROSSWALK.get(rid) + if not entry: + continue + for n in entry["nist"]: + if n not in nist: + nist.append(n) + for code in entry["owasp"]: + if code not in seen_owasp: + seen_owasp.add(code) + owasp.append({"code": code, "label": OWASP_2021.get(code, "")}) + return {"nist": nist, "owasp": owasp} diff --git a/backend-compliance/tests/test_cra_finding_mapper.py b/backend-compliance/tests/test_cra_finding_mapper.py index 6ac0bca7..c091f987 100644 --- a/backend-compliance/tests/test_cra_finding_mapper.py +++ b/backend-compliance/tests/test_cra_finding_mapper.py @@ -2,6 +2,8 @@ from compliance.services.cra_finding_mapper import ( ScannerFinding, map_finding, assess_findings, assess_findings_payload, ) +from compliance.services.cra_security_crosswalk import CRA_SECURITY_CROSSWALK +from compliance.api.cra_annex_i_data import ANNEX_I_REQUIREMENTS def test_hardcoded_credentials_cwe_maps_to_credential_requirement(): @@ -82,3 +84,31 @@ def test_empty_payload_is_safe(): r = assess_findings_payload({}) assert r["findings_total"] == 0 assert r["coverage_pct"] == 0.0 + + +# --- Security golden-set crosswalk (NIST 800-53 + OWASP Top 10:2021) --- + +def test_default_password_carries_nist_and_owasp_refs(): + m = map_finding(ScannerFinding(id="g1", title="default password", cwe="CWE-259", severity="high")) + assert "IA-5" in m.nist_refs + assert any(o["code"] == "A07:2021" for o in m.owasp_refs) + + +def test_dependency_finding_maps_to_owasp_a06_and_nist_ra5(): + m = map_finding(ScannerFinding(id="g2", title="outdated dependency", category="dependency", severity="high")) + assert "RA-5" in m.nist_refs + assert any(o["code"] == "A06:2021" for o in m.owasp_refs) + + +def test_unmapped_finding_has_no_security_refs(): + m = map_finding(ScannerFinding(id="g3", title="zzz nothing", severity="low")) + assert m.nist_refs == [] and m.owasp_refs == [] + + +def test_golden_set_covers_every_requirement(): + # Completeness invariant: every CRA-AI requirement has a crosswalk entry + # with at least one NIST control id (OWASP may be empty for process reqs). + for req in ANNEX_I_REQUIREMENTS: + rid = req["req_id"] + assert rid in CRA_SECURITY_CROSSWALK, "missing crosswalk for {}".format(rid) + assert CRA_SECURITY_CROSSWALK[rid]["nist"], "no NIST refs for {}".format(rid)