feat(consent-tester): cookieless Opt-out erkennen statt False-HIGHs
Cookie-freie Analyse mit reinem Opt-out-Hinweis (z.B. bayshore.ai: "Privacy-friendly, cookie-free analytics are currently enabled ... Disable") ist KEIN Consent-Banner: cookieless = kein Endgeräte-Zugriff → §25 TDDDG verlangt keine Einwilligung → Opt-out statt Opt-in. Die Standard-Opt-in- Checks (granulare Kategorien, Accept/Reject-Balance, Impressum-im-Banner) trafen nicht zu und erzeugten 3 Falsch-HIGHs. is_cookieless_optout() erkennt das Muster (cookieless-Signal + Opt-out-Wort, KEIN Consent-Signal); check_banner_text gibt dann früh EINEN ausführlichen LOW-Erklär-Befund zurück (zählt nicht als HIGH) und setzt die Opt-in-Checks aus. Ausführlich, weil der Fall extrem untypisch ist. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,73 @@ from services.banner_dom_walkers import (
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Cookieless Opt-out-Erkennung (sehr untypischer Sonderfall) ──────────
|
||||||
|
# Manche Sites nutzen cookie-FREIE Analyse mit einem reinen Opt-out-Hinweis
|
||||||
|
# statt eines Consent-Banners (z.B. bayshore.ai: "Privacy-friendly, cookie-free
|
||||||
|
# analytics are currently enabled. You can change your choice at any time."
|
||||||
|
# + ein einziger Button "Disable"). Cookieless = kein Zugriff aufs Endgeraet →
|
||||||
|
# §25 Abs.1 TDDDG/ePrivacy verlangt KEINE Einwilligung → Opt-out (default an,
|
||||||
|
# abschaltbar) ist zulaessig statt Opt-in. Die Standard-Opt-in-Checks (granulare
|
||||||
|
# Kategorien, Accept/Reject-Balance, Impressum-im-Banner, Dark-Pattern-Buttons)
|
||||||
|
# treffen hier NICHT zu → sonst False Positives.
|
||||||
|
_CL_SIGNAL = (
|
||||||
|
"cookie-free", "cookieless", "cookie free", "without cookies", "no cookies",
|
||||||
|
"ohne cookies", "cookielos", "privacy-friendly analytics",
|
||||||
|
"privacy friendly analytics", "cookie-freie analyse", "cookiefreie analyse",
|
||||||
|
"datenschutzfreundliche analyse",
|
||||||
|
)
|
||||||
|
_CL_OPTOUT = (
|
||||||
|
"disable", "deaktivieren", "opt-out", "opt out", "abschalten", "ausschalten",
|
||||||
|
"currently enabled", "widersprechen", "change your choice",
|
||||||
|
)
|
||||||
|
# Wenn ein echtes Consent-Banner-Signal da ist, ist es KEIN reiner Opt-out-Hinweis.
|
||||||
|
_CL_STD_CONSENT = (
|
||||||
|
"alle akzeptieren", "accept all", "alle annehmen", "accept cookies",
|
||||||
|
"cookies akzeptieren", "zustimmen", "einwilligen",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_cookieless_optout(banner_lower: str) -> bool:
|
||||||
|
"""True bei cookie-freier Analyse mit reinem Opt-out-Hinweis (kein
|
||||||
|
Consent-Banner). Pure + testbar. Standard-Opt-in-Checks treffen dann nicht zu."""
|
||||||
|
return (
|
||||||
|
any(s in banner_lower for s in _CL_SIGNAL)
|
||||||
|
and any(o in banner_lower for o in _CL_OPTOUT)
|
||||||
|
and not any(c in banner_lower for c in _CL_STD_CONSENT)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _cookieless_finding(has_dse: bool) -> Violation:
|
||||||
|
"""Vollstaendiger Erklaer-Befund (LOW) fuer den cookieless-Opt-out-Fall —
|
||||||
|
bewusst ausfuehrlich, weil der Fall extrem untypisch ist und ein DSB/Nutzer
|
||||||
|
die Einordnung sonst nicht versteht."""
|
||||||
|
return Violation(
|
||||||
|
service="Cookie-Banner",
|
||||||
|
severity="LOW",
|
||||||
|
text=(
|
||||||
|
"Cookieless Analytics im Opt-out-Modell erkannt — KEIN klassisches "
|
||||||
|
"Consent-Banner. Der Hinweis bezieht sich auf datenschutzfreundliche, "
|
||||||
|
"cookie-freie Analyse: sie arbeitet OHNE Cookies und OHNE persistente "
|
||||||
|
"Kennung (typisch ein taeglich zurueckgesetzter Hash aus IP+Browser), "
|
||||||
|
"speichert keine personenbezogenen Daten und verfolgt nicht ueber "
|
||||||
|
"Sitzungen oder Geraete hinweg. Weil dabei NICHT auf das Endgeraet "
|
||||||
|
"zugegriffen wird, greift die Einwilligungspflicht nach §25 Abs.1 TDDDG "
|
||||||
|
"(ePrivacy) NICHT — deshalb ist Opt-out (standardmaessig aktiv, jederzeit "
|
||||||
|
"ueber 'Disable' abschaltbar) zulaessig statt Opt-in. Die ueblichen "
|
||||||
|
"Opt-in-Pruefungen (granulare Kategorien, Accept/Reject-Gleichwertigkeit, "
|
||||||
|
"Impressum-Link im Banner, Dark-Pattern-Buttons) sind hier NICHT anwendbar "
|
||||||
|
"und werden bewusst ausgesetzt, um Falschbefunde zu vermeiden. Bewertung: "
|
||||||
|
"wahrscheinlich konform und sogar datenschutzfreundlicher als ein Standard-"
|
||||||
|
"Cookie-Banner. Zu pruefen bleibt nur: funktioniert der Opt-out wirklich "
|
||||||
|
"(Analyse stoppt nach 'Disable') und ist eine Datenschutzerklaerung verlinkt"
|
||||||
|
+ ("." if has_dse else " — ein DSE-Link fehlt im Hinweis.")
|
||||||
|
),
|
||||||
|
legal_ref="§25 Abs. 1 TDDDG (cookieless ohne Endgeraete-Zugriff = keine "
|
||||||
|
"Einwilligungspflicht), ErwGr. 30 DSGVO, EDPB Guidelines 05/2020 "
|
||||||
|
"(Anwendungsbereich der Einwilligung)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def check_banner_text(page) -> dict:
|
async def check_banner_text(page) -> dict:
|
||||||
"""Check cookie banner text for legal issues.
|
"""Check cookie banner text for legal issues.
|
||||||
|
|
||||||
@@ -86,6 +153,19 @@ async def check_banner_text(page) -> dict:
|
|||||||
|
|
||||||
banner_lower = banner_text.lower()
|
banner_lower = banner_text.lower()
|
||||||
|
|
||||||
|
# Sonderfall cookieless Opt-out (sehr untypisch): erkennen, vollstaendig
|
||||||
|
# erklaeren und die Opt-in-Checks aussetzen — statt Falsch-HIGHs zu erzeugen.
|
||||||
|
if is_cookieless_optout(banner_lower):
|
||||||
|
has_dse = any(
|
||||||
|
"datenschutz" in l["href"] or "datenschutz" in l["text"]
|
||||||
|
or "privacy" in l["href"] or "privacy" in l["text"]
|
||||||
|
or "dsgvo" in l["href"]
|
||||||
|
for l in banner_links
|
||||||
|
)
|
||||||
|
return {"violations": [_cookieless_finding(has_dse)],
|
||||||
|
"has_impressum": False, "has_dse": has_dse,
|
||||||
|
"cookieless_optout": True}
|
||||||
|
|
||||||
# Check 1: Impressum link in or accessible through banner
|
# Check 1: Impressum link in or accessible through banner
|
||||||
has_impressum = any(
|
has_impressum = any(
|
||||||
"impressum" in l["href"] or "impressum" in l["text"] or
|
"impressum" in l["href"] or "impressum" in l["text"] or
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""Cookieless Opt-out-Erkennung im banner_text_checker.
|
||||||
|
|
||||||
|
Sehr untypischer Sonderfall: cookie-FREIE Analyse mit reinem Opt-out-Hinweis
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_bayshore_cookieless_optout_detected():
|
||||||
|
# Realer bayshore.ai-Bannertext (Opt-out fuer cookie-freie Analyse).
|
||||||
|
bay = ("privacy-friendly, cookie-free analytics are currently enabled. "
|
||||||
|
"you can change your choice at any time. disable")
|
||||||
|
assert is_cookieless_optout(bay) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_standard_consent_banner_not_cookieless():
|
||||||
|
assert not is_cookieless_optout(
|
||||||
|
"wir nutzen cookies. alle akzeptieren ablehnen einstellungen")
|
||||||
|
|
||||||
|
|
||||||
|
def test_cookiefree_but_with_accept_is_not_optout():
|
||||||
|
# 'cookie-free' genannt, aber echtes Consent ('accept all') → kein reiner Opt-out.
|
||||||
|
assert not is_cookieless_optout("cookie-free analytics. accept all disable")
|
||||||
|
|
||||||
|
|
||||||
|
def test_signal_without_optout_word_is_not_detected():
|
||||||
|
# cookie-free, aber kein Opt-out-Mechanismus im Text.
|
||||||
|
assert not is_cookieless_optout("cookie-free analytics enabled")
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty():
|
||||||
|
assert not is_cookieless_optout("")
|
||||||
Reference in New Issue
Block a user