feat(cookie+routing): Storage-Typ-Filter + legal_notice capture-only

#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>
This commit is contained in:
Benjamin Admin
2026-06-11 20:45:18 +02:00
parent 0f6cdc93fd
commit 97e39579d5
8 changed files with 137 additions and 11 deletions
@@ -31,10 +31,18 @@ _compliance_check_jobs: dict[str, dict] = {}
# a separate page. We check 'DSB benannt' as a sub-check of the DSE.
_ALL_DOC_TYPES = [
"dse", "impressum", "social_media", "cookie",
"agb", "nutzungsbedingungen", "widerruf",
"agb", "nutzungsbedingungen", "widerruf", "legal_notice",
]
# Capture-only doc types: erfasst + als doc_entry persistiert (für die
# Cross-Doc-Reconciliation B), aber NICHT einzeln gescort. Sie haben keine
# eigene Checkliste/MCs → _check_single würde nur eine irreführende 0%-Zeile
# erzeugen. 'legal_notice' (Footer-„Rechtlicher Hinweis"/Disclaimer) trägt oft
# VSBG/ODR-Aussagen, die Impressum-Pflichten erfüllen → wertvoll für B.
_CAPTURE_ONLY = {"legal_notice"}
# Human-readable labels per doc_type. Used in the report + emails.
_DOC_TYPE_LABELS = {
"dse": "Datenschutzerklaerung",
@@ -77,8 +85,14 @@ _DISCOVERY_RULES: list[tuple[str, tuple[str, ...]]] = [
"allgemeine-nutzungsbedingungen")),
("dsb", ("datenschutzbeauftragt", "data-protection-officer",
"dpo-contact", "/dsb")),
# A: 'legal-disclaimer' (Footer-„Rechtlicher Hinweis") VOR impressum, damit
# die Disclaimer-Seite NICHT mehr als Impressum substituiert wird (war die
# Ursache, dass die Cross-Doc-Reconciliation nie zündete).
("legal_notice", ("legal-disclaimer", "legal-disclaimer-pool",
"rechtlicher-hinweis", "rechtliche-hinweise",
"haftungsausschluss")),
("impressum", ("impressum", "imprint", "legal-notice", "site-notice",
"anbieterkennzeichnung", "legal-disclaimer-pool")),
"anbieterkennzeichnung")),
("dse", ("data-privacy", "datenschutz", "data-protection",
"privacy-policy", "privacy-notice", "dsgvo",
"data_privacy", "datenschutzinformation")),
@@ -17,6 +17,7 @@ from dataclasses import asdict
import httpx
from ._constants import _CAPTURE_ONLY
from ._helpers import (
_apply_profile_filter,
_doc_type_label,
@@ -117,6 +118,16 @@ async def run_phase_b(state: dict) -> None:
))
continue
# A: Capture-only — Text ist via doc_entries schon im Snapshot (für die
# Cross-Doc-Reconciliation B); hier NICHT scoren (keine eigene
# Checkliste → sonst irreführende 0%-Zeile).
if doc_type in _CAPTURE_ONLY:
results.append(DocCheckResult(
label=label, url=url, doc_type=doc_type,
error="Erfasst für Cross-Dokument-Abgleich (nicht einzeln bewertet).",
))
continue
pct = int(40 + (i / n_entries) * 40)
_update(check_id, f"Pruefen {i+1}/{n_entries}: {label}...", pct)
@@ -81,11 +81,15 @@ 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,
@@ -98,6 +102,8 @@ def build_storage_inventory(vendors: list[dict]) -> dict:
"real_cookies": cookies,
"other_storage": total - cookies,
"examples": examples,
# name_lower → Speichertyp (für den Frontend-Filter).
"per_cookie": per_cookie,
}
@@ -50,6 +50,11 @@ def test_inventory_counts_and_transparency_finding():
tf = storage_transparency_finding(inv)
assert tf and tf["type"] == "storage_transparency"
assert "§ 25" in tf["control"]["article"]
# per_cookie-Map (für den Frontend-Storage-Filter): name_lower → Typ.
pc = inv["per_cookie"]
assert pc["componentdefstorage__mutex_x"] == "framework_storage"
assert pc["_ga"] == "cookie"
assert pc["browserid1"] == "cookie"
def test_no_finding_when_all_real_cookies():