feat(consent+report): P56-P67 Mercedes-Audit-Cycle (Anti-Audit, Phase G Vendors, Cookie-Behavior-Validator + 5 Mail-Polish-Items) [migration-approved]
CI / detect-changes (push) Successful in 11s
CI / branch-name (push) Has been skipped
CI / nodejs-build (push) Successful in 2m19s
CI / test-go (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 16s
CI / loc-budget (push) Failing after 15s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 37s

P56  Anti-Auditing-Detection als constructive Compliance-Finding (Audit-API-
     Empfehlung statt Anklage, weil Mercedes berechtigt Bots blockiert)
P57  Phase G vendor_details Union mit cmp_vendors -> 42 Anbieter sichtbar
P58  Anti-Audit-Detection robuster (Script-Domain-Check + Settings-spezifisch)
P59  Cookie-Behavior-Validator (4 Layer, 3-Tier-Severity: MEDIUM=Kategorie-
     Mismatch / HIGH=Zweck-Mismatch / CRITICAL=beide=Vorsatz-Indiz)
     + Open Cookie Database (CC0) als Library-Seed (2264 Cookies)
P59b Cookie-Behavior in Banner-Check verdrahtet + Mail-Block (BUGFIX:
     SessionLocal selbst oeffnen, db war im Background-Task nicht im Scope)

Mail-Polish nach Mercedes-Review:
P63  Banner-Footer-Links auch im wb7-link/role=link erkennen (Shadow-DOM-
     Walker label-based statt nur <a href>)
P64  Re-Access-Severity: MEDIUM statt HIGH, wenn Footer "Einstellungen" oder
     Mercedes-typisch existiert; OEM-Footer-Detection (wb7-footer)
P65  Text-Truncation: Word-Boundary statt Zeichen-Cut (kein "einfa"-Bruch
     mehr in Sofortmassnahmen)
P66  GF-Aktionen: Service-Zweck vs Cookie-Zweck explizit erklaert
     (haeufige Verwechslung Marketing/GF: "Akamai-Beschreibung" != Cookie-
     Zweck pro DSK-OH 2024)
P67  Stirring-Finding mit "Verlust-Framing"-Erklaerung + Alt-vs-Neutral-
     Beispiel, statt nur EDPB-Fachbegriff

Compliance-Advisor FAQ (admin agent-core/soul):
  + CNIL/EDPB Top-Bussgelder (Google 100M, Meta 60M, Amazon 35M)
  + Deutsche Praezedenz (LG Muenchen Google Fonts, EuGH Planet49, BGH I ZR 7/16)
  + 4 Risiko-Pfade (Bussgeld/Abmahnung/Sammelklage/NOYB) + Berechnungs-Methodik

Document-Generator Templates: AGB-DE (142), Impressum (140), Widerrufs-
formular-Anlage (143), DSR-Process-Dedup (139), Cookie-Library (144).

Architektur: doc_action_mappings.py + banner_dom_walkers.py +
cookie_behavior_validator.py + vendor_detail_extractor.py rausgezogen,
um die 500-LOC-Caps in agent_doc_check_report.py und
banner_text_checker.py einzuhalten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-21 06:28:25 +02:00
parent badb356740
commit 57c0f940a2
38 changed files with 3656 additions and 116 deletions
+119 -32
View File
@@ -19,6 +19,10 @@ import logging
from services.script_analyzer import Violation
from services.banner_advanced_checks import run_advanced_checks
from services.banner_dom_walkers import (
SHADOW_BANNER_WALKER_JS,
FOOTER_LABELS_WALKER_JS,
)
logger = logging.getLogger(__name__)
@@ -62,6 +66,21 @@ async def check_banner_text(page) -> dict:
except Exception:
continue
# P28a + P63: Shadow-DOM Web Component CMPs (Mercedes cmm-cookie-banner,
# BMW cookie-consent-banner). Walker pierces shadow tree + extracts
# label-based legal links (wb7-link/button/role=link). See
# banner_dom_walkers.SHADOW_BANNER_WALKER_JS.
if not banner_text or not banner_links:
try:
shadow_data = await page.evaluate(SHADOW_BANNER_WALKER_JS)
if shadow_data and isinstance(shadow_data, dict):
if shadow_data.get("text"):
banner_text = (banner_text + " " + shadow_data["text"]).strip()
if shadow_data.get("links"):
banner_links.extend(shadow_data["links"])
except Exception:
pass
if not banner_text:
return {"violations": violations, "has_impressum": False, "has_dse": False}
@@ -134,17 +153,38 @@ async def check_banner_text(page) -> dict:
))
break
# Check 4: Reject button visible (no hidden reject)
reject_texts = ["ablehnen", "reject", "nur notwendige", "alle ablehnen", "decline"]
has_visible_reject = any(t in banner_lower for t in reject_texts)
if not has_visible_reject:
# P28b Check 4: Reject mechanism present + explicit-labeled?
# HIGH = no reject mechanism at all
# MEDIUM = reject available but not labeled "Ablehnen"/"Reject"
# (e.g. only "Nur technisch Notwendige" — semantically
# a reject but EDPB 5/2020 + DSK-OH 2024 prefer explicit
# labeling so users recognize it as the reject option)
explicit_reject_texts = ["ablehnen", "reject", "alle ablehnen",
"decline", "alles ablehnen"]
implicit_reject_texts = ["nur notwendige", "nur technisch", "nur essenzielle",
"nur essentielle", "notwendige akzeptieren",
"essential only", "only necessary",
"nur erforderliche"]
has_explicit_reject = any(t in banner_lower for t in explicit_reject_texts)
has_implicit_reject = any(t in banner_lower for t in implicit_reject_texts)
if not has_explicit_reject and not has_implicit_reject:
violations.append(Violation(
service="Cookie-Banner",
severity="HIGH",
text="Kein sichtbarer 'Ablehnen'-Button im Banner erkannt. "
text="Kein 'Ablehnen'-Mechanismus im Banner erkannt. "
"Die Ablehnung muss ebenso einfach sein wie die Zustimmung.",
legal_ref="§25 Abs. 1 TDDDG, EDPB Guidelines 05/2020 (Consent)",
))
elif not has_explicit_reject and has_implicit_reject:
violations.append(Violation(
service="Cookie-Banner",
severity="MEDIUM",
text="Reject-Moeglichkeit vorhanden ('Nur technisch Notwendige' o.ae.), "
"aber nicht als 'Ablehnen' beschriftet. Nutzer erkennen 'Ablehnen' "
"schneller als sprachlich umschriebene Varianten. "
"Empfehlung: zusaetzlich 'Ablehnen' als Button-Label.",
legal_ref="EDPB 5/2020 (Consent) + DSK-OH 2024 (Telemedien)",
))
# Check 5: Pre-ticked checkboxes (EuGH Planet49)
try:
@@ -210,7 +250,8 @@ async def check_banner_text(page) -> dict:
accept_btn = None
reject_btn = None
accept_kw = ["akzeptieren", "accept", "zustimmen", "agree", "einverstanden", "ok"]
reject_kw = ["ablehnen", "reject", "notwendige", "decline", "nein"]
reject_kw = ["ablehnen", "reject", "notwendige", "decline", "nein",
"technisch", "essenzielle", "essential", "erforderliche"]
for btn in button_info:
text_lower = btn["text"].lower()
@@ -245,44 +286,90 @@ async def check_banner_text(page) -> dict:
# Check 7: Cookie Wall — does rejecting block the site?
# (This is checked in Phase B — if after reject the page is not navigable)
# Check 8: Re-access to settings (Art. 7(3) — revocation as easy as consent)
# P29 Check 8: Re-access to cookie settings (Art. 7(3) DSGVO).
# Three quality tiers:
# OK = persistent floating cookie icon OR explicit-labeled
# footer link ("Cookie-Einstellungen", "Cookie-Richtlinie",
# "Cookies verwalten", etc.)
# MEDIUM = re-access only via ambiguous label (e.g. "Einstellungen"
# alone — could mean theme/language) OR only via
# cookies.html doc link (not a settings dialog)
# HIGH = no re-access mechanism found at all
try:
settings_accessible = False
settings_selectors = [
'[class*="cookie-settings"]', '[class*="privacy-settings"]',
'a[href*="cookie"]', 'a[href*="datenschutz-einstellungen"]',
'[class*="consent-settings"]', '#ot-sdk-btn',
'.cky-btn-revisit', '#CybotCookiebotDialogBodyButtonDetails',
'[data-testid="uc-footer-link"]',
has_floating_icon = False
floating_selectors = [
".cky-btn-revisit", "#ot-sdk-btn", "#ot-sdk-btn-floating",
"[class*='ot-floating']", "[class*='cookie-floating']",
"[id*='cookiebot-renew']", "[class*='cmp-floating']",
"[id*='cmplz-cookiebanner-status']", ".uc-cookie-settings-trigger",
"[class*='consent-floating']", "[data-testid*='cookie-revisit']",
]
for sel in settings_selectors:
for sel in floating_selectors:
try:
if await page.locator(sel).count() > 0:
settings_accessible = True
has_floating_icon = True
break
except Exception:
continue
# Also check footer for cookie settings link
if not settings_accessible:
footer_text = ""
try:
footer = page.locator("footer").first
if await footer.count() > 0:
footer_text = (await footer.text_content() or "").lower()
except Exception:
pass
if any(kw in footer_text for kw in ["cookie-einstellungen", "cookie settings",
"datenschutz-einstellungen", "privacy settings"]):
settings_accessible = True
# Footer label inspection — distinguish explicit vs ambiguous
# P64: OEM design-systems (Mercedes wb7-footer, BMW b-footer) don't
# use <footer>. Scan via evaluate() with multiple candidate roots
# including page-bottom region as last-ditch fallback.
footer_labels: list[str] = []
try:
footer_labels = await page.evaluate(FOOTER_LABELS_WALKER_JS) or []
except Exception:
pass
if not settings_accessible:
# Explicit, unambiguous cookie/consent labels
explicit_patterns = [
"cookie-einstellungen", "cookie einstellungen",
"cookie-richtlinie", "cookie richtlinie",
"cookie-praeferenzen", "cookie preferences",
"cookies verwalten", "manage cookies",
"datenschutz-einstellungen", "privacy preferences",
"datenschutzeinstellungen", "datenschutz einstellungen",
"cookie consent", "consent settings",
"cookie-banner", "cookies anpassen",
# P64: OEM-typical labels
"tracking-einstellungen", "tracking einstellungen",
"cookie-zustimmung", "consent verwalten",
]
has_explicit_footer = any(
any(p in lbl for p in explicit_patterns)
for lbl in footer_labels
)
# Ambiguous labels — "Einstellungen" alone, generic "Cookies"
ambiguous_patterns = ["einstellungen", "settings", "cookies"]
has_ambiguous_footer = (not has_explicit_footer) and any(
lbl.strip() in ambiguous_patterns
or any(lbl.strip() == p for p in ambiguous_patterns)
for lbl in footer_labels
)
if has_floating_icon or has_explicit_footer:
pass # OK — no violation
elif has_ambiguous_footer:
violations.append(Violation(
service="Cookie-Banner",
severity="MEDIUM",
text="Kein erneuter Zugang zu Cookie-Einstellungen gefunden. "
"Der Widerruf der Einwilligung muss ebenso einfach sein wie "
"die Erteilung (Art. 7 Abs. 3 DSGVO).",
text="Re-Zugang zu Cookie-Einstellungen nur ueber mehrdeutiges "
"Footer-Label (z.B. 'Einstellungen' oder 'Cookies'). "
"Empfehlung: persistenten Cookie-Icon-Button (Floating) "
"oder explizites Footer-Label 'Cookie-Einstellungen', "
"'Cookie-Richtlinie' o.ae. damit Nutzer den Widerruf "
"ohne Suchen finden.",
legal_ref="Art. 7 Abs. 3 DSGVO + EDPB 5/2020 (informierte, "
"leicht widerrufbare Einwilligung)",
))
else:
violations.append(Violation(
service="Cookie-Banner",
severity="HIGH",
text="Kein erneuter Zugang zu Cookie-Einstellungen gefunden "
"(weder Floating-Icon noch Footer-Link). Widerruf muss "
"ebenso einfach sein wie Erteilung.",
legal_ref="Art. 7 Abs. 3 DSGVO (Widerruf so einfach wie Einwilligung)",
))
except Exception: