fix(mc-audit): TOM/AVV case-mismatch + Ausnahmen-Pattern Wortabstand

- _PROCESS_INTERNAL_PATTERNS: Patterns wurden gegen lowercased Blob
  geprueft, aber Case-sensitive geschrieben (TOM/AVV/SCC). Matchen
  nie. Auf lowercase normalisiert.
- "Ausnahmen ... dokumentieren": Pattern war zu eng, verlangte direkte
  Adjazenz. Jetzt bis zu 60 Zeichen Wortabstand.
- Test-Suite mit 22 kuratierten DSGVO/AI-Act/eCall-MC-Labels. Alle
  gruen (vorher 2/22 FAIL — beide vom User explizit als Beispiele
  genannt: TOM, Ausnahmen).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-22 10:22:51 +02:00
parent e2be51b0aa
commit 6baf44ac84
2 changed files with 111 additions and 5 deletions
@@ -38,13 +38,15 @@ _PROCESS_INTERNAL_PATTERNS = [
r"\bsensibilisier",
# Vertraege intern
r"\bauftragsverarbeitungsvertrag\b",
r"\bAVV\b\s+abgeschlossen",
r"\bavv\b\s+abgeschlossen",
r"\bvertrag.*abgeschlossen",
r"\bdpa\s+(geschlossen|abgeschlossen|vorhanden)",
r"\bSCC\s+(geschlossen|abgeschlossen|implementiert)",
# Technisch-organisatorische Massnahmen (intern)
r"\bscc\s+(geschlossen|abgeschlossen|implementiert)",
# Technisch-organisatorische Massnahmen (intern). Lowercase: blob
# ist bereits .lower(); Case-sensitive Patterns (TOM/AVV/SCC) matchen
# nie. Daher hier explizit klein.
r"\btechnisch[-\s]*organisatorische\s+ma(ß|ss)nahmen?\b",
r"\bTOM\s+(umgesetzt|dokumentiert|implementiert)",
r"\btom\s+(umgesetzt|dokumentiert|implementiert)",
r"\bverschluesselung\s+(implementiert|aktiv)",
r"\bpseudonymisierung\s+(implementiert|aktiv)",
r"\bbackup[s]?\s+(eingerichtet|vorhanden)",
@@ -68,7 +70,11 @@ _PROCESS_INTERNAL_PATTERNS = [
r"\bbitte\s+(intern\s+)?dokumentieren",
r"\bin\s+der\s+verfahrens",
r"\bnach\s+innen\s+geh",
r"\bausnahmen\s+(dokumentieren|protokollieren)",
# "Ausnahmen ... dokumentieren" — Wortabstand bis 60 Zeichen erlauben,
# damit "ausnahmen bei bereits vorhandenen informationen dokumentieren"
# matcht. Sonst Pattern zu eng → process-internal-MC bleibt FAIL.
r"\bausnahmen\b[^\n]{0,60}\b(dokumentieren|protokollieren)\b",
r"\bdokumentations[\s-]?pflicht",
r"\bkostenfrei\s+(zur\s+verfuegung|gewaehren|ermoegli)",
r"\bunentgeltlich\s+(zur\s+verfuegung)",
# Vertragsleistung / Service-Level (intern)
@@ -0,0 +1,100 @@
"""Heuristic-coverage tests for mc_audit_type.classify_mc_audit_type.
Hand-kuratierte Beispiele aus DSGVO-/AI-Act-/eCall-Kontext, die nahe an
den real-world FAILs aus VW-/BMW-Audits liegen. Wenn ein Pattern hier
fehlschlaegt, fehlt _PROCESS_INTERNAL_PATTERNS bzw. _VERIFIABLE_PATTERNS
eine entsprechende Regel.
"""
import pytest
from compliance.services.mc_audit_type import classify_mc_audit_type
VERIFIABLE_CASES = [
("Datenschutzerklaerung nennt Kontaktdaten des Verantwortlichen",
"Steht im Impressum / DSE der Name + Adresse + Email?"),
("Cookie-Banner enthaelt Ablehnen-Button",
"Ist im Cookie-Banner ein 'Ablehnen' Button sichtbar?"),
("Datenschutzerklaerung verweist auf Recht auf Auskunft",
"Wird in der Datenschutzerklaerung Art. 15 DSGVO erwaehnt?"),
("Cookie-Richtlinie listet alle Drittanbieter",
"Sind alle Vendors in der Cookie-Richtlinie aufgefuehrt?"),
("Impressum nennt Telefonnummer",
"Steht im Impressum eine Kontakttelefonnummer?"),
("Datenschutzerklaerung nennt Speicherdauer",
"Sind in der DSE die Aufbewahrungsfristen angegeben?"),
("Cookie-Banner Save-Label entspricht UWG-Vorgabe",
"Ist der Speichern-Button im Banner korrekt beschriftet?"),
]
PROCESS_INTERNAL_CASES = [
("Mitarbeiter-Schulung zu Datenschutz durchgefuehrt",
"Wurden alle Mitarbeiter zum Thema Datenschutz geschult?"),
("AVV mit allen Auftragsverarbeitern abgeschlossen",
"Liegt fuer jeden Dienst ein Auftragsverarbeitungsvertrag vor?"),
("TOM dokumentiert und implementiert",
"Sind die technisch-organisatorischen Massnahmen umgesetzt?"),
("DSFA fuer Hochrisiko-Verarbeitung durchgefuehrt",
"Wurde eine DSFA durchgefuehrt und dokumentiert?"),
("Rollenkonzept eingerichtet",
"Existiert ein Berechtigungskonzept mit klaren Rollen?"),
("Pseudonymisierung von Daten implementiert",
"Wird Pseudonymisierung aktiv eingesetzt?"),
("Backup-Strategie eingerichtet",
"Sind Backups vorhanden und werden regelmaessig getestet?"),
("Abschaltung der Standortdatenverarbeitung kostenfrei ermoeglichen",
"Kann der Nutzer die Standortdatenverarbeitung kostenfrei abschalten?"),
("Ausnahmen bei bereits vorhandenen Informationen dokumentieren",
"Werden Ausnahmen intern dokumentiert?"),
("Sensibilisierung neue Mitarbeiter",
"Erhalten neue Mitarbeiter eine Awareness-Schulung?"),
("72-Stunden-Meldung an Aufsichtsbehoerde eingehalten",
"Wird die Meldepflicht innerhalb von 72 Stunden umgesetzt?"),
]
DOC_INTERNAL_CASES = [
("Verzeichnis von Verarbeitungstaetigkeiten gefuehrt",
"Existiert ein VVT mit allen Pflichtangaben?"),
("Subprozessor-Liste aktualisiert",
"Wird die Sub-Prozessor-Liste gepflegt?"),
("Auftragsverarbeitungsverzeichnis vollstaendig",
"Sind alle AVVs im Auftragsverarbeitungsverzeichnis erfasst?"),
("Aufbewahrungskonzept dokumentiert",
"Existiert ein dokumentiertes Aufbewahrungskonzept?"),
]
@pytest.mark.parametrize("title,question", VERIFIABLE_CASES)
def test_verifiable_cases(title: str, question: str) -> None:
"""These MCs MUST be classified as verifiable — they describe things
visible from the outside (website / banner / DSE)."""
result = classify_mc_audit_type(title, question)
assert result == "verifiable", (
f"Expected 'verifiable' for {title!r}, got {result!r}"
)
@pytest.mark.parametrize("title,question", PROCESS_INTERNAL_CASES)
def test_process_internal_cases(title: str, question: str) -> None:
"""These MCs MUST be classified as process_internal — they describe
internal customer processes that we cannot verify from the outside."""
result = classify_mc_audit_type(title, question)
assert result == "process_internal", (
f"Expected 'process_internal' for {title!r}, got {result!r}"
)
@pytest.mark.parametrize("title,question", DOC_INTERNAL_CASES)
def test_doc_internal_cases(title: str, question: str) -> None:
"""These MCs MUST be classified as doc_internal — they describe
internal documentation (VVT, AVV-Verzeichnis) we cannot inspect."""
result = classify_mc_audit_type(title, question)
assert result == "doc_internal", (
f"Expected 'doc_internal' for {title!r}, got {result!r}"
)
def test_empty_input_is_ambiguous() -> None:
assert classify_mc_audit_type("", "") == "ambiguous"
assert classify_mc_audit_type(None, None) == "ambiguous"