"""Cross-Browser-Befunde — deterministische Sicht über die Browser-Matrix. KEINE zweite Engine, kein LLM: rein eine Auswertung der bereits vom consent-tester gemessenen Per-Engine-Ergebnisse (siehe [[feedback_agents_delegate_not_reimplement]]). Findet Inkonsistenzen ZWISCHEN den Browsern und ordnet sie ein — der zentrale Mehrwert gegenüber einem Einzel-Browser-Scan: * Cookies vor Consent / „Ablehnen“ nicht universell respektiert? * Browser-Tracking-Schutz (Safari/ITP, Brave/Shields, Firefox/ETP) maskiert Verstöße clientseitig → ein „sauberes" Ergebnis in einer strengen Engine ist KEIN Compliance-Beleg; maßgeblich sind die nachgiebigen Engines (Chrome/Edge/Chromium), in denen tatsächlich gesetzt wird. """ from __future__ import annotations from typing import Any # Welcher Browser-Schutzmechanismus greift je Profil — für die Einordnung, # warum sich Engines unterscheiden (nicht zum Bewerten der Compliance). _PROTECTION = [ ("brave", "Brave-Shields"), ("webkit", "Safari/ITP"), ("iphone", "Safari/ITP"), ("safari", "Safari/ITP"), ("firefox", "Firefox/ETP"), ("gecko", "Firefox/ETP"), ] def _protection_label(row: dict) -> str: key = f"{row.get('profile_id', '')} {row.get('engine', '')}".lower() for needle, label in _PROTECTION: if needle in key: return label return "" # blink/chrome/edge = nachgiebig (kein client-seitiger Schutz) def _labels(rows: list[dict]) -> list[str]: return [r.get("label") or r.get("profile_id") or "?" for r in rows] def build_cross_findings(matrix: dict | None) -> list[dict]: """Liefert eine priorisierte Liste von Cross-Browser-Befunden. Befund-Form: {title, detail, severity, affected: [labels], measure}. Nur Engines MIT Messdaten werden verglichen (Fehler-/„nicht verfügbar"- Zeilen fließen nur als Coverage-Hinweis ein).""" rows_all = (matrix or {}).get("browser_matrix") or [] data = [r for r in rows_all if r.get("summary")] missing = [r for r in rows_all if not r.get("summary")] out: list[dict] = [] if not data: return out def _s(r: dict) -> dict: return r.get("summary") or {} def _pre_track(r: dict) -> int: # Nicht-essentielles TRACKING vor Consent (§ 25 Abs. 1 TDDDG) — das # rechtlich relevante Signal. NICHT die Cookie-Rohzahl: die enthaelt # technisch notwendige Cookies inkl. des Consent-Cookies selbst (das # speichern MUSS, dass abgelehnt wurde) → § 25 Abs. 2, einwilligungsfrei. return (_s(r).get("violations") or {}).get("before_consent") or 0 track_yes = [r for r in data if _pre_track(r) > 0] track_no = [r for r in data if _pre_track(r) == 0] rej_bad = [r for r in data if _s(r).get("reject_respected") is False] rej_ok = [r for r in data if _s(r).get("reject_respected") is True] # ── Tracking VOR der Einwilligung (nicht: jede Cookie-Rohzahl) ──────── _ESS = (" Technisch notwendige Cookies inkl. des Consent-Cookies sind " "ausgenommen (§ 25 Abs. 2 TDDDG).") if track_yes and not track_no: out.append({ "title": "Tracking vor der Einwilligung — in allen Browsern", "detail": "In jeder getesteten Engine feuern vor einer aktiven " "Einwilligung nicht-essentielle Tracker." + _ESS, "severity": "HIGH", "affected": _labels(track_yes), "measure": "Tracking-/Marketing-Skripte erst nach aktiver " "Einwilligung laden (§ 25 Abs. 1 TDDDG).", }) elif track_yes and track_no: masked = sorted({_protection_label(r) for r in track_no if _protection_label(r)}) hint = (f" Die unauffälligen Engines ({', '.join(_labels(track_no))}) " f"unterdrücken die Tracker vermutlich clientseitig " f"({', '.join(masked)}) — das ist KEIN Compliance-Beleg." if masked else "") out.append({ "title": "Tracking vor Einwilligung — nur in manchen Browsern", "detail": f"Nicht-essentielle Tracker vor Consent in " f"{', '.join(_labels(track_yes))}, nicht in " f"{', '.join(_labels(track_no))}.{hint}", "severity": "HIGH", "affected": _labels(track_yes), "measure": "Server-/skriptseitig auf Consent gaten statt auf den " "Tracking-Schutz einzelner Browser zu vertrauen.", }) # ── „Ablehnen“ respektiert? ────────────────────────────────────────── if rej_bad and not rej_ok: out.append({ "title": "„Ablehnen“ wird in keinem Browser respektiert", "detail": "Nach Klick auf „Ablehnen“ verbleiben in jeder Engine " "Verstöße oder neue Tracker.", "severity": "HIGH", "affected": _labels(rej_bad), "measure": "Reject-Handler muss alle nicht-essentiellen Skripte/" "Cookies tatsächlich stoppen (Art. 7 Abs. 3 DSGVO).", }) elif rej_bad and rej_ok: masked = sorted({_protection_label(r) for r in rej_ok if _protection_label(r)}) hint = (f" Dass {', '.join(_labels(rej_ok))} sauber wirken, liegt " f"vermutlich am Browser-Schutz ({', '.join(masked)}), nicht am " f"Banner." if masked else "") out.append({ "title": "„Ablehnen“ wird nicht in allen Browsern respektiert", "detail": f"Verstoß nach Ablehnen in {', '.join(_labels(rej_bad))}; " f"sauber in {', '.join(_labels(rej_ok))}.{hint}", "severity": "HIGH", "affected": _labels(rej_bad), "measure": "Reject-Handler universell wirksam machen — unabhängig " "vom Tracking-Schutz des Browsers.", }) # ── Oberfläche (Banner-Links) durchgängig fehlend ──────────────────── if all(not _s(r).get("surface", {}).get("has_impressum_link") for r in data): out.append({ "title": "Impressum-Link im Banner fehlt (alle Browser)", "detail": "In keiner Engine ist aus dem Banner ein Impressum " "erreichbar.", "severity": "MEDIUM", "affected": _labels(data), "measure": "Impressum-Link im Banner ergänzen (§ 5 DDG).", }) if all(not _s(r).get("surface", {}).get("has_dse_link") for r in data): out.append({ "title": "Datenschutz-Link im Banner fehlt (alle Browser)", "detail": "In keiner Engine ist aus dem Banner die " "Datenschutzerklärung erreichbar.", "severity": "MEDIUM", "affected": _labels(data), "measure": "Link zur Datenschutzerklärung im Banner ergänzen " "(Art. 13 DSGVO).", }) # ── Coverage-Hinweis: nicht getestete Browser ──────────────────────── if missing: out.append({ "title": "Nicht alle Browser getestet", "detail": f"{len(missing)} Browser nicht verfügbar " f"({', '.join(_labels(missing))}). Echtes Brave/Chrome/" f"Edge laufen nur auf dem amd64-Server, nicht in der " f"arm64-Dev-Umgebung.", "severity": "LOW", "affected": _labels(missing), "measure": "Für vollständige Abdeckung auf dem Produktiv-Server " "(amd64) testen.", }) return out