feat: Dreistufenmodell normative Verbindlichkeit + Duplikat-Filter + Auto-Deploy

- Source-Type-Klassifikation (58 Regulierungen: law/guideline/framework)
- Backfill-Endpoint POST /controls/backfill-normative-strength
- exclude_duplicates Filter fuer Control-Library (Backend + Proxy + UI-Toggle)
- MkDocs-Kapitel: Normative Verbindlichkeit mit Mermaid-Diagrammen
- scripts/deploy.sh: Auto-Push + Mac Mini rebuild + Coolify health monitoring
- 26 Unit Tests fuer Klassifikations-Logik

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-25 08:18:00 +01:00
parent 6d3bdf8e74
commit 230fbeb490
8 changed files with 796 additions and 4 deletions

View File

@@ -0,0 +1,201 @@
# Normative Verbindlichkeit — Dreistufenmodell
## Uebersicht
Nicht jede Quelle, aus der Controls abgeleitet werden, hat die gleiche rechtliche
Verbindlichkeit. Ein Control, das aus einem EU-Gesetz stammt, hat ein anderes
Gewicht als eines aus einem freiwilligen Framework.
Das Dreistufenmodell klassifiziert jede Quell-Regulierung und leitet daraus die
**effektive normative Staerke** der daraus erzeugten Obligations ab.
## Die drei Stufen
```mermaid
graph TB
subgraph "Stufe 1 — GESETZ (law)"
direction LR
A1["DSGVO, NIS2, AI Act, CRA..."]
A2["Rechtlich bindend"]
A3["Bussgeld bei Verstoss"]
A4["normative_strength: must/should/may"]
end
subgraph "Stufe 2 — LEITLINIE (guideline)"
direction LR
B1["EDPB-Leitlinien, BSI-TR, WP29"]
B2["Offizielle Auslegungshilfe"]
B3["Beweislastumkehr"]
B4["max normative_strength: should"]
end
subgraph "Stufe 3 — FRAMEWORK (framework)"
direction LR
C1["ENISA, NIST, OWASP, OECD"]
C2["Freiwillige Best Practice"]
C3["Stand der Technik"]
C4["max normative_strength: can"]
end
A1 --> A2 --> A3 --> A4
B1 --> B2 --> B3 --> B4
C1 --> C2 --> C3 --> C4
```
### Stufe 1: Gesetz (law)
| Eigenschaft | Beschreibung |
|---|---|
| **Verbindlichkeit** | Rechtlich bindend, Bussgeld bei Verstoss |
| **normative_strength** | Bleibt wie im Gesetzestext: `must`, `should` oder `may` |
| **Beispiele** | DSGVO (EU) 2016/679, NIS2-Richtlinie, KI-Verordnung, CRA, BDSG |
| **Warum relevant** | "Sie MUESSEN angemessene technische Massnahmen ergreifen" (Art. 32 DSGVO) |
!!! warning "Wichtig"
Gesetze formulieren Pflichten **abstrakt**. Art. 32 DSGVO sagt:
"dem Stand der Technik entsprechende Massnahmen" — aber NICHT
"verwende AES-256". Das WAS ist Pflicht, das WIE bleibt offen.
### Stufe 2: Leitlinie (guideline)
| Eigenschaft | Beschreibung |
|---|---|
| **Verbindlichkeit** | Nicht direkt bindend, aber Beweislastumkehr |
| **normative_strength** | Maximal `should` — auch wenn die Leitlinie intern "must" schreibt |
| **Beispiele** | EDPB-Leitlinien, BSI Technische Richtlinien, WP29-Dokumente |
| **Warum relevant** | "Daten at rest muessen verschluesselt werden" (BSI-TR) → `should` |
!!! info "Beweislastumkehr"
Wenn eine Aufsichtsbehoerde fragt "Warum verschluesselt ihr nicht?",
muss die Firma begruenden, warum sie von der Leitlinie abweicht.
Die Firma muss aber nicht genau so verschluesseln wie die BSI vorschlaegt.
### Stufe 3: Framework (framework)
| Eigenschaft | Beschreibung |
|---|---|
| **Verbindlichkeit** | Freiwillig, nicht rechtsverbindlich |
| **normative_strength** | Maximal `can` — unabhaengig von interner Sprache |
| **Beispiele** | ENISA CCM, NIST CSF, OWASP Top 10, OECD KI-Empfehlung |
| **Warum relevant** | "Organizations SHALL implement..." (ENISA) → `can` fuer den Anwender |
!!! tip "Stand der Technik"
NIS2 Art. 21 verweist auf ENISA-Leitlinien als Referenz fuer den
"Stand der Technik". Das hebt ENISA-Controls faktisch auf Stufe 2 (`should`)
— aber nur im Kontext von NIS2-pflichtigen Unternehmen, nicht generell.
## Ableitungskette
Die vollstaendige Kette von der Rechtsquelle zum atomaren Control:
```mermaid
graph LR
R["Regulierung<br/>(DSGVO Art. 32)"] -->|"MUSS"| O["Obligation<br/>(Daten schuetzen)"]
O -->|decomposition| RC["Rich Control<br/>(Verschluesselung)"]
RC -->|pass0b| AC["Atomares Control<br/>(AES-256 at rest)"]
R2["Framework<br/>(ENISA CCM)"] -->|"KANN"| AC
style R fill:#fee2e2,stroke:#dc2626
style R2 fill:#dbeafe,stroke:#2563eb
style O fill:#fef3c7,stroke:#d97706
style RC fill:#e0e7ff,stroke:#4f46e5
style AC fill:#d1fae5,stroke:#059669
```
**Beispiel**: Das atomare Control "AES-256 Verschluesselung at rest"
- Aus DSGVO Art. 32 abgeleitet → Obligation "must secure data" → **MUSS** (die Pflicht zu schuetzen)
- Aus ENISA CCM konkretisiert → **KANN** (AES-256 ist *eine* moegliche Umsetzung)
- Resultat: Die Firma MUSS verschluesseln, KANN aber waehlen wie
## Multi-Parent-Links
Ein atomares Control kann aus mehreren Quellen stammen:
| Control | Parent 1 | Parent 2 | Parent 3 | Effektive Staerke |
|---|---|---|---|---|
| SEC-042 (Encrypt at rest) | DSGVO Art. 32 (law) | NIS2 Art. 21 (law) | ENISA CCM (framework) | **must** (Gesetz uebertrumpft) |
| NET-015 (Zero Trust) | NIST SP 800-207 (framework) | CISA (framework) | — | **can** (nur Frameworks) |
| AUTH-003 (MFA) | DSGVO Art. 32 (law) | BSI-TR (guideline) | OWASP ASVS (framework) | **must** (Gesetz vorhanden) |
**Regel**: Der hoechste source_type bestimmt, ob die normative_strength begrenzt wird.
Wenn mindestens ein Parent-Link ein Gesetz ist, bleibt die Staerke wie extrahiert.
## Technische Umsetzung
### Klassifikations-Map
Datei: `backend-compliance/compliance/data/source_type_classification.py`
Jeder `source_regulation`-Wert aus `control_parent_links` wird klassifiziert:
```python
SOURCE_REGULATION_CLASSIFICATION = {
"DSGVO (EU) 2016/679": "law",
"EDPB Leitlinien 01/2020 (Datentransfers)": "guideline",
"NIST Cybersecurity Framework 2.0": "framework",
# ... 55+ Eintraege
}
```
### Backfill-Endpoint
```
POST /api/compliance/v1/canonical/controls/backfill-normative-strength?dry_run=true
```
Ablauf:
1. Alle aktiven `obligation_candidates` laden
2. Fuer jede Obligation den Parent-Control finden
3. Ueber `control_parent_links` die source_regulations ermitteln
4. Hoechsten source_type bestimmen
5. `normative_strength` begrenzen falls noetig
6. Bei `dry_run=false`: Aenderungen in die DB schreiben
### Cap-Funktion
```python
def cap_normative_strength(original: str, source_type: str) -> str:
"""
cap_normative_strength("must", "framework") → "can"
cap_normative_strength("should", "law") → "should"
cap_normative_strength("must", "guideline") → "should"
"""
```
## Frontend-Anzeige
In der Control-Detail-Ansicht werden Obligations mit farbcodierten Badges angezeigt:
| normative_strength | Badge | Farbe | Bedeutung |
|---|---|---|---|
| `must` | **MUSS** | Rot | Gesetzliche Pflicht |
| `should` | **SOLL** | Gelb/Amber | Empfohlen, Begruendungspflicht bei Abweichung |
| `can` / `may` | **KANN** | Gruen | Freiwillige Best Practice |
## Haeufige Fragen
### Warum steht bei einem ENISA-Control "MUSS"?
**Vor dem Backfill**: Das System uebernahm die Sprache des Quelldokuments 1:1.
ENISA schreibt intern "shall/must" weil es innerhalb seines Frameworks
verbindlich formuliert. Fuer den Anwender ist das ENISA-Dokument aber nicht
rechtsverbindlich.
**Nach dem Backfill**: ENISA-Controls zeigen maximal "KANN", es sei denn
ein Gesetz (z.B. NIS2) referenziert dasselbe Control — dann gilt die
gesetzliche Verbindlichkeit.
### Was bedeutet "Stand der Technik"?
NIS2 und DSGVO verweisen auf den "Stand der Technik", ohne ihn zu definieren.
In der Praxis werden ENISA- und BSI-Dokumente als Referenz herangezogen.
Das macht ihre Empfehlungen relevant ("SOLL"), aber nicht zu Gesetzen ("MUSS").
### Wie gehe ich mit unbekannten Quellen um?
Neue Regulierungen muessen in der `SOURCE_REGULATION_CLASSIFICATION` Map
eingetragen werden. Der Fallback fuer unbekannte Quellen ist `framework`
(konservativstes Ergebnis — geringste Verbindlichkeit zugewiesen).