"""B11 — AI-Retention-Granularity-Check (TH-RETENTION-002). DSGVO Art. 13 Abs. 2 lit. a + DSK-Empfehlung: pro Datenkategorie eine spezifische Speicherdauer. Eine pauschale Angabe wie "6 Monate für alle Daten" reicht nicht. GT-Pattern Elli: Vertex-AI-Chatbot speichert "IT- und pseudonymisierte Nutzungsdaten" pauschal 6 Monate. Keine Abstufung nach Datenkategorie (Texteingaben / IP / Geräteinformationen / Session-ID / Fehlerprotokolle). Heuristik: 1. AI-Kontext erkennen (vertex ai / openai / claude / etc.) 2. In ±600-char-Window prüfen: - Existiert eine Speicherdauer-Aussage? (parse_duration_to_days) - Werden ≥2 Datenkategorien aus AI-Standardliste genannt? (Texteingaben, IP, Geräteinformationen, Session, Fehlerprotokolle) - Wenn 1 Speicherdauer + ≥2 Kategorien aber kein per-Kategorie-Differential → LOW """ from __future__ import annotations import logging import re from .retention_comparator import parse_duration_to_days logger = logging.getLogger(__name__) _AI_PROVIDERS = ( "vertex ai", "google vertex", "openai", "gpt-3", "gpt-4", "chatgpt", "anthropic", "claude.ai", "claude-3", "mistral ai", "ki-assistent", "ki assistent", "ai assistant", ) _AI_DATA_CATEGORIES = ( "texteingab", # Texteingaben / Texteingabe "chatverlauf", "chatverläuf", "ip-adress", "geräteinform", "geraeteinform", "device-info", "session-id", "sitzungs-id", "browserversion", "user-agent", "fehlerprotokoll", "zeitstempel", ) def _per_category_phrases() -> tuple[str, ...]: """Patterns indicating per-category retention is mentioned.""" return ( "pro datenkategorie", "je datenkategorie", "unterschiedlich je", "abhängig vom datentyp", "abhaengig vom datentyp", "differenziert nach", "pro kategorie", ) def check_ai_retention_granularity(state: dict) -> list[dict]: doc_texts = state.get("doc_texts") or {} dse = (doc_texts.get("dse") or "").lower() if not dse: return [] findings: list[dict] = [] for ai_kw in _AI_PROVIDERS: idx = dse.find(ai_kw) if idx < 0: continue window = dse[max(0, idx - 800): idx + 800] if not window: continue categories_found = [c for c in _AI_DATA_CATEGORIES if c in window] if len(categories_found) < 2: continue # Per-category retention phrase already present? then OK if any(p in window for p in _per_category_phrases()): return [] # Retention-claim in window? parse duration m = re.search( r"(\d+(?:[.,]\d+)?\s*(?:tage?|monat\w*|jahre?|" r"day|month|year))", window, ) if not m: continue days, kind = parse_duration_to_days(m.group(1)) if days is None: continue findings.append({ "check_id": "TH-RETENTION-GRANULARITY-001", "severity": "LOW", "severity_reason": "incomplete", "title": ( "AI-Speicherdauer pauschal — pro Datenkategorie " "differenzieren empfohlen" ), "norm": "DSGVO Art. 13 Abs. 2 lit. a + DSK-OH AI", "ai_provider": ai_kw, "retention_days": int(days), "categories_detected": categories_found, "action": ( f"Für '{ai_kw}'-Kontext separate Speicherdauern je " f"Datenkategorie angeben (Texteingaben / IP / " f"Geräteinformationen / Session). Aktuell pauschal " f"{int(days)} Tage." ), }) break # one per DSE is enough if findings: logger.info("B11 AI-retention-granularity: %d findings", len(findings)) return findings