Files
breakpilot-compliance/backend-compliance/compliance/services/browser_cross_finding.py
T
Benjamin Admin 3f90e40807 fix(browser-matrix): Tracking-Signal statt Cookie-Rohzahl + Matrix-Schnellpfad
Korrektheit (§ 25 TDDDG): "Cookies vor Consent" ist KEIN Verstoss per se —
technisch notwendige Cookies inkl. des Consent-Cookies (speichert die
Ablehnung) sind nach Abs. 2 erlaubt. Verstoss ist nur nicht-essentielles
TRACKING vor Consent.
- browser_cross_finding: Befund haengt jetzt an violations.before_consent
  (Tracking), nicht an der Cookie-Rohzahl; § 25 Abs. 2-Hinweis im Detail.
  Regressionstest: Cookies-ohne-Tracking → KEIN Befund.
- multi_browser_scanner._extract_dimensions: Score nutzt Tracking-Violations
  + reject_respected-Verdikt statt Rohzahl (Fallback erhalten).
- BrowserBehaviorView: "Cookies vor Consent" nur rot/⚠ bei Tracking,
  "nach Ablehnen" neutral (Verdikt = reject-Spalte); erklaerende Zeile.

Speed: run_consent_test ueberspringt im Matrix-Modus (browser_profile gesetzt)
die teuren Phasen C/D-F/G — nur A+B noetig. Verhindert das 504 beim
Multi-Engine-Scan (BMW 4 Engines lief sonst in den 338s-Gateway-Timeout).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-13 00:10:41 +02:00

162 lines
7.6 KiB
Python

"""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