Files
breakpilot-compliance/consent-tester/services/scan_matrix_summary.py
T
Benjamin Admin 9587726936 feat(admin): Tab "Browser-Verhalten" — Per-Browser-Matrix + Screenshots (Phase 3)
- BrowserBehaviorView: laedt gespeicherte Matrix (GET), sonst "Browser-Test
  starten" (POST run, Live-Lauf). Per-Browser-Tabelle (Cookies vor Consent /
  nach Ablehnen / Ablehnen respektiert / Oberflaeche / Score), Engine-Detail
  mit Banner-Screenshot + Oberflaechen-Befunden, Mobil-Badge, "nicht
  verfuegbar"-Zeilen fuer fehlende Browser (arm64-Dev).
- Proxys browser-behavior (GET) + browser-behavior/run (POST, langer Timeout).
- page.tsx: Tab "Browser-Verhalten" (sichtbar sobald scanbare URL im Snapshot).
- consent-tester scan_matrix_summary: banner_findings je Engine im summary
  (Text/Severity/Norm) → Oberflaechen-Befunde im Tab.
- tsc strict clean; Vitest BrowserBehaviorView (2).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-12 23:15:06 +02:00

96 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Kompakte Per-Engine-Projektion eines ConsentTestResult für die Browser-Matrix.
Die Matrix braucht NICHT die volle `/scan`-Antwort — nur die Felder, die je
Browser-Zeile angezeigt + persistiert werden: Cookies vor Consent / nach
Ablehnen, ob „Ablehnen" respektiert wurde, Oberflächen-Signale, Screenshot.
Bewusst schlank gehalten, damit der in `banner_result.browser_matrix` (JSONB)
persistierte Block klein bleibt — 6 Engines × voller Cookie-Liste + Screenshot
würde sonst schnell mehrere MB groß (BMW: ~780 Cookies je Phase).
"""
from __future__ import annotations
from typing import Any
# Cookie-Namen je Phase deckeln — die Matrix zeigt Zahlen + Beispiele, nicht
# die volle Liste (die steckt im textbasierten Cookie-Modul).
_NAME_CAP = 40
_TRACK_CAP = 20
def _vdict(v: Any) -> dict:
"""Violation (dataclass/obj/dict) → serialisierbares dict."""
if isinstance(v, dict):
return v
return getattr(v, "__dict__", None) or {"text": str(v)}
def matrix_scan_dict(result: Any) -> dict:
"""`ConsentTestResult` → dict in der Form, die
`multi_browser_scanner._extract_dimensions` liest (phases/banner_checks)
plus ein kompakter `summary`-Block für Frontend + Persistenz.
Defensiv via getattr — funktioniert auch, falls der Scanner mal ein
bereits serialisiertes dict liefert (dann greifen die Defaults)."""
before = list(getattr(result, "before_cookies", []) or [])
after = list(getattr(result, "reject_cookies", []) or [])
before_violations = list(getattr(result, "before_violations", []) or [])
reject_violations = list(getattr(result, "reject_violations", []) or [])
reject_new_tracking = list(getattr(result, "reject_new_tracking", []) or [])
banner_text_violations = list(
getattr(result, "banner_text_violations", []) or [])
provider = getattr(result, "banner_provider", "") or ""
summary = {
"cookies_before_consent": len(before),
"cookies_after_reject": len(after),
"cookies_before_names": before[:_NAME_CAP],
"cookies_after_reject_names": after[:_NAME_CAP],
# „Ablehnen respektiert" = nach dem Klick auf „Ablehnen" keine Verstöße
# UND kein neuer Tracker. Verbleibende essentielle Cookies (z.B. die
# gespeicherte Consent-Entscheidung selbst) sind erlaubt → NICHT über
# die reine Cookie-Zahl bewerten (sonst False Positive).
"reject_respected": (len(reject_violations) == 0
and len(reject_new_tracking) == 0),
"reject_new_tracking": reject_new_tracking[:_TRACK_CAP],
"banner_detected": bool(getattr(result, "banner_detected", False)),
"banner_provider": provider,
"banner_screenshot_b64": getattr(result, "banner_screenshot_b64", "") or "",
"surface": {
"has_impressum_link": bool(
getattr(result, "banner_has_impressum_link", False)),
"has_dse_link": bool(
getattr(result, "banner_has_dse_link", False)),
"banner_text_issues": len(banner_text_violations),
},
# Oberflächen-Befunde je Engine (die 20 Banner-Checks: Button-Prominenz,
# Toggle-Vorauswahl, Einleitungstext/Links …) — Text + Severity +
# Norm-Bezug. Aggregierte Maßnahmen folgen im Cross-Finding.
"banner_findings": [
{"text": d.get("text", ""),
"severity": d.get("severity", "MEDIUM"),
"legal_ref": d.get("legal_ref", ""),
"service": d.get("service", "")}
for d in (_vdict(v) for v in banner_text_violations)
][:20],
"violations": {
"before_consent": len(before_violations),
"after_reject": len(reject_violations),
"banner_text": len(banner_text_violations),
},
}
return {
"banner_detected": bool(getattr(result, "banner_detected", False)),
"banner_provider": provider,
# Minimal-Form für _extract_dimensions (nur cookies-Listen + violations):
"phases": {
"before_consent": {"cookies": before},
"after_reject": {"cookies": after},
},
"banner_checks": {
"violations": [_vdict(v) for v in banner_text_violations],
},
"summary": summary,
}