From c2422138e660dcccc135bba091f7d08d5e83135b Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Fri, 12 Jun 2026 19:49:55 +0200 Subject: [PATCH] =?UTF-8?q?feat(consent-tester):=203=20Edge-Cases=20?= =?UTF-8?q?=E2=80=94=20kein-Banner-konform,=20Geo-Caveat,=20Non-Cookie-Tra?= =?UTF-8?q?cking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1/#2: Wenn KEIN Banner erkannt UND kein Tracking vor Consent (statische Seite oder nur technisch notwendige Cookies, §25 Abs.2 TDDDG) → affirmativer LOW-Befund "konform, kein Banner nötig" statt stillem "Banner fehlt". Inkl. Geo-Caveat (Scan außerhalb EU sieht geo-getargetete Banner evtl. nicht). #3: detect_non_cookie_tracking erkennt Pixel/Fingerprinting per Domain-Signatur (Meta, TikTok, LinkedIn, Pinterest, Clarity, FingerprintJS, Hotjar, Reddit, Snapchat) → MEDIUM-Befund "§25/Art.5(3) gilt auch ohne Cookies". '0 Cookies' ≠ 'kein einwilligungspflichtiges Tracking'. Verdrahtet in consent_scanner vor dem Return. Tests + py_compile grün. Co-Authored-By: Claude Opus 4.7 --- .../services/banner_text_checker.py | 70 +++++++++++++++++++ consent-tester/services/consent_scanner.py | 22 ++++++ .../tests/test_cookieless_optout.py | 38 +++++++++- 3 files changed, 129 insertions(+), 1 deletion(-) diff --git a/consent-tester/services/banner_text_checker.py b/consent-tester/services/banner_text_checker.py index 41a13098..1a75f167 100644 --- a/consent-tester/services/banner_text_checker.py +++ b/consent-tester/services/banner_text_checker.py @@ -94,6 +94,76 @@ def _cookieless_finding(has_dse: bool) -> Violation: ) +# ── #1/#2: Kein Banner erkannt → affirmativ einordnen (nicht still) ───── +def build_no_banner_finding(has_dse: bool) -> Violation: + """Wenn KEIN Banner erkannt wurde UND vor Consent kein einwilligungs- + pflichtiges Tracking lief: kein Banner noetig (statische Seite / nur + technisch notwendige Cookies, §25 Abs.2 TDDDG) → konform. Inkl. Geo-Caveat. + Nur aufrufen, wenn before_violations UND before_tracking leer sind.""" + return Violation( + service="Cookie-Banner", + severity="LOW", + text=( + "Kein Consent-Banner erkannt — und es ist auch keiner erforderlich: " + "Vor einer Einwilligung wurde kein einwilligungspflichtiges Tracking " + "geladen (keine Tracking-Skripte/-Pixel). Typisch fuer eine statische " + "Seite oder eine Seite mit ausschliesslich technisch notwendigen Cookies " + "(Session, CSRF, Login, Warenkorb, Sprache) — die nach §25 Abs.2 TDDDG / " + "Art.5(3) ePrivacy KEINE Einwilligung brauchen. Bewertung: konform. Es " + "bleibt nur die Informationspflicht — eine Datenschutzerklaerung muss " + "erreichbar sein" + + ("." if has_dse else " (DSE-Link wurde hier nicht gefunden — bitte pruefen).") + + " METHODEN-HINWEIS: Falls die Seite Geo-Targeting nutzt, sieht ein Scan " + "ausserhalb der EU evtl. keinen Banner — den Scanner-Standort (EU-IP) bei " + "diesem Befund mitdenken." + ), + legal_ref="§25 Abs. 2 TDDDG (Ausnahme technisch notwendig), Art. 5(3) ePrivacy, " + "EDPB Guidelines 2/2023", + ) + + +# ── #3: Non-Cookie-Tracking (Pixel/Fingerprinting) — §25 gilt auch ohne Cookies ── +# Domain-Signaturen, die auf Script-URLs zuverlaessig matchen. +_NON_COOKIE_TRACKERS = { + "Meta-Pixel (Facebook)": ("connect.facebook.net", "facebook.com/tr"), + "TikTok-Pixel": ("analytics.tiktok.com",), + "LinkedIn Insight Tag": ("snap.licdn.com",), + "Pinterest-Tag": ("s.pinimg.com/ct",), + "Microsoft Clarity": ("clarity.ms",), + "Fingerprinting (FingerprintJS)": ("fingerprintjs", "fpjs.io", "fpcdn.io"), + "Hotjar": ("static.hotjar.com",), + "Reddit-Pixel": ("redditstatic.com/ads",), + "Snapchat-Pixel": ("sc-static.net", "tr.snapchat.com"), +} + + +def detect_non_cookie_tracking(scripts: list) -> list: + """Erkennt cookieloses/script-basiertes Tracking (Pixel/Fingerprinting) in + den geladenen Skript-URLs. Pure + testbar. §25 Abs.1 TDDDG/Art.5(3) ist + technologieneutral → gilt auch ohne Cookies.""" + blob = " ".join(s.lower() for s in (scripts or []) if s) + return [name for name, sigs in _NON_COOKIE_TRACKERS.items() + if any(sig in blob for sig in sigs)] + + +def build_non_cookie_tracking_finding(detected: list) -> Violation: + return Violation( + service="Cookie-Banner", + severity="MEDIUM", + text=( + "Non-Cookie-Tracking erkannt: " + ", ".join(detected) + ". " + "§25 Abs.1 TDDDG / Art.5(3) ePrivacy ist technologieneutral — die " + "Einwilligungspflicht gilt AUCH ohne Cookies (Pixel, Web-Beacons, " + "Fingerprinting). Ein reiner Cookie-Check uebersieht das: '0 Cookies' " + "heisst NICHT 'kein einwilligungspflichtiges Tracking'. Diese Techniken " + "vor jeder Einwilligung pruefen. (Hinweis: Google Consent Mode v2 ist " + "KEINE gueltige Einwilligung, sondern sendet nur modellierte Signale.)" + ), + legal_ref="§25 Abs.1 TDDDG, Art.5(3) ePrivacy, EDPB Guidelines 2/2023 " + "(Pixel/Fingerprinting/URL-Tracking einwilligungspflichtig)", + ) + + async def check_banner_text(page) -> dict: """Check cookie banner text for legal issues. diff --git a/consent-tester/services/consent_scanner.py b/consent-tester/services/consent_scanner.py index 9a4596f2..56eb59d7 100644 --- a/consent-tester/services/consent_scanner.py +++ b/consent-tester/services/consent_scanner.py @@ -540,6 +540,28 @@ async def run_consent_test( result.banner_provider, len(result.before_violations), len(result.reject_violations), len(result.category_tests), len(result.cmp_payloads), ) + + # Edge-Cases: kein Banner affirmativ einordnen (#1/#2) + Non-Cookie-Tracking (#3). + try: + from services.banner_text_checker import ( + build_no_banner_finding, detect_non_cookie_tracking, + build_non_cookie_tracking_finding, + ) + # #1/#2: KEIN Banner + KEIN Tracking vor Consent → konform (statisch / + # nur technisch notwendig), nicht still "Banner fehlt". Inkl. Geo-Caveat. + if (not result.banner_detected and not result.before_violations + and not result.before_tracking): + result.banner_text_violations.append( + build_no_banner_finding(result.banner_has_dse_link)) + # #3: Pixel/Fingerprinting (cookieloses Tracking) → §25 gilt auch ohne Cookies. + _nct = detect_non_cookie_tracking( + (result.before_scripts or []) + (result.accept_scripts or [])) + if _nct: + result.banner_text_violations.append( + build_non_cookie_tracking_finding(_nct)) + except Exception as e: + logger.warning("Edge-case findings skipped: %s", e) + return result diff --git a/consent-tester/tests/test_cookieless_optout.py b/consent-tester/tests/test_cookieless_optout.py index 5ff0aceb..6fd9c4bd 100644 --- a/consent-tester/tests/test_cookieless_optout.py +++ b/consent-tester/tests/test_cookieless_optout.py @@ -5,7 +5,11 @@ statt Consent-Banner (z.B. bayshore.ai). Standard-Opt-in-Checks duerfen dann NICHT feuern (sonst False Positives). """ -from services.banner_text_checker import is_cookieless_optout +from services.banner_text_checker import ( + is_cookieless_optout, + detect_non_cookie_tracking, + build_no_banner_finding, +) def test_bayshore_cookieless_optout_detected(): @@ -32,3 +36,35 @@ def test_signal_without_optout_word_is_not_detected(): def test_empty(): assert not is_cookieless_optout("") + + +# ── #3: Non-Cookie-Tracking-Erkennung ────────────────────────────────── +def test_detect_meta_pixel(): + assert detect_non_cookie_tracking( + ["https://connect.facebook.net/en_US/fbevents.js"]) == ["Meta-Pixel (Facebook)"] + + +def test_detect_clarity_and_fingerprint(): + found = detect_non_cookie_tracking([ + "https://www.clarity.ms/tag/abc", "https://cdn.fpjs.io/v3/x.js"]) + assert "Microsoft Clarity" in found + assert "Fingerprinting (FingerprintJS)" in found + + +def test_detect_none_on_plain_scripts(): + assert detect_non_cookie_tracking( + ["https://example.com/app.js", "/static/main.css"]) == [] + assert detect_non_cookie_tracking([]) == [] + + +# ── #1/#2: Kein-Banner-affirmativ-Befund ─────────────────────────────── +def test_no_banner_finding_is_low_and_compliant(): + v = build_no_banner_finding(has_dse=True) + assert v.severity == "LOW" + assert "konform" in v.text.lower() + assert "geo-targeting" in v.text.lower() # Geo-Caveat enthalten + + +def test_no_banner_finding_flags_missing_dse(): + v = build_no_banner_finding(has_dse=False) + assert "dse" in v.text.lower() or "datenschutzerkl" in v.text.lower()