97e39579d5
#3 Storage-Filter: cookie-check exponiert per-Cookie-Speichertyp (storage_inventory.per_cookie); CookieResultView bekommt Filter-Chips (Cookie/Local Storage/Framework …) + eine Speicher-Spalte, Anbieter ohne passenden Treffer werden ausgeblendet, KPI zeigt gefilterte Zahl. A-Routing: legal_notice ist jetzt ein kanonischer Doc-Type. Eigene Discovery-Regel (legal-disclaimer/rechtlicher-hinweis) VOR impressum → die Disclaimer-Seite wird nicht mehr als Impressum substituiert (Ursache, dass die Cross-Doc-Reconciliation nie zündete). capture-only: als doc_entry für B persistiert, aber nicht einzeln gescort (keine 0%-Noise, da ohne eigene Checkliste). Im Scan-Form als Option auswählbar. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
137 lines
4.9 KiB
Python
137 lines
4.9 KiB
Python
"""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] = []
|
|
per_cookie: dict[str, str] = {}
|
|
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
|
|
n = (c.get("name") or "").lower()
|
|
if n:
|
|
per_cookie[n] = st
|
|
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,
|
|
# name_lower → Speichertyp (für den Frontend-Filter).
|
|
"per_cookie": per_cookie,
|
|
}
|
|
|
|
|
|
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",
|
|
},
|
|
}
|