"""Storage-Inventory — trennt echte Cookies von anderem Endgeräte-Speicher. Viele CMPs/Scanner werfen Cookies + Local/Session Storage + IndexedDB + Framework-Artefakte in EINE „Cookie"-Liste und erfinden Laufzeiten dazu. § 25 TDDDG ist aber technologieneutral (alle Endgeräte-Speicher gleich). Dieses Modul klassifiziert heuristisch (Name-Muster + Laufzeit-Text) und liefert das Inventar + einen Transparenz-Befund. v1 ohne Scanner-Umbau; echte Erfassung (localStorage/IndexedDB/SW) folgt im consent-tester (v2). """ from __future__ import annotations import re # Salesforce-Lightning/Aura- + typische Framework-/LocalStorage-Artefakte. _FRAMEWORK_RE = re.compile( r"componentdefstorage|globalvalueproviders|__mutex|\blskey|\$vfrc", re.IGNORECASE, ) _SESSION_HINT = ("session storage", "sessionstorage") _INDEXEDDB_HINT = ("indexeddb", "indexed db") _LOCAL_HINT = ("local storage", "localstorage") # „kein Ablauf"-Formulierungen → Persistenzspeicher (kein echtes Cookie). _PERSIST_NOEXPIRY = ( "bis es durch den nutzer", "deaktiviert wird", "bis zur löschung", "bis zur loeschung", "vom nutzer gelöscht", "vom nutzer geloescht", ) STORAGE_LABELS = { "cookie": "Cookie", "local_storage": "Local Storage", "session_storage": "Session Storage", "indexeddb": "IndexedDB", "framework_storage": "Framework-Storage", } def dedupe_vendor_cookies(vendors: list[dict]) -> list[dict]: """Cookies tauchen je Vendor mehrfach auf (Consent-Phasen before_consent / after_accept / after_reject derselben Crawl-Session). Dedupliziert je Vendor nach (lower) Name — behält den ersten. Behebt aufgeblähte Cookie-/Finding- Zahlen (BMW: 2196 → ~772 eindeutig).""" out: list[dict] = [] for v in vendors or []: seen: set[str] = set() uniq: list[dict] = [] for c in (v.get("cookies") or []): n = (c.get("name") or "").strip().lower() if n and n in seen: continue if n: seen.add(n) uniq.append(c) nv = dict(v) nv["cookies"] = uniq out.append(nv) return out def detect_storage_type(name: str, expiry: str = "") -> str: """Heuristik: echtes Cookie vs. anderer Endgeräte-Speicher. Konservativ — im Zweifel 'cookie'. Ist eine VERMUTUNG (kein Scanner-Beleg). """ n = (name or "").strip() e = (expiry or "").lower() if _FRAMEWORK_RE.search(n): return "framework_storage" if any(h in e for h in _SESSION_HINT): return "session_storage" if any(h in e for h in _INDEXEDDB_HINT): return "indexeddb" if any(h in e for h in _LOCAL_HINT): return "local_storage" if any(h in e for h in _PERSIST_NOEXPIRY): return "local_storage" return "cookie" def build_storage_inventory(vendors: list[dict]) -> dict: """Zählt je Speichertyp + liefert Beispiele für Nicht-Cookies.""" by_type: dict[str, int] = {} examples: list[dict] = [] for v in vendors or []: vname = v.get("name") or "?" for c in v.get("cookies") or []: st = detect_storage_type(c.get("name", ""), c.get("expiry", "")) by_type[st] = by_type.get(st, 0) + 1 if st != "cookie" and len(examples) < 10: examples.append({ "name": c.get("name", ""), "type": st, "vendor": vname, }) total = sum(by_type.values()) cookies = by_type.get("cookie", 0) return { "total": total, "by_type": by_type, "real_cookies": cookies, "other_storage": total - cookies, "examples": examples, } def storage_transparency_finding(inv: dict) -> dict | None: """Ein Summen-Befund, wenn Nicht-Cookies als Cookies gelistet sind.""" other = inv.get("other_storage", 0) if other <= 0: return None by = inv.get("by_type", {}) parts = ", ".join( f"{by[k]} {STORAGE_LABELS.get(k, k)}" for k in by if k != "cookie" ) return { "vendor": "—", "cookie": f"{other} Objekte", "type": "storage_transparency", "severity": "MEDIUM", "declared": f"{inv['total']} als Cookies gelistet", "library_purpose": f"vermutlich: {parts}", "remediation": ( f"{other} von {inv['total']} als 'Cookie' gelisteten Objekten sind " f"vermutlich anderer Endgeräte-Speicher ({parts}). § 25 TDDDG ist " f"technologieneutral — Speichertechnologie + -dauer pro Objekt " f"transparent darstellen (echtes Cookie vs. Local Storage / Framework)." ), "control": { "control_id": "DATA-2851-A05", "regulation": "TDDDG", "article": "§ 25 Abs. 1", }, }