feat(cra): Maßnahmen-Provenienz + Lizenzklasse je Normquelle
Jede Normreferenz einer Maßnahme wird lizenzklassifiziert (eu_law / public_domain / open / paid_reference) — paid-reference-Normen werden nur als Verweis geführt, nie im Text gespeichert (idea/expression). Kuratierte Maßnahmen tragen Tier 'core', KI-/Fallback-Maßnahmen 'review' (indikativ). Frontend zeigt Quellen-Badges + "indikativ"-Kennzeichnung. Methodik in docs-src/development/mapping-methodology.md (Szenario C, Due-Diligence). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -48,8 +48,23 @@ function FindingDetail({ f, measuresById }: { f: CRAFinding; measuresById: Recor
|
|||||||
return (
|
return (
|
||||||
<li key={mid} className="text-sm text-gray-700 dark:text-gray-200">
|
<li key={mid} className="text-sm text-gray-700 dark:text-gray-200">
|
||||||
<span className="font-medium">{m ? m.name : mid}</span>
|
<span className="font-medium">{m ? m.name : mid}</span>
|
||||||
|
{m?.tier === 'review' && (
|
||||||
|
<span title="KI-vorgeschlagen — mit DSB/Auditor verifizieren" className="ml-1 rounded bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300 px-1 py-0.5 text-[11px]">indikativ</span>
|
||||||
|
)}
|
||||||
{m?.description ? <span className="text-gray-500"> — {m.description}</span> : null}
|
{m?.description ? <span className="text-gray-500"> — {m.description}</span> : null}
|
||||||
{m?.norm_refs?.length ? <span className="text-gray-400"> · {m.norm_refs.join(', ')}</span> : null}
|
{m?.norm_sources?.length ? (
|
||||||
|
<span className="mt-1 flex flex-wrap gap-1">
|
||||||
|
{m.norm_sources.map((s) => (
|
||||||
|
<span key={s.ref} title={s.label}
|
||||||
|
className={`rounded px-1.5 py-0.5 text-[11px] ${
|
||||||
|
s.license_class === 'paid_reference'
|
||||||
|
? 'bg-amber-50 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
||||||
|
: 'bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300'}`}>
|
||||||
|
{s.ref}{s.license_class === 'paid_reference' ? ' · nur Verweis' : ''}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
) : (m?.norm_refs?.length ? <span className="text-gray-400"> · {m.norm_refs.join(', ')}</span> : null)}
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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,
|
name: om.name || DEMO_SCENARIO.open_measures.find((d) => d.id === om.id)?.name || om.id,
|
||||||
description: om.description || '',
|
description: om.description || '',
|
||||||
norm_refs: om.norm_refs || [],
|
norm_refs: om.norm_refs || [],
|
||||||
|
norm_sources: om.norm_sources || [],
|
||||||
|
tier: om.tier,
|
||||||
|
provenance: om.provenance,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -39,11 +39,16 @@ export interface CRAFinding {
|
|||||||
objective?: string
|
objective?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NormSource { ref: string; license_class: string; label: string }
|
||||||
|
|
||||||
export interface Measure {
|
export interface Measure {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
norm_refs: string[]
|
norm_refs: string[]
|
||||||
|
norm_sources?: NormSource[] // license-classified refs (provenance)
|
||||||
|
tier?: string // core (validated) | review (indicative)
|
||||||
|
provenance?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CrossLink {
|
export interface CrossLink {
|
||||||
|
|||||||
@@ -268,6 +268,8 @@ SEVERITY_WEIGHT = {
|
|||||||
import json as _json
|
import json as _json
|
||||||
import os as _os
|
import os as _os
|
||||||
|
|
||||||
|
from compliance.data.norm_sources import classify_refs as _classify_refs
|
||||||
|
|
||||||
MEASURE_DETAILS: dict = {}
|
MEASURE_DETAILS: dict = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -275,6 +277,12 @@ try:
|
|||||||
with open(_MEAS_PATH, encoding="utf-8") as _fh:
|
with open(_MEAS_PATH, encoding="utf-8") as _fh:
|
||||||
_curated = _json.load(_fh)
|
_curated = _json.load(_fh)
|
||||||
MEASURE_DETAILS = {m["id"]: m for m in _curated if m.get("id")}
|
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:
|
for _m in _curated:
|
||||||
if _m.get("id") and _m.get("name"):
|
if _m.get("id") and _m.get("name"):
|
||||||
MEASURES[_m["id"]] = _m["name"]
|
MEASURES[_m["id"]] = _m["name"]
|
||||||
|
|||||||
@@ -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 [])
|
||||||
|
]
|
||||||
@@ -24,8 +24,10 @@ def _measure_obj(mid: str) -> dict:
|
|||||||
d = MEASURE_DETAILS.get(mid)
|
d = MEASURE_DETAILS.get(mid)
|
||||||
if d:
|
if d:
|
||||||
return {"id": mid, "name": d.get("name", ""), "description": d.get("description", ""),
|
return {"id": mid, "name": d.get("name", ""), "description": d.get("description", ""),
|
||||||
"norm_refs": d.get("norm_refs", [])}
|
"norm_refs": d.get("norm_refs", []), "norm_sources": d.get("norm_sources", []),
|
||||||
return {"id": mid, "name": MEASURES.get(mid, ""), "description": MEASURES.get(mid, ""), "norm_refs": []}
|
"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}
|
_REQ_INDEX = {r["req_id"]: r for r in ANNEX_I_REQUIREMENTS}
|
||||||
|
|||||||
@@ -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"])
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user