diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/_components/TechFindings.tsx b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/TechFindings.tsx index db2b31b8..fb025de6 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/_components/TechFindings.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/TechFindings.tsx @@ -48,8 +48,23 @@ function FindingDetail({ f, measuresById }: { f: CRAFinding; measuresById: Recor return (
  • {m ? m.name : mid} + {m?.tier === 'review' && ( + indikativ + )} {m?.description ? — {m.description} : null} - {m?.norm_refs?.length ? · {m.norm_refs.join(', ')} : null} + {m?.norm_sources?.length ? ( + + {m.norm_sources.map((s) => ( + + {s.ref}{s.license_class === 'paid_reference' ? ' · nur Verweis' : ''} + + ))} + + ) : (m?.norm_refs?.length ? · {m.norm_refs.join(', ')} : null)}
  • ) })} 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 1bf24c77..873ea101 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts @@ -72,6 +72,9 @@ function merge(live: any): CRADemo { name: om.name || DEMO_SCENARIO.open_measures.find((d) => d.id === om.id)?.name || om.id, description: om.description || '', norm_refs: om.norm_refs || [], + norm_sources: om.norm_sources || [], + tier: om.tier, + provenance: om.provenance, })) return { 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 edb22d91..fd7069ac 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRADemo.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRADemo.ts @@ -39,11 +39,16 @@ export interface CRAFinding { objective?: string } +export interface NormSource { ref: string; license_class: string; label: string } + export interface Measure { id: string name: string description: string norm_refs: string[] + norm_sources?: NormSource[] // license-classified refs (provenance) + tier?: string // core (validated) | review (indicative) + provenance?: string } export interface CrossLink { diff --git a/backend-compliance/compliance/api/cra_annex_i_data.py b/backend-compliance/compliance/api/cra_annex_i_data.py index 8ff8f517..6130431b 100644 --- a/backend-compliance/compliance/api/cra_annex_i_data.py +++ b/backend-compliance/compliance/api/cra_annex_i_data.py @@ -268,6 +268,8 @@ SEVERITY_WEIGHT = { import json as _json import os as _os +from compliance.data.norm_sources import classify_refs as _classify_refs + MEASURE_DETAILS: dict = {} try: @@ -275,6 +277,12 @@ try: with open(_MEAS_PATH, encoding="utf-8") as _fh: _curated = _json.load(_fh) MEASURE_DETAILS = {m["id"]: m for m in _curated if m.get("id")} + # Provenance: curated measures are expert/standards-based ('core' tier); each + # norm_ref is license-classified (law/public/open vs paid-reference-only). + for _m in MEASURE_DETAILS.values(): + _m.setdefault("tier", "core") + _m.setdefault("provenance", "curated_expert_standards") + _m["norm_sources"] = _classify_refs(_m.get("norm_refs", [])) for _m in _curated: if _m.get("id") and _m.get("name"): MEASURES[_m["id"]] = _m["name"] diff --git a/backend-compliance/compliance/data/norm_sources.py b/backend-compliance/compliance/data/norm_sources.py new file mode 100644 index 00000000..60c8b02e --- /dev/null +++ b/backend-compliance/compliance/data/norm_sources.py @@ -0,0 +1,55 @@ +"""Provenance / license classification for norm references. + +Encodes the BreakPilot mapping methodology (idea/expression): a *reference* to +where a topic sits in a standard is a fact and citable; the paid normative *text* +is never stored or reproduced. This classifier marks which sources are freely +usable (EU law, US-gov public domain, open licenses) vs. paid standards we may +only REFERENCE by clause/control ID. + +See docs-src/development/mapping-methodology.md. +""" + +LAW = "eu_law" # EU legislation — public, reproducible (EUR-Lex) +PUBLIC_DOMAIN = "public_domain" # e.g. NIST (US gov work) — reproducible +OPEN = "open" # OWASP (CC), ETSI EN 303 645, BSI — freely available +PAID_REFERENCE = "paid_reference" # ISO/IEC/EN/DIN — REFERENCE ONLY, no text stored + +LABEL = { + LAW: "EU-Recht (frei)", + PUBLIC_DOMAIN: "Public Domain (frei)", + OPEN: "offen lizenziert", + PAID_REFERENCE: "kostenpflichtige Norm — nur Verweis", +} + +_LAW = ("2024/2847", "2023/1230", "verordnung (eu)", "maschinenverordnung", "(cra)", + "anhang", "nis2", "nis-2", " art. ", "dsgvo", "2016/679", "2022/2555") +_PUBLIC = ("nist", "ntia", "nvd", "cisa") +_OPEN = ("owasp", "slsa", "etsi en 303 645", "bsi", "cyclonedx", "spdx", + "nist privacy framework") +_PAID = ("iso", "iec", "din", "en iso", "62443", "27002", "27035", "29147", + "30111", "15408", "18045", "13849", "13850", "13857", "14119", "14120", + "61496", "61800", "62061", "60204", "82079", "15066", "10218", "13855", "62061") + + +def classify_norm_ref(ref: str) -> str: + r = (ref or "").lower() + # NIST Privacy Framework is open-ish; keep public-domain check after open guard. + if "nist privacy" in r: + return OPEN + if any(k in r for k in _LAW): + return LAW + if any(k in r for k in _PUBLIC): + return PUBLIC_DOMAIN + if any(k in r for k in _OPEN): + return OPEN + if any(k in r for k in _PAID): + return PAID_REFERENCE + return PAID_REFERENCE # conservative default: treat unknown as reference-only + + +def classify_refs(refs) -> list: + """[{ref, license_class, label}] for each norm reference.""" + return [ + {"ref": r, "license_class": (lc := classify_norm_ref(r)), "label": LABEL[lc]} + for r in (refs or []) + ] diff --git a/backend-compliance/compliance/services/cra_finding_mapper.py b/backend-compliance/compliance/services/cra_finding_mapper.py index f3105f5a..7a40c866 100644 --- a/backend-compliance/compliance/services/cra_finding_mapper.py +++ b/backend-compliance/compliance/services/cra_finding_mapper.py @@ -24,8 +24,10 @@ def _measure_obj(mid: str) -> dict: d = MEASURE_DETAILS.get(mid) if d: return {"id": mid, "name": d.get("name", ""), "description": d.get("description", ""), - "norm_refs": d.get("norm_refs", [])} - return {"id": mid, "name": MEASURES.get(mid, ""), "description": MEASURES.get(mid, ""), "norm_refs": []} + "norm_refs": d.get("norm_refs", []), "norm_sources": d.get("norm_sources", []), + "tier": d.get("tier", "core"), "provenance": d.get("provenance", "")} + return {"id": mid, "name": MEASURES.get(mid, ""), "description": MEASURES.get(mid, ""), + "norm_refs": [], "norm_sources": [], "tier": "review", "provenance": ""} _REQ_INDEX = {r["req_id"]: r for r in ANNEX_I_REQUIREMENTS} diff --git a/backend-compliance/tests/test_norm_sources.py b/backend-compliance/tests/test_norm_sources.py new file mode 100644 index 00000000..1e872f8f --- /dev/null +++ b/backend-compliance/tests/test_norm_sources.py @@ -0,0 +1,38 @@ +"""License/provenance classification of norm references — encodes the line between +freely-usable sources and paid standards we may only reference (not reproduce).""" +from compliance.data.norm_sources import ( + LAW, OPEN, PAID_REFERENCE, PUBLIC_DOMAIN, classify_norm_ref, +) +from compliance.api.cra_annex_i_data import MEASURE_DETAILS + + +def test_eu_law_is_reproducible_class(): + assert classify_norm_ref("Verordnung (EU) 2024/2847 (CRA), Anhang I") == LAW + assert classify_norm_ref("MaschinenVO Anhang III") == LAW + assert classify_norm_ref("NIS2 Art. 21") == LAW + + +def test_nist_is_public_domain(): + assert classify_norm_ref("NIST SP 800-53: IA-5") == PUBLIC_DOMAIN + assert classify_norm_ref("NIST SP 800-218") == PUBLIC_DOMAIN + + +def test_open_licensed(): + assert classify_norm_ref("OWASP ASVS V3") == OPEN + assert classify_norm_ref("ETSI EN 303 645") == OPEN + + +def test_paid_standards_reference_only(): + assert classify_norm_ref("IEC 62443-4-1") == PAID_REFERENCE + assert classify_norm_ref("ISO/IEC 27002") == PAID_REFERENCE + assert classify_norm_ref("EN ISO 13849-1") == PAID_REFERENCE + + +def test_unknown_defaults_conservative(): + assert classify_norm_ref("Irgendein Hausstandard XY") == PAID_REFERENCE + + +def test_curated_measures_carry_provenance(): + m = MEASURE_DETAILS["M540"] + assert m.get("tier") == "core" + assert m.get("norm_sources") and all("license_class" in s for s in m["norm_sources"]) diff --git a/docs-src/development/mapping-methodology.md b/docs-src/development/mapping-methodology.md new file mode 100644 index 00000000..cbffead7 --- /dev/null +++ b/docs-src/development/mapping-methodology.md @@ -0,0 +1,75 @@ +# BreakPilot Mapping Methodology + +> Interne Methodik-Doku für Due Diligence, Kunden-, Anwalts- und Lizenzfragen. +> Stand 2026-06-16. Kern: **Wir modellieren gemeinsame Konzepte (Szenario C), +> nicht Normstrukturen.** + +## Grundprinzip: Idee vs. Ausdruck + +Urheberrecht schützt die **konkrete Formulierung** (den Normtext), **nicht** Ideen, +Fakten oder Themen. Daraus folgt unsere Linie: + +- **Normtext** wird **nicht gespeichert** und **nicht reproduziert**. +- **Fundstellen** (Klausel-/Control-IDs, Artikel-Nummern, Titel) sind **Fakten** und + als Quellenhinweis zitierbar — wie ein Buch-Kapitelverweis. +- **Mappings** sind **Tatsachenaussagen über Bezüge** ("dieselbe Anforderungsidee + taucht in CRA, NIST, IEC 62443 auf"). + +## Unser Modell ist Szenario C, nicht A oder B + +``` +Cybersecurity-/Safety-Realität + ↓ +Master Control / Maßnahme ← eigenständig formuliert (unser Werk) + ↓ +Normen referenzieren darauf ← CRA · MaschinenVO · NIST · OWASP · ETSI · BSI · IEC 62443 · ENISA +``` + +Wir behaupten **nicht** „die Norm sagt X" (mit Normwortlaut), sondern „diese +Maßnahme adressiert dieselbe Anforderungsidee, die auch in mehreren Standards +auftaucht". Je mehr Quellen auf dasselbe Konzept verweisen (Least Privilege, +Logging, Updates, Authentisierung …), desto klarer ist es ein **eigenständiges +Wissensmodell**, kein Norm-Derivat. + +Vergleichbar mit Beck/Juris/Wolters: nicht „§ 823 BGB sagt …", sondern „nach +herrschender Meinung folgt daraus …" — eigene redaktionelle Leistung. + +## Quellen-Lizenzklassen (siehe `compliance/data/norm_sources.py`) + +| Klasse | Beispiele | Umgang | +|---|---|---| +| `eu_law` | CRA (2024/2847), MaschinenVO (2023/1230), NIS2, DSGVO | EU-Recht, öffentlich — reproduzierbar (EUR-Lex) | +| `public_domain` | NIST SP 800-53/218, NIST CSF, NIST OLIR, NTIA, CISA | US-Gov, gemeinfrei — reproduzierbar | +| `open` | OWASP (CC), ETSI EN 303 645, BSI IT-Grundschutz, SPDX, CycloneDX | offen lizenziert — mit Quellenangabe nutzbar | +| `paid_reference` | ISO/IEC, EN/DIN, IEC 62443, ISO 27002, ISO/IEC 15408, EN ISO 13849 … | **nur Verweis** auf Klausel-/Control-ID — KEIN Text gespeichert | + +Crosswalk-Wissen stammt überwiegend aus **publizierten, autoritativen Quellen** +(NIST OLIR = öffentliche Crosswalk-Datenbank, ENISA CRA-Mapping, Norm-Annexe, +EU-Normungsauftrag) plus gemeinfreien Inhalten — nicht aus kostenpflichtigem +Normtext. + +## Rolle der KI + +KI ist **Skalierer**, nicht Quelle: sie schlägt semantische Zuordnungen vor und +normalisiert; das Fundament sind publizierte Crosswalks + gemeinfreie Quellen, und +die tragenden Zuordnungen werden expertengeprüft. Jede Zuordnung trägt daher +**Provenienz + Tier**: `core` (belegt/expertengeprüft) vs. `review` +(KI-vorgeschlagen, indikativ). Indikative Zuordnungen werden als solche gekennzeichnet +("mit DSB/Auditor verifizieren"). + +## Die 6 Methodik-Aussagen (Kurzform) + +1. Normtexte werden **nicht gespeichert**. +2. Normtexte werden **nicht reproduziert**. +3. Master Controls / Maßnahmen sind **eigenständig formuliert**. +4. Mappings entstehen als **semantische Zuordnung** (Experte/KI), mit Provenienz/Tier. +5. Die Plattform **ersetzt keine Norm** und ermöglicht **keine Rekonstruktion** der Norm. +6. Normreferenzen dienen ausschließlich als **Quellenhinweis** (Klausel-/Control-ID). + +## Roter Faden / kritischer Bereich + +Risiko ist **nicht** das Mapping, sondern das **Aufkumulieren von Normstruktur** +(viele Kapitelüberschriften + Original-Requirement-IDs + Originaltext + Tabellen), +sodass sich die Originalnorm rekonstruieren ließe. Solange wir auf der Ebene +**Master Control → Maßnahme → Evidenz → Referenzen** bleiben (Szenario C), ist die +Position robust.