""" P102 — Cookie-Library-Mismatch-Detection pro Site. Vergleicht die in einem Lauf erfassten Cookies (mit deklarierter Kategorie aus dem Cookie-Doc-Text) gegen die Library (compliance.cookie_library). Liefert Mismatches: deklariert ≠ Library. Genutzt im Mail-Render als neuer Block "Cookie-Klassifikations-Pruefung". """ from __future__ import annotations import logging import re from sqlalchemy import text from sqlalchemy.orm import Session logger = logging.getLogger(__name__) _CATEGORY_PATTERNS = [ (re.compile(r"\b(?:strictly[-\s]?)?(?:notwendig|essential|funktional|" r"funktionscookie|technisch[- ]?notwendig)\b", re.I), "essential"), (re.compile(r"\b(?:tracking|analytics|analyse|statistik|" r"measurement|performance)\b", re.I), "statistics"), (re.compile(r"\b(?:marketing|werbung|advertising|targeting|" r"drittanbieter[- ]?cookie)\b", re.I), "marketing"), (re.compile(r"\b(?:social[-\s]?media|share|like)\b", re.I), "social_media"), ] def _category_for(name: str, doc_text: str) -> str | None: if not doc_text or not name: return None idx = doc_text.find(name) if idx < 0: return None window = doc_text[max(0, idx - 50):idx + 400] for pat, cat in _CATEGORY_PATTERNS: if pat.search(window): return cat return None def _load_library(db: Session) -> dict[str, dict]: rows = db.execute(text( "SELECT cookie_name, actual_category, vendor_name " "FROM compliance.cookie_library" )).fetchall() return {r[0].lower(): {"category": r[1], "vendor": r[2]} for r in rows} def detect_mismatches( db: Session, cookie_names_seen: list[str], doc_text: str, ) -> list[dict]: """Returns list of finding dicts.""" if not cookie_names_seen or not doc_text: return [] lib = _load_library(db) findings: list[dict] = [] seen: set[str] = set() for cname in cookie_names_seen: cname = (cname or "").strip() if not cname or cname.lower() in seen: continue seen.add(cname.lower()) declared = _category_for(cname, doc_text) if not declared: continue lib_entry = lib.get(cname.lower()) if not lib_entry: continue lib_cat = lib_entry["category"] if lib_cat in (None, "unknown") or lib_cat == declared: continue # HIGH wenn Library sagt Marketing aber Site als essential/statistics # deklariert (faktische Drittland-/Werbe-Verarbeitung versteckt # als technische/statistische Notwendigkeit). MEDIUM sonst. severity = "HIGH" if ( lib_cat == "marketing" and declared in ("essential", "statistics") ) else "MEDIUM" findings.append({ "cookie": cname, "declared_category": declared, "library_category": lib_cat, "library_vendor": lib_entry["vendor"], "severity": severity, }) return findings def build_mismatch_block_html(findings: list[dict]) -> str: """Render the mismatch findings as a Mail-Block.""" if not findings: return "" n_high = sum(1 for f in findings if f["severity"] == "HIGH") items: list[str] = [] for f in findings[:25]: sev_color = "#dc2626" if f["severity"] == "HIGH" else "#d97706" items.append( f'
'
f'{f["cookie"]} '
f'— deklariert als '
f'{f["declared_category"]}, '
f'unsere Bibliothek + verbreitete '
f'Vendor-Doku sagen '
f'{f["library_category"]} '
f'(Vendor: {f["library_vendor"]})'
f'' 'Wir haben die in Ihrer Cookie-Richtlinie deklarierte Kategorie der ' 'Cookies mit unserer globalen Bibliothek (~2.300 Cookies aus Open-' 'Cookie-Database + DACH-spezifischen Quellen) und der verbreiteten ' 'Vendor-Doku abgeglichen. Bei den folgenden Cookies stimmt die ' 'deklarierte Kategorie nicht mit dem typischerweise erwarteten ' 'Zweck ueberein. Das ist kein automatischer Verstoss — aber ein ' 'Pruefanlass: bei Marketing-Cookies braucht es Einwilligung, bei ' 'als "essential" deklarierten nicht. Empfehlung: mit DSB / ' 'Marketing-Agentur klaeren ob die Klassifikation korrigiert ' 'oder die Einwilligung anders eingeholt werden muss.
' 'Hintergrund: Art. 13(1)(c) DSGVO + EDPB 5/2020 ' '— der angegebene Verarbeitungszweck muss dem tatsaechlichen ' 'entsprechen.
' '