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>
This commit is contained in:
@@ -13,10 +13,12 @@ import React, { useEffect, useState } from 'react'
|
||||
|
||||
type Finding = { text: string; severity: string; legal_ref?: string; service?: string }
|
||||
type Surface = { has_impressum_link?: boolean; has_dse_link?: boolean; banner_text_issues?: number }
|
||||
type Violations = { before_consent?: number; after_reject?: number; banner_text?: number }
|
||||
type Summary = {
|
||||
cookies_before_consent?: number; cookies_after_reject?: number
|
||||
reject_respected?: boolean; banner_detected?: boolean; banner_provider?: string
|
||||
banner_screenshot_b64?: string; surface?: Surface; banner_findings?: Finding[]
|
||||
violations?: Violations
|
||||
}
|
||||
type Row = {
|
||||
profile_id: string; label: string; engine?: string; is_mobile?: boolean
|
||||
@@ -164,6 +166,7 @@ export function BrowserBehaviorView({ snapshotId }: { snapshotId: string }) {
|
||||
const s = r.summary
|
||||
const before = s?.cookies_before_consent ?? null
|
||||
const after = s?.cookies_after_reject ?? null
|
||||
const trackBefore = s?.violations?.before_consent ?? 0
|
||||
const sld = r.profile_id === sel
|
||||
return (
|
||||
<tr key={r.profile_id} onClick={() => setSel(r.profile_id)}
|
||||
@@ -178,8 +181,11 @@ export function BrowserBehaviorView({ snapshotId }: { snapshotId: string }) {
|
||||
</td>
|
||||
) : (
|
||||
<>
|
||||
<td className={`px-3 py-2 text-center ${(before ?? 0) > 0 ? 'text-red-700 font-semibold' : 'text-green-700'}`}>{before}</td>
|
||||
<td className={`px-3 py-2 text-center ${(after ?? 0) > 0 ? 'text-amber-700' : 'text-green-700'}`}>{after}</td>
|
||||
<td className={`px-3 py-2 text-center ${trackBefore > 0 ? 'text-red-700 font-semibold' : 'text-gray-500'}`}
|
||||
title={trackBefore > 0 ? `${trackBefore} davon Tracking (Verstoß)` : 'kein Tracking vor Consent'}>
|
||||
{before}{trackBefore > 0 ? ` · ${trackBefore}⚠` : ''}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center text-gray-500">{after}</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
{s.reject_respected ? <span className="text-green-700">✓</span> : <span className="text-red-700 font-semibold">✗</span>}
|
||||
</td>
|
||||
@@ -200,6 +206,13 @@ export function BrowserBehaviorView({ snapshotId }: { snapshotId: string }) {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-400">
|
||||
„Cookies vor Consent" ist die Rohzahl — technisch notwendige Cookies
|
||||
(inkl. des Consent-Cookies, das die Ablehnung speichert) sind nach
|
||||
§ 25 Abs. 2 TDDDG erlaubt. Rot/⚠ markiert nur den einwilligungspflichtigen
|
||||
Tracking-Anteil. Das Verdikt zu „Ablehnen" trägt die Spalte rechts.
|
||||
</p>
|
||||
|
||||
{selRow && (
|
||||
<div className="border border-gray-200 rounded-xl p-4 space-y-3">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
|
||||
@@ -58,34 +58,44 @@ def build_cross_findings(matrix: dict | None) -> list[dict]:
|
||||
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]
|
||||
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]
|
||||
|
||||
# ── Cookies vor der Einwilligung ─────────────────────────────────────
|
||||
if pre_yes and not pre_no:
|
||||
# ── 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": "Cookies vor der Einwilligung — in allen Browsern",
|
||||
"detail": "In jeder getesteten Engine werden vor einer aktiven "
|
||||
"Einwilligung Cookies gesetzt.",
|
||||
"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(pre_yes),
|
||||
"measure": "Tracking-/Marketing-Cookies erst nach aktiver "
|
||||
"Einwilligung setzen (§ 25 Abs. 1 TDDDG).",
|
||||
"affected": _labels(track_yes),
|
||||
"measure": "Tracking-/Marketing-Skripte erst nach aktiver "
|
||||
"Einwilligung laden (§ 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 "
|
||||
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": "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}",
|
||||
"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(pre_yes),
|
||||
"affected": _labels(track_yes),
|
||||
"measure": "Server-/skriptseitig auf Consent gaten statt auf den "
|
||||
"Tracking-Schutz einzelner Browser zu vertrauen.",
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ from compliance.services.browser_cross_finding import build_cross_findings
|
||||
|
||||
|
||||
def _row(pid, label, engine, *, before=0, reject_ok=True,
|
||||
impressum=True, dse=True, with_summary=True):
|
||||
impressum=True, dse=True, with_summary=True, track_before=0):
|
||||
if not with_summary:
|
||||
return {"profile_id": pid, "label": label, "engine": engine,
|
||||
"summary": None, "error": "launch failed"}
|
||||
@@ -19,6 +19,9 @@ def _row(pid, label, engine, *, before=0, reject_ok=True,
|
||||
"cookies_after_reject": 0 if reject_ok else 2,
|
||||
"reject_respected": reject_ok,
|
||||
"surface": {"has_impressum_link": impressum, "has_dse_link": dse},
|
||||
# before_consent = nicht-essentielles Tracking vor Consent (das
|
||||
# rechtlich relevante Signal, NICHT die Cookie-Rohzahl `before`).
|
||||
"violations": {"before_consent": track_before},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -32,22 +35,34 @@ def test_empty_matrix():
|
||||
assert build_cross_findings({"browser_matrix": []}) == []
|
||||
|
||||
|
||||
def test_pre_consent_in_all_engines_high():
|
||||
def test_tracking_before_consent_in_all_engines_high():
|
||||
# Nicht-essentielles Tracking vor Consent (track_before>0) in allen Engines.
|
||||
m = {"browser_matrix": [
|
||||
_row("chromium-headed-de", "Chromium", "blink", before=5),
|
||||
_row("firefox-headed-de", "Firefox", "gecko", before=3),
|
||||
_row("chromium-headed-de", "Chromium", "blink", before=5, track_before=2),
|
||||
_row("firefox-headed-de", "Firefox", "gecko", before=3, track_before=1),
|
||||
]}
|
||||
f = build_cross_findings(m)
|
||||
hit = [x for x in f if "in allen Browsern" in x["title"]]
|
||||
hit = [x for x in f if "Tracking vor der Einwilligung — in allen" in x["title"]]
|
||||
assert hit and hit[0]["severity"] == "HIGH"
|
||||
assert "TDDDG" in hit[0]["measure"]
|
||||
assert "§ 25 Abs. 2" in hit[0]["detail"] # essentielle Cookies ausgenommen
|
||||
|
||||
|
||||
def test_pre_consent_inconsistent_flags_browser_protection():
|
||||
# Chrome (nachgiebig) setzt vor Consent, Safari/WebKit (ITP) nicht.
|
||||
def test_cookies_before_consent_without_tracking_is_no_finding():
|
||||
# KERN-REGRESSION (User-Frage): Cookies vor Consent vorhanden, aber KEIN
|
||||
# Tracking (z.B. nur das Consent-Cookie selbst) → KEIN Verstoß-Befund.
|
||||
m = {"browser_matrix": [
|
||||
_row("chrome-channel-desktop-de", "Chrome", "blink", before=4),
|
||||
_row("webkit-headed-de", "Safari/WebKit", "webkit", before=0),
|
||||
_row("chromium-headed-de", "Chromium", "blink", before=5, track_before=0),
|
||||
_row("firefox-headed-de", "Firefox", "gecko", before=4, track_before=0),
|
||||
]}
|
||||
f = build_cross_findings(m)
|
||||
assert [x for x in f if "Tracking vor" in x["title"]] == []
|
||||
|
||||
|
||||
def test_tracking_before_consent_inconsistent_flags_browser_protection():
|
||||
# Chrome (nachgiebig) lässt Tracker vor Consent, Safari/WebKit (ITP) nicht.
|
||||
m = {"browser_matrix": [
|
||||
_row("chrome-channel-desktop-de", "Chrome", "blink", before=4, track_before=3),
|
||||
_row("webkit-headed-de", "Safari/WebKit", "webkit", before=0, track_before=0),
|
||||
]}
|
||||
f = build_cross_findings(m)
|
||||
hit = [x for x in f if "nur in manchen" in x["title"]]
|
||||
|
||||
@@ -344,6 +344,17 @@ async def run_consent_test(
|
||||
|
||||
await ctx_b.close()
|
||||
|
||||
# Matrix-Modus (browser_profile gesetzt): die Per-Engine-Summary
|
||||
# braucht nur Phase A+B (Cookies/Tracking vor Consent + nach
|
||||
# Ablehnen, Banner-Text-Checks + Screenshot — alle schon erfasst).
|
||||
# Die teuren Phasen C (Accept) / D-F (Kategorien) / G (Vendor-Detail)
|
||||
# ueberspringen → ein Multi-Engine-Scan bleibt im HTTP-Zeitbudget
|
||||
# (sonst 504). Browser sofort schliessen (Semaphore-Speicher).
|
||||
if browser_profile:
|
||||
_apply_edge_case_findings(result, url)
|
||||
await browser.close()
|
||||
return result
|
||||
|
||||
# ── Phase C: After accepting ─────────────────────────
|
||||
logger.info("Phase C: Accept consent (%s)", banner.provider)
|
||||
ctx_c = await browser.new_context(**_ctx_base)
|
||||
|
||||
@@ -55,10 +55,26 @@ def _extract_dimensions(banner_result: dict) -> dict[str, float]:
|
||||
before = phases.get("before_consent") or phases.get("before") or {}
|
||||
after_reject = phases.get("after_reject") or {}
|
||||
bv = (banner_result.get("banner_checks") or {}).get("violations") or []
|
||||
pre_cookies = len(before.get("cookies") or [])
|
||||
rej_cookies = len(after_reject.get("cookies") or [])
|
||||
pre_consent = max(0.0, 1.0 - min(1.0, pre_cookies / 10.0))
|
||||
reject_respect = max(0.0, 1.0 - min(1.0, rej_cookies / 5.0))
|
||||
summary = banner_result.get("summary") or {}
|
||||
viol = summary.get("violations") or {}
|
||||
# Pre-Consent: das rechtliche Signal ist nicht-essentielles TRACKING vor
|
||||
# Consent, NICHT die Cookie-Rohzahl (essentielle inkl. Consent-Cookie sind
|
||||
# nach § 25 Abs. 2 erlaubt). Fallback auf Rohzahl nur ohne Summary.
|
||||
if "before_consent" in viol:
|
||||
pre_track = viol.get("before_consent") or 0
|
||||
pre_consent = max(0.0, 1.0 - min(1.0, pre_track / 3.0))
|
||||
else:
|
||||
pre_cookies = len(before.get("cookies") or [])
|
||||
pre_consent = max(0.0, 1.0 - min(1.0, pre_cookies / 10.0))
|
||||
# Reject: bevorzugt das reject_respected-Verdikt (kein Verstoß UND kein
|
||||
# neuer Tracker), sonst after_reject-Tracking, sonst Cookie-Rohzahl.
|
||||
if summary.get("reject_respected") is not None:
|
||||
reject_respect = 1.0 if summary.get("reject_respected") else 0.2
|
||||
elif "after_reject" in viol:
|
||||
reject_respect = max(0.0, 1.0 - min(1.0, (viol.get("after_reject") or 0) / 2.0))
|
||||
else:
|
||||
rej_cookies = len(after_reject.get("cookies") or [])
|
||||
reject_respect = max(0.0, 1.0 - min(1.0, rej_cookies / 5.0))
|
||||
banner_design = max(0.0, 1.0 - min(1.0, len(bv) / 5.0))
|
||||
return {
|
||||
"pre_consent": round(pre_consent, 3),
|
||||
|
||||
Reference in New Issue
Block a user