Automated comparison: services mentioned in privacy policy vs. actually embedded on website. Three categories: undocumented (Art. 13 violation), outdated (cleanup), correctly documented (check third country only). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
21 KiB
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):
{
"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_conditionsincompliance.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[]mitrelevant: 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
-- 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)
- Migration:
relevance_conditionsFeld zucanonical_controls - Seed-Script: Top-20 generische Controls mit Bedingungen versehen (C_TRANSPARENCY, C_EXPLICIT_CONSENT, C_DSFA_REQUIRED, etc.)
- Filter-Funktion in
agent_analyze_routes.py - Test: Opodo erneut analysieren — C_TRANSPARENCY sollte rausfallen
Phase 2: LLM-Validierung (1 Tag)
- Neuer SDK-Endpoint
/sdk/v1/ucca/validate-controls - Integration in den Agent-Workflow
- Caching-Layer (Redis/Valkey)
Phase 3: Batch-Seeding (2-3 Tage)
- Pipeline-Job: Fuer alle 166k Controls
relevance_conditionsgenerieren (LLM-gestuetzt: "Welche Keywords im Quelltext wuerden diesen Control relevant machen?") - 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
# 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
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:
# 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?"
Phase 4b: Soll-Ist-Abgleich (Dienstleister DSE vs. Website)
Der wertvollste Output des Agents: automatischer Abgleich zwischen dem was in der Datenschutzerklaerung STEHT und dem was tatsaechlich auf der Website EINGEBUNDEN ist.
Schritt 1: IST — Website scannen (bereits in Phase 4) Alle eingebundenen externen Dienste per HTML/Script-Analyse erkennen.
Schritt 2: SOLL — Datenschutzerklaerung parsen Aus dem DSE-Text extrahieren welche Dienstleister erwaehnt werden:
# Qwen/LLM extrahiert strukturiert:
PROMPT = """
Extrahiere aus dieser Datenschutzerklaerung ALLE erwaehnten Dienstleister/Tools.
Fuer jeden Dienstleister nenne:
- Name (z.B. "Google Analytics")
- Zweck (z.B. "Webanalyse")
- Land/Sitz (z.B. "USA")
- Genannte Rechtsgrundlage (z.B. "Einwilligung" oder "berechtigtes Interesse")
- Genannte Schutzmassnahme (z.B. "Standardvertragsklauseln")
Antworte als JSON-Array.
"""
Schritt 3: Abgleich → 3 Kategorien
| Kategorie | Bedeutung | Finding-Typ |
|---|---|---|
| Eingebunden + NICHT in DSE | Informationspflicht verletzt | HIGH — Art. 13 DSGVO Verstoss |
| In DSE + NICHT eingebunden | Veraltete/irrefuehrende DSE | LOW — Aufraumbedarf |
| Eingebunden + in DSE | Korrekt dokumentiert | OK — nur Drittland pruefen |
Beispiel-Output fuer Opodo:
Dienstleister-Abgleich (opodo.de)
══════════════════════════════════
Eingebunden auf Website In DSE erwaehnt? Status
─────────────────────────────── ─────────────────── ───────
Google Analytics (G-03F834EHLM) Ja (Abschnitt 3.6) ✓ OK — aber USA, SCCs pruefen
Didomi CMP Ja (Cookie Notice) ✓ OK — Frankreich/EU
Bootstrap CDN (jsdelivr) Nein ✗ FINDING: Nicht in DSE
Google Tag Manager Ja (Abschnitt 3.6) ✓ OK
In DSE erwaehnt Auf Website gefunden? Status
─────────────────────────────── ───────────────────── ───────
Amadeus IT (Buchungssystem) Nicht pruefbar ? Backend-Dienst
Adyen (Zahlungsabwicklung) Nicht pruefbar ? Backend-Dienst
Salesforce (CRM) Nicht pruefbar ? Backend-Dienst
Zusammenfassung:
- 1 Dienstleister eingebunden aber NICHT in DSE dokumentiert (jsdelivr CDN)
- 3 Backend-Dienste in DSE erwaehnt, nicht im Frontend pruefbar
- Empfehlung: jsdelivr CDN in Datenschutzerklaerung aufnehmen oder lokal hosten
Dieser Output allein ist fuer einen Datenschutzbeauftragten Gold wert — er spart Stunden manueller Arbeit und deckt Luecken auf die bei Website-Updates entstehen.
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)
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
- Opodo-Test: C_TRANSPARENCY sollte NICHT mehr empfohlen werden (kein KI-Nachweis)
- Chatbot-Anbieter: C_TRANSPARENCY SOLL empfohlen werden (KI explizit erwaehnt)
- Arztpraxis-Website: C_DSFA_REQUIRED SOLL empfohlen werden (Gesundheitsdaten)
- Blog ohne Tracking: Nur minimale Controls (Impressum, Datenschutzerklaerung)