85a8a1d545
- browser_cross_finding: deterministische Sicht ueber die Matrix (keine 2. Engine, kein LLM). Findet Inkonsistenzen ZWISCHEN Browsern (Cookies vor Consent / Ablehnen nicht universell respektiert / Banner-Links fehlend) und ordnet ein: Safari-ITP / Brave-Shields / Firefox-ETP maskieren Verstoesse clientseitig → strenge Engine "sauber" ist KEIN Compliance-Beleg, massgeblich sind die nachgiebigen (Chrome/Edge). Coverage-Hinweis fuer nicht verfuegbare Browser. Je Befund Titel/Detail/Severity/affected/Massnahme. - snapshot_check_routes: cross_findings frisch in run + GET (nicht persistiert). - BrowserBehaviorView: "Cross-Browser-Befunde"-Block ueber der Tabelle. - Tests: test_browser_cross_finding (6). Offen (Folge-Task): Borlabs-Consent-Historie-Live-Erkennung (braucht consent-tester-Storage-Scan). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
152 lines
7.1 KiB
Python
152 lines
7.1 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 {}
|
|
|
|
pre_yes = [r for r in data if (_s(r).get("cookies_before_consent") or 0) > 0]
|
|
pre_no = [r for r in data if (_s(r).get("cookies_before_consent") or 0) == 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]
|
|
|
|
# ── Cookies vor der Einwilligung ─────────────────────────────────────
|
|
if pre_yes and not pre_no:
|
|
out.append({
|
|
"title": "Cookies vor der Einwilligung — in allen Browsern",
|
|
"detail": "In jeder getesteten Engine werden vor einer aktiven "
|
|
"Einwilligung Cookies gesetzt.",
|
|
"severity": "HIGH",
|
|
"affected": _labels(pre_yes),
|
|
"measure": "Tracking-/Marketing-Cookies erst nach aktiver "
|
|
"Einwilligung setzen (§ 25 Abs. 1 TDDDG).",
|
|
})
|
|
elif pre_yes and pre_no:
|
|
masked = sorted({_protection_label(r) for r in pre_no if _protection_label(r)})
|
|
hint = (f" Die unauffälligen Engines ({', '.join(_labels(pre_no))}) "
|
|
f"unterdrücken die Cookies vermutlich clientseitig "
|
|
f"({', '.join(masked)}) — das ist KEIN Compliance-Beleg."
|
|
if masked else "")
|
|
out.append({
|
|
"title": "Cookies vor Einwilligung — nur in manchen Browsern",
|
|
"detail": f"Cookies vor Consent in {', '.join(_labels(pre_yes))}, "
|
|
f"nicht in {', '.join(_labels(pre_no))}.{hint}",
|
|
"severity": "HIGH",
|
|
"affected": _labels(pre_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
|