Files
breakpilot-compliance/backend-compliance/compliance/services/ai_act_transparency_check.py
T
Benjamin Admin d0e3621192 feat(audit): V2 mail render + 5 new findings (B4/B5/B6/B7/B8) + LLM-Plausibility-Phase
Mail Render V2 (compliance/services/mail_render_v2/) — 11-Modul-Subpackage
das einen einheitlichen Audit-Mail-Output erzeugt mit:
  - Header + KPI-Kacheln (Score / Findings / Docs / Vendors)
  - TOC + Sprung-Links
  - 3-Bucket-Trennung: Kritische Befunde / Manuelle Prüfung / Interne Reminder
  - Cookie-Inventar (Name·Vendor·Kategorie·Speicherdauer·Löschfrist·Sitzland·Quelle·Status)
  - Sofortmaßnahmen-Aggregator ("Sitzland ergänzen für 11 Cookies")
  - 24 Legacy-Wrappers — alle alten build_*_html in V2-Sections
  - Scope-Filter: FIN/GOV/MED/INS/EDU/LEG aus Berichten wenn nicht relevant
  - Hint/Action-Dedup: keine doppelten Sätze pro Card mehr
Aktiviert via env MAIL_RENDER_V2=true (Default: legacy renderer).

5 neue deterministische Findings als Phase D-2b/B4/B5/B6/B7/B8:

  B4 vendor_consistency_check — Cross-Doc-Provider-Widerspruch
     (Elli: DSE nennt Vertex AI für Chatbot, /de/cookies nennt Iadvize → HIGH).
     6 Service-Types: chatbot/analytics/tag_manager/pixel/cdn/cmp.

  B5 ai_act_transparency_check — AI Act Art. 50 Transparenzpflicht
     (Elli: Vertex AI vorhanden ohne Pre-Chat-Disclosure → HIGH).
     Plus B5-Erweiterung: Rechtsgrundlage Art-6-Abs-1-lit-f bei AI → MED
     (Einwilligung empfehlen).

  B6 cross_doc_dpo_check — DPO in DSE genannt, nicht im Impressum (LOW).

  B7 doc_staleness_check — Datum-Extraktion aus DSE/AGB/Nutzungsbedingungen.
     Cap: AGB/NB 3y, DSE 2y. Älter → MEDIUM (Elli NB Stand 2018 → HIGH).

  B8 cmp_fingerprint_check — Banner detected, aber CMP-Provider generic
     (kein Usercentrics/OneTrust/Cookiebot/etc → MED).

  B3-Erweiterung detect_intra_doc_contradictions — Widersprüchliche
     Speicherdauer im SELBEN Doc (Elli: Logfile 7d vs 30d → HIGH).

LLM-Plausibility-Phase (Phase D-2b, finding_plausibility_check.py):
  - Läuft AFTER MC pipeline, BEFORE D3 render
  - Prompt mit Beispiel-IDs + 3-Phase-Mapping: exact-ID / position-fallback /
    fuzzy-tail-match
  - Stempelt llm_title / llm_severity / llm_recommendation / llm_drop auf
    jeden FAIL CheckItem
  - V2-Render zeigt "🤖 LLM-Plausibility:" Box pro Finding wenn gestempelt
  - KNOWN ISSUE: qwen3:30b-a3b liefert oft empty content auf format='json' +
    8000-char-excerpt prompts. Pipeline läuft mit stamped=0 weiter. Task #16.

Coverage gegen Elli Ground Truth (zeroclaw/docs/ground-truth/elli_eco_2026-06-06.json,
13 expected findings via WebFetch-Agent-Crawl):
  - 4/4 HIGH-Findings ✓ (COOKIE-CONSENT-UX-001 + WIDERRUFSBELEHRUNG-001 +
    VENDOR-CONSISTENCY-001 + AI-ACT-TRANSPARENCY-001)
  - 4/6 MEDIUM ✓
  - 2/3 LOW ✓
  - Total: 10/13 = 77% (Sprung von 4/13 = 31%)

Restliche 3 Gaps als Task #17: IMPRESSUM-001 (multi-entity USt-IdNr),
TRANSFER-001 (Vendor-Mechanismus DPF/SCC), TH-RETENTION-002 (AI-Retention
pro Datenkategorie).

V2-Mail-Preview in Mailpit: 'v2all@local.test' Subject '[V2 ALL] ELLI'.
Backend healthy, B1+B3+B4+B5+B6+B7+B8 alle live im Orchestrator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 21:19:49 +02:00

222 lines
8.3 KiB
Python

"""AI-Act Art. 50 Transparenzpflicht-Check (vereinfacht).
Art. 50 AI Act verlangt, dass Nutzer beim Interagieren mit einem
KI-System (Chatbot, Sprachassistent etc.) erkennen können, dass sie
mit einer KI sprechen — es sei denn, das ist offensichtlich aus dem
Kontext heraus.
Der Check ist heuristisch (kein LLM) und prüft drei Schichten:
1. AI-Provider-Detection in DSE und Vendor-Liste
(Vertex AI, OpenAI, Anthropic, etc.)
2. Disclosure-Text-Detection in DSE / Cookie-Doc
("KI-System", "Sie chatten mit einer KI", "automatisiert",
"Artificial Intelligence", "Konversations-KI", "GPT", …)
3. Cross-Check: AI-Provider gefunden + keine Disclosure → HIGH
AI-Provider gefunden + Disclosure vorhanden, aber kein "Sie
interagieren mit einer KI"-Hinweis → MEDIUM (Pre-Chat-Hinweis
vor erstem Input gefordert; kann nur ein consent-tester-DOM-Scan
verifizieren)
Bekannte Limitation: ohne consent-tester-Erweiterung kann der Check
nicht entscheiden, ob ein Pre-Chat-Hinweis im Live-DOM vor dem
ersten Nutzer-Input erscheint. Wir flaggen das daher als MEDIUM
"manuell verifizieren".
"""
from __future__ import annotations
import logging
import re
logger = logging.getLogger(__name__)
# AI-Anbieter / Modelle / Frameworks, die "AI" auslösen.
_AI_KEYWORDS = (
"vertex ai", "google vertex", "openai", "gpt-3", "gpt-4", "chatgpt",
"anthropic", "claude.ai", "claude-3", "mistral ai", "huggingface",
"hugging face", "stable diffusion", "midjourney", "llama-2", "llama-3",
"qwen", "deepseek", "perplexity ai", "azure openai", "copilot",
"konversations-ki", "konversations ki",
"ai assistant", "ai-assistant", "ki-assistent", "ki assistent",
"intelligenter assistent",
"automatisierter chat", "chatbot", "live-chat",
)
# Phrasen, die als Art-50-Disclosure gelten.
_DISCLOSURE_PHRASES = (
"sie chatten mit einer ki", "sie kommunizieren mit einer ki",
"automatisierter chat", "automatisierter assistent",
"ki-gestützter", "ki gestützt", "ki-gestuetzt",
"künstliche intelligenz", "kuenstliche intelligenz",
"artificial intelligence",
"art. 50 ai act", "ai act art. 50", "art. 50 ki-vo",
"ki-verordnung", "ki verordnung",
"automatisiertes system",
"generativ", "generative ki", "generative ai",
"large language model", "llm-",
"machine learning",
)
def _has_any(text: str, phrases) -> list[str]:
text_lc = (text or "").lower()
if not text_lc:
return []
return [p for p in phrases if p in text_lc]
def _find_ai_in_vendors(cmp_vendors: list[dict]) -> list[dict]:
"""Find vendors whose name or category mentions an AI provider."""
hits: list[dict] = []
for v in cmp_vendors or []:
haystack = " ".join([
(v.get("name") or "").lower(),
(v.get("category") or "").lower(),
(v.get("processing_company") or "").lower(),
])
if not haystack.strip():
continue
matched = [k for k in _AI_KEYWORDS if k in haystack]
if matched:
hits.append({
"vendor": v.get("name") or "",
"matched": matched[:3],
})
return hits
def check_ai_act_transparency(state: dict) -> list[dict]:
"""Return findings about AI Art. 50 transparency obligations."""
doc_texts = state.get("doc_texts") or {}
dse_text = doc_texts.get("dse") or ""
cookie_text = doc_texts.get("cookie") or ""
cmp_vendors = state.get("cmp_vendors") or []
if not dse_text and not cookie_text and not cmp_vendors:
return []
ai_mentions_dse = _has_any(dse_text, _AI_KEYWORDS)
ai_mentions_cookie = _has_any(cookie_text, _AI_KEYWORDS)
ai_vendors = _find_ai_in_vendors(cmp_vendors)
has_ai_signal = bool(
ai_mentions_dse or ai_mentions_cookie or ai_vendors
)
if not has_ai_signal:
return []
disc_dse = _has_any(dse_text, _DISCLOSURE_PHRASES)
disc_cookie = _has_any(cookie_text, _DISCLOSURE_PHRASES)
has_disclosure = bool(disc_dse or disc_cookie)
findings: list[dict] = []
summary_signals = (
f"DSE-AI-Hinweise: {len(ai_mentions_dse)} "
f"(z.B. {', '.join(ai_mentions_dse[:3])}); "
f"Cookie-AI-Hinweise: {len(ai_mentions_cookie)}; "
f"AI-Vendors: {len(ai_vendors)}"
)
if not has_disclosure:
findings.append({
"check_id": "AI-ACT-TRANSPARENCY-001",
"severity": "HIGH",
"severity_reason": "missing",
"title": (
"AI-Act Art. 50 Transparenz-Hinweis fehlt — "
"KI-System eingesetzt, aber keine Nutzer-Erklärung"
),
"norm": "AI Act Art. 50 Abs. 1 (Transparenz gegenüber Nutzern)",
"detected_signals": summary_signals,
"ai_vendors": ai_vendors,
"ai_keywords_in_dse": ai_mentions_dse[:5],
"ai_keywords_in_cookie": ai_mentions_cookie[:5],
"action": (
"DSE und Pre-Chat-UI mit ausdrücklichem Hinweis "
"'Sie kommunizieren mit einer KI (System X)' ergänzen. "
"Anbieter offen nennen + Rechtsgrundlage + Speicherdauer."
),
})
else:
# AI detected + DSE-Disclosure vorhanden — aber Pre-Chat-Hinweis
# im Live-DOM kann der Check nicht verifizieren.
findings.append({
"check_id": "AI-ACT-TRANSPARENCY-002",
"severity": "MEDIUM",
"severity_reason": "manual_review_required",
"title": (
"AI-Act Art. 50: DSE-Disclosure vorhanden — Pre-Chat-Hinweis "
"im UI manuell verifizieren"
),
"norm": "AI Act Art. 50 Abs. 1",
"detected_signals": summary_signals,
"ai_vendors": ai_vendors,
"disclosure_in_dse": disc_dse[:3],
"disclosure_in_cookie": disc_cookie[:3],
"action": (
"Pre-Chat-UI öffnen: vor der ersten Nutzereingabe muss "
"ein klarer Hinweis erscheinen, dass die Konversation "
"mit einer KI geführt wird. Verifizieren ob Banner/Modal "
"vorhanden oder reine Footnote."
),
})
# Zusatzcheck: Wenn AI vorhanden und Rechtsgrundlage = berechtigtes
# Interesse (Art. 6 Abs. 1 lit. f) statt Einwilligung — MEDIUM
if ai_vendors or ai_mentions_dse:
if _legitimate_interest_for_ai(dse_text):
findings.append({
"check_id": "AI-ACT-RISK-001",
"severity": "MEDIUM",
"severity_reason": "misclassified",
"title": (
"Rechtsgrundlage 'berechtigtes Interesse' für "
"KI-Verarbeitung — Einwilligung empfehlen"
),
"norm": "DSGVO Art. 6 Abs. 1 lit. a vs lit. f + AI Act",
"detected_signals": (
"AI-Provider erkannt; Art. 6 Abs. 1 lit. f als "
"Rechtsgrundlage in DSE genannt"
),
"ai_vendors": ai_vendors,
"action": (
"Bei generativer KI (insbesondere mit Drittland-"
"Transfer und Profiling-Verwandtschaft) "
"Rechtsgrundlage auf Einwilligung (Art. 6 Abs. 1 "
"lit. a) umstellen. Interessenabwägung dokumentieren."
),
})
if findings:
logger.info("ai-act-transparency: %d findings", len(findings))
return findings
def _legitimate_interest_for_ai(dse_text: str) -> bool:
"""Detect 'Rechtsgrundlage Art. 6 Abs. 1 lit. f' near AI mentions."""
text_lc = (dse_text or "").lower()
if not text_lc:
return False
# crude proximity check: any of the AI keywords AND lit-f phrase
# within a ~600 char window
import re
lit_f_patterns = (
"art. 6 abs. 1 lit. f", "artikel 6 abs. 1 lit. f",
"art. 6 1 f", "berechtigtes interesse", "berechtigten interesses",
)
for ai_kw in _AI_KEYWORDS:
for pos in range(0, len(text_lc) - 200):
window = text_lc[max(0, pos-300):pos+300]
if ai_kw in window and any(p in window for p in lit_f_patterns):
return True
# don't walk every char; jump to next ai_kw occurrence
idx = text_lc.find(ai_kw, pos)
if idx == -1:
break
pos = idx + 1
break
return False