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
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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user