# Plan: Control Relevance Filter — Generische Controls kontextsensitiv filtern ## Problem Die UCCA-Engine empfiehlt Controls pauschal basierend auf Intake-Flags (Boolean-Felder wie `personal_data: true`, `marketing: true`). Sie prueft NICHT, ob der analysierte Text die Bedingungen fuer einen spezifischen Control tatsaechlich erfuellt. ### Konkretes Beispiel (Opodo-Test, 2026-04-28) - **Control:** `[C_TRANSPARENCY] Nutzer informieren dass sie mit KI interagieren` - **Quelle:** AI Act Art. 52 — nur relevant wenn KI eingesetzt wird - **Opodo sagt:** "automated processing" (kann regelbasierte Software sein, muss keine KI sein) - **Ergebnis:** False Positive — Control wird empfohlen obwohl kein KI-Einsatz belegt ist ### Skalierung Von ~166.740 Controls in der RAG-Datenbank wird ein unbekannter Prozentsatz bei jeder Bewertung generisch empfohlen. Jedes False Positive untergräbt das Vertrauen des Nutzers und macht das Tool fuer Abmahnungen unbrauchbar. ## Loesung: 3-Stufen Relevance Filter ### Stufe 1: Regelbasierter Vorfilter (deterministisch, schnell) Jeder Control bekommt ein `relevance_conditions` Feld (JSON): ```json { "control_id": "C_TRANSPARENCY", "relevance_conditions": { "text_must_contain_any": ["KI", "kuenstliche Intelligenz", "artificial intelligence", "machine learning", "maschinelles Lernen", "neural", "deep learning", "AI system", "AI-System", "algorith"], "text_must_not_contain": [], "requires_intake_flag": "automation", "min_confidence": 0.5 } } ``` **Implementierung:** - Neues Feld `relevance_conditions` in `compliance.canonical_controls` (JSONB) - Funktion `check_relevance(control, source_text) -> (relevant: bool, confidence: float)` - Laeuft NACH dem UCCA-Assessment, BEVOR das Ergebnis zurueckgegeben wird - Filtert Controls raus deren Keywords im Quelltext nicht vorkommen **Aufwand:** ~200 LOC Python, kein LLM-Call noetig **Datei:** `ai-compliance-sdk/internal/ucca/relevance_filter.go` oder `backend-compliance/compliance/services/relevance_filter.py` ### Stufe 2: LLM-Validierung (fuer High-Value Controls) Fuer Controls mit `severity >= HIGH` oder wenn der regelbasierte Filter unsicher ist (confidence < 0.7), wird Qwen gefragt: ``` Gegeben dieser Dokumenttext: "[...Auszug...]" Ist der folgende Control relevant fuer dieses Dokument? Control: "[C_TRANSPARENCY] Nutzer informieren dass sie mit KI interagieren" Rechtsgrundlage: Art. 52 AI Act Antworte NUR mit: JA (mit Begruendung) oder NEIN (mit Begruendung) ``` **Implementierung:** - Neuer Endpoint: `POST /sdk/v1/ucca/validate-controls` - Nimmt: `assessment_id`, `source_text`, `controls[]` - Gibt zurueck: `controls[]` mit `relevant: bool`, `reason: string` - Cached: Gleicher Text + Control = gleiche Antwort (24h TTL) **Aufwand:** ~150 LOC, 1 LLM-Call pro Control (parallelisierbar) ### Stufe 3: Follow-Up-Fragen an den Nutzer (Hybrid) Wenn weder Regel noch LLM sicher entscheiden koennen: ``` Follow-Up: "Setzt der Anbieter KI oder maschinelles Lernen ein?" → Ja: Control bleibt → Nein: Control wird entfernt → Unsicher: Control bleibt mit Hinweis "Nicht verifizierbar" ``` **Bereits implementiert:** Das `follow_up_questions` System im Agent-Endpoint. ## Datenmodell-Aenderung ```sql -- Neues Feld in canonical_controls ALTER TABLE compliance.canonical_controls ADD COLUMN IF NOT EXISTS relevance_conditions JSONB DEFAULT '{}'; -- Index fuer schnelle Abfrage CREATE INDEX IF NOT EXISTS idx_controls_relevance ON compliance.canonical_controls USING gin (relevance_conditions); ``` ## Architektur ``` UCCA Assessment │ ▼ ┌────────────────────┐ │ Stufe 1: Regelfilter│ ← text_must_contain_any, intake_flags │ (deterministisch) │ └────────┬───────────┘ │ unsicher oder high-severity ▼ ┌────────────────────┐ │ Stufe 2: LLM-Check │ ← Qwen validiert Relevanz │ (1 Call/Control) │ └────────┬───────────┘ │ immer noch unsicher ▼ ┌────────────────────┐ │ Stufe 3: Follow-Up │ ← Nutzer beantwortet Frage │ (Frontend) │ └────────────────────┘ ``` ## Implementierungsreihenfolge ### Phase 1: Regelfilter (1 Tag) 1. Migration: `relevance_conditions` Feld zu `canonical_controls` 2. Seed-Script: Top-20 generische Controls mit Bedingungen versehen (C_TRANSPARENCY, C_EXPLICIT_CONSENT, C_DSFA_REQUIRED, etc.) 3. Filter-Funktion in `agent_analyze_routes.py` 4. Test: Opodo erneut analysieren — C_TRANSPARENCY sollte rausfallen ### Phase 2: LLM-Validierung (1 Tag) 1. Neuer SDK-Endpoint `/sdk/v1/ucca/validate-controls` 2. Integration in den Agent-Workflow 3. Caching-Layer (Redis/Valkey) ### Phase 3: Batch-Seeding (2-3 Tage) 1. Pipeline-Job: Fuer alle 166k Controls `relevance_conditions` generieren (LLM-gestuetzt: "Welche Keywords im Quelltext wuerden diesen Control relevant machen?") 2. Qualitaetspruefung: Stichprobe von 100 Controls manuell validieren ## Betroffene Dateien | Datei | Aenderung | |-------|-----------| | `backend-compliance/compliance/api/agent_analyze_routes.py` | Filter-Integration | | `backend-compliance/compliance/services/relevance_filter.py` | NEU: Regelfilter | | `ai-compliance-sdk/internal/ucca/relevance_filter.go` | NEU: SDK-seitig (alternativ) | | `ai-compliance-sdk/internal/api/handlers/ucca_handlers.go` | Neuer Endpoint | | Migration | `relevance_conditions` Spalte | | `control-pipeline/` | Batch-Seeding Job (Phase 3) | ## Phase 4: Website-Scan (Multi-Page Crawl) ### Problem Aktuell analysieren wir nur EINE URL (z.B. `/datenschutz/`). Aber relevante Hinweise auf KI, Chatbots, automatisierte Entscheidungen oder Tracking koennen auf ANDEREN Seiten der Website stehen: - Chatbot-Widget auf der Startseite (nicht auf der Datenschutzseite) - "Powered by ChatGPT" im Footer - KI-gestuetzte Produktempfehlungen auf der Shopseite - Cookie-Scripts die Tracking-Dienste laden (Google Analytics, Meta Pixel, etc.) - Chatbot-Anbieter wie Intercom, Drift, Zendesk, Tidio im HTML ### Loesung: Lightweight Website-Scan Kein vollstaendiger Crawl (zu langsam, zu invasiv), sondern ein gezielter Scan von 5-10 strategischen Seiten: ``` Eingabe: https://www.opodo.de/datenschutz/ Automatisch gescannte Seiten: 1. Startseite: https://www.opodo.de/ 2. Datenschutz (bereits): https://www.opodo.de/datenschutz/ 3. Impressum: https://www.opodo.de/impressum/ (aus Footer-Links) 4. AGB: https://www.opodo.de/agb/ (aus Footer-Links) 5. Cookie-Policy: https://www.opodo.de/cookies/ (falls vorhanden) ``` ### Scan-Logik **Schritt 1: Startseite holen + Footer-Links extrahieren** ```python # Aus der Startseite die typischen Footer-Links extrahieren: footer_patterns = [ r'href="([^"]*(?:impressum|imprint|legal)[^"]*)"', r'href="([^"]*(?:datenschutz|privacy|dsgvo)[^"]*)"', r'href="([^"]*(?:agb|terms|nutzungsbedingungen)[^"]*)"', r'href="([^"]*(?:cookie|cookies)[^"]*)"', r'href="([^"]*(?:kontakt|contact)[^"]*)"', ] ``` **Schritt 2: Jede Seite auf KI/Chatbot/Tracking-Indikatoren scannen** ```python AI_INDICATORS = { # Chatbot-Widgets (JavaScript-Einbindungen) "chatbot_widgets": [ r"intercom", # Intercom (KI-gestuetzt) r"drift\.com", # Drift Chatbot r"tidio", # Tidio Chat r"zendesk", # Zendesk Chat r"crisp\.chat", # Crisp Chat r"livechat", # LiveChat r"hubspot.*chat", # HubSpot Chat r"tawk\.to", # Tawk.to r"freshchat", # Freshworks r"dialogflow", # Google Dialogflow r"watson.*assistant", # IBM Watson r"chatgpt|openai", # OpenAI/ChatGPT r"anthropic|claude", # Anthropic/Claude ], # KI-Hinweise im Text "ai_text_mentions": [ r"k(?:ue|ü)nstliche.?intelligenz", r"artificial.?intelligence", r"machine.?learning", r"maschinelles.?lernen", r"KI.?gest(?:ue|ü)tzt", r"AI.?powered", r"algorithm", r"automatisierte.?entscheidung", r"automated.?decision", r"profiling", r"personalisier", # Personalisierung ], # Tracking & Analytics (EU + non-EU) "tracking_analytics": [ # Google (USA) r"google.?analytics|gtag|UA-\d+|G-\w+", r"googletagmanager|gtm\.js", r"google.?ads|googleads|adwords", r"doubleclick\.net", # Meta (USA) r"facebook.?pixel|fbq\(|connect\.facebook", r"meta.?pixel", # Microsoft (USA) r"clarity\.ms", # Microsoft Clarity r"bing\.com/bat", # Bing Ads r"linkedin\.com/insight", # LinkedIn Insight # Analytics-Anbieter r"hotjar", # Hotjar (Malta/EU — OK) r"segment\.com", # Segment (USA) r"mixpanel", # Mixpanel (USA) r"amplitude", # Amplitude (USA) r"heap\.io", # Heap (USA) r"posthog", # PostHog (USA, self-host moeglich) r"matomo|piwik", # Matomo (EU — self-host = OK, Cloud = pruefen) r"plausible", # Plausible (EU — OK) r"fathom", # Fathom (Kanada — Angemessenheitsbeschluss) r"pirsch", # Pirsch (DE — OK) r"umami", # Umami (self-host) ], # CDN und Drittanbieter-Dienste (Drittlandtransfer-Risiko) "third_party_services": [ # CDN (pruefen ob Drittland) r"cdn\.cloudflare\.com", # Cloudflare (USA) r"fastly\.net", # Fastly (USA) r"akamai", # Akamai (USA) r"cdn\.jsdelivr\.net", # jsDelivr (international) r"unpkg\.com", # unpkg (USA) r"cdnjs\.cloudflare\.com", # cdnjs (USA) r"stackpath", # StackPath (USA) r"bunny\.net|bunnycdn", # BunnyCDN (Slowenien/EU — OK) r"keycdn", # KeyCDN (Schweiz — Angemessenheit) # Fonts (IP-Uebermittlung!) r"fonts\.googleapis\.com", # Google Fonts (USA — DSGVO-Verstoss!) r"fonts\.gstatic\.com", # Google Fonts CDN r"use\.typekit\.net", # Adobe Fonts (USA) # Captcha r"recaptcha|grecaptcha", # Google reCAPTCHA (USA) r"hcaptcha", # hCaptcha (USA) r"turnstile.*cloudflare", # Cloudflare Turnstile (USA) # Maps r"maps\.googleapis\.com", # Google Maps (USA) r"maps\.google\.com", r"openstreetmap", # OpenStreetMap (EU — OK) r"mapbox", # Mapbox (USA) # Video r"youtube\.com|youtube-nocookie", # YouTube (USA) r"vimeo\.com", # Vimeo (USA) r"wistia", # Wistia (USA) # Social Media Embeds r"platform\.twitter\.com|x\.com/embed", # X/Twitter (USA) r"instagram\.com/embed", # Instagram (USA) r"linkedin\.com/embed", # LinkedIn (USA) # Content Moderation r"besedo", # Besedo (Schweden/EU — OK, aber pruefen) # Payment (PCI-DSS relevant) r"stripe\.com|js\.stripe", # Stripe (USA) r"paypal\.com", # PayPal (USA) r"adyen", # Adyen (NL/EU — OK) r"mollie", # Mollie (NL/EU — OK) # Andere r"sentry\.io|sentry-cdn", # Sentry Error Tracking (USA) r"intercom\.io", # Intercom (USA) — auch in chatbot_widgets r"zendesk\.com", # Zendesk (USA) r"freshdesk|freshworks", # Freshworks (USA/Indien) ], } ``` ### Drittland-Erkennung Fuer jeden erkannten externen Dienst wird geprueft ob er aus einem Drittland stammt (kein EU/EWR-Staat, kein Angemessenheitsbeschluss). Dafuer wird eine Registry gepflegt: ```python # Statische Registry — ca. 80 Eintraege THIRD_PARTY_REGISTRY = { "google_analytics": {"provider": "Google LLC", "country": "US", "eu_adequate": False, "requires_consent": True, "legal_ref": "Art. 44-49 DSGVO, Schrems II"}, "google_fonts": {"provider": "Google LLC", "country": "US", "eu_adequate": False, "requires_consent": True, "legal_ref": "LG Muenchen I, Az. 3 O 17493/20 (Google Fonts Urteil)"}, "facebook_pixel": {"provider": "Meta Platforms", "country": "US", "eu_adequate": False, "requires_consent": True, "legal_ref": "Art. 44-49 DSGVO"}, "cloudflare_cdn": {"provider": "Cloudflare Inc", "country": "US", "eu_adequate": False, "requires_consent": False, "legal_ref": "Art. 44-49 DSGVO, berechtigtes Interesse moeglich"}, "matomo_cloud": {"provider": "Matomo (InnoCraft)", "country": "NZ", "eu_adequate": True, "requires_consent": True, "legal_ref": "Neuseeland hat Angemessenheitsbeschluss"}, "matomo_selfhost": {"provider": "Self-hosted", "country": "depends", "eu_adequate": True, "requires_consent": False, "legal_ref": "Kein Drittlandtransfer bei Self-Hosting"}, "plausible": {"provider": "Plausible Insights", "country": "EE", "eu_adequate": True, "requires_consent": False, "legal_ref": "EU-Anbieter, cookieless"}, "bunnycdn": {"provider": "BunnyCDN d.o.o.", "country": "SI", "eu_adequate": True, "requires_consent": False, "legal_ref": "EU-Anbieter"}, "stripe": {"provider": "Stripe Inc", "country": "US", "eu_adequate": False, "requires_consent": False, "legal_ref": "Art. 6(1)(b) Vertragserfuellung, SCCs"}, "besedo": {"provider": "Besedo AB", "country": "SE", "eu_adequate": True, "requires_consent": False, "legal_ref": "EU-Anbieter"}, # ... ~80 weitere Eintraege } ``` ### Generierte Findings **Beispiel: Opodo mit erweitertem Scan:** ``` Externe Dienste erkannt: - Google Analytics (G-03F834EHLM) — USA, kein Angemessenheitsbeschluss → FINDING: Drittlandtransfer USA ohne Einwilligung (Art. 44 DSGVO) - Google Fonts (fonts.googleapis.com) — USA → FINDING: Google Fonts Einbindung (LG Muenchen I, Az. 3 O 17493/20) - Didomi CMP — Frankreich (EU — OK) - Bootstrap CDN (jsdelivr.net) — International, pruefen → FOLLOW-UP: "Wird das CDN aus der EU oder einem Drittland geladen?" ``` ### Controls die durch Drittland-Dienste ausgeloest werden | Erkannter Dienst | Control | |-----------------|---------| | Jeder US-Dienst ohne SCCs | C_THIRD_COUNTRY_TRANSFER: Drittlandtransfer absichern (Art. 44-49 DSGVO) | | Google Fonts remote | C_GOOGLE_FONTS: Fonts lokal einbinden (LG Muenchen I Urteil) | | Tracking ohne Consent-Banner | C_EXPLICIT_CONSENT: Einwilligung vor Tracking einholen | | reCAPTCHA | C_CAPTCHA_PRIVACY: Datenschutzkonformen Captcha-Dienst nutzen | | YouTube Embed | C_VIDEO_EMBED: 2-Klick-Loesung oder youtube-nocookie verwenden | ``` **Schritt 3: Ergebnis aggregieren** ```python scan_result = { "pages_scanned": 5, "chatbot_detected": True, # z.B. Intercom auf Startseite "chatbot_provider": "intercom", # Identifizierter Anbieter "ai_mentions_found": False, # Kein expliziter KI-Text "tracking_services": ["google_analytics", "facebook_pixel"], "tracking_count": 2, } ``` **Schritt 4: Scan-Ergebnis in Relevanzpruefung einbeziehen** - Chatbot erkannt → C_TRANSPARENCY wird relevant (auch ohne KI-Text) - Tracking erkannt → C_EXPLICIT_CONSENT wird relevant - Kein KI-Nachweis auf gesamter Website → C_TRANSPARENCY faellt weg ### Implementierung **Neue Datei:** `backend-compliance/compliance/services/website_scanner.py` (~200 LOC) ```python class WebsiteScanner: async def scan(self, base_url: str) -> ScanResult: """Scan 5-10 pages for AI, chatbot, and tracking indicators.""" pages = await self._discover_pages(base_url) indicators = {} for page_url in pages[:10]: html = await self._fetch(page_url) indicators[page_url] = self._detect_indicators(html) return self._aggregate(indicators) ``` **Integration in Agent-Workflow:** - Zwischen Schritt 1 (Fetch) und Schritt 3 (UCCA Assess) - Scan-Ergebnis fliesst in die Intake-Flags UND in den Relevanzfilter - Scan-Ergebnis wird im Response zurueckgegeben (Transparenz) **Frontend-Erweiterung:** - "Erweiterte Analyse" Toggle: Nur Einzelseite vs. Website-Scan - Scan-Ergebnis als aufklappbare Sektion: "5 Seiten gescannt, Chatbot auf Startseite erkannt" ### Aufwand | Komponente | LOC | Zeit | |-----------|-----|------| | `website_scanner.py` | ~200 | 0.5 Tage | | Integration in `agent_analyze_routes.py` | ~50 | 2h | | Frontend: Scan-Ergebnis anzeigen | ~80 | 2h | | Tests | ~100 | 2h | ### Beispiel: Opodo mit Website-Scan ``` Seiten gescannt: 5 - https://www.opodo.de/ → Didomi Cookie-Consent, Google Analytics - https://www.opodo.de/datenschutz/ → Datenschutzerklaerung - https://www.opodo.de/impressum/ → 404 (FINDING!) - https://www.opodo.de/agb/ → AGB vorhanden - https://www.opodo.de/cookies/ → Cookie-Policy Chatbot erkannt: Nein KI-Hinweise: Nein Tracking: Google Analytics (G-03F834EHLM), Didomi CMP → C_TRANSPARENCY: NICHT relevant (kein KI-Nachweis auf gesamter Website) → C_EXPLICIT_CONSENT: Relevant (Google Analytics + Didomi = Tracking aktiv) → Impressum-Finding: 404 auf /impressum/ (§5 TMG Verstoss) ``` ## Risiken | Risiko | Mitigation | |--------|------------| | Zu aggressive Filterung (False Negatives) | Stufe 1 nur fuer klare Faelle, Stufe 2 als Fallback | | LLM-Kosten bei vielen Controls | Caching + nur high-severity Controls | | Datenbank-Migration auf Production | `ADD COLUMN IF NOT EXISTS` ist non-blocking | | 166k Controls ohne relevance_conditions | Default `{}` = kein Filter = bisheriges Verhalten | ## Testfaelle 1. **Opodo-Test:** C_TRANSPARENCY sollte NICHT mehr empfohlen werden (kein KI-Nachweis) 2. **Chatbot-Anbieter:** C_TRANSPARENCY SOLL empfohlen werden (KI explizit erwaehnt) 3. **Arztpraxis-Website:** C_DSFA_REQUIRED SOLL empfohlen werden (Gesundheitsdaten) 4. **Blog ohne Tracking:** Nur minimale Controls (Impressum, Datenschutzerklaerung)