+ „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.
+
+
{selRow && (
diff --git a/backend-compliance/compliance/services/browser_cross_finding.py b/backend-compliance/compliance/services/browser_cross_finding.py
index b2418d95..cde91912 100644
--- a/backend-compliance/compliance/services/browser_cross_finding.py
+++ b/backend-compliance/compliance/services/browser_cross_finding.py
@@ -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.",
})
diff --git a/backend-compliance/tests/test_browser_cross_finding.py b/backend-compliance/tests/test_browser_cross_finding.py
index f160e83c..76331897 100644
--- a/backend-compliance/tests/test_browser_cross_finding.py
+++ b/backend-compliance/tests/test_browser_cross_finding.py
@@ -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"]]
diff --git a/consent-tester/services/consent_scanner.py b/consent-tester/services/consent_scanner.py
index d5af8b11..aec869bd 100644
--- a/consent-tester/services/consent_scanner.py
+++ b/consent-tester/services/consent_scanner.py
@@ -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)
diff --git a/consent-tester/services/multi_browser_scanner.py b/consent-tester/services/multi_browser_scanner.py
index 88fe0b3f..6d3c3bcd 100644
--- a/consent-tester/services/multi_browser_scanner.py
+++ b/consent-tester/services/multi_browser_scanner.py
@@ -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),