Files
breakpilot-compliance/consent-tester/services/scan_matrix_summary.py
T
Benjamin Admin d92dd3b5fc feat(banner): Consent-Historie/Widerruf live erkennen (Borlabs-Stil, #62)
consent_history.detect_consent_history: erkennt CMP-Anbieter (Borlabs/
Usercentrics/OneTrust/Cookiebot/…) aus Storage+Cookies, versionierten Consent
(historie-fähig) + dauerhaftes Widerruf-/Einstellungs-Widget. consent_scanner
ruft es in Phase A; scan_matrix_summary surft summary.consent_history;
browser_cross_finding: positiver Befund wenn vorhanden, sonst Best-Practice-LOW
(„Nutzer sehen, wann sie welcher Version zugestimmt haben"); BrowserBehaviorView
zeigt es im Engine-Detail. Tests: 7 (classify/versioned) + 2 Cross-Finding.

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

98 lines
4.3 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),
},
# #62: Consent-Historie/Widerruf (Borlabs-Stil).
"consent_history": getattr(result, "consent_history", {}) or {},
# 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,
}