fix(audit): VW-404-Recovery + P52 LLM-Merge + P51 Banner-UX-Checks
CI / detect-changes (push) Successful in 10s
CI / branch-name (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 14s
CI / loc-budget (push) Failing after 17s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 42s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped

VW-404-Fix: submitted_types zaehlt jetzt nur Doc-Types mit >= 200 Zeichen
echtem Text. Eine eingegebene URL die 404/Mini-Text liefert (VW cookie-
richtlinie.html) wird als 'missing' behandelt, sodass Auto-Discovery
alternative URLs auf der Homepage probiert. In-place-Update statt
Duplicate-Entry, rejected_url wird fuer Audit-Transparenz aufgehoben.

P52 LLM-Cascade Merge: vendor_llm_extractor laeuft jetzt bei < 5 Vendors
(nicht nur bei 0), und die Ergebnisse werden MIT existing cmp_vendors
gemerged statt zu ueberschreiben. VW-typische Setups (Generic CMP +
0 cmp_payloads) bekommen damit den Text-basierten Vendor-Layer dazu.

P51 — banner_consistency_checks erweitert:
* check_banner_copyability: scannt banner_html nach user-select:none /
  oncopy=return false / onselectstart. MEDIUM Finding wenn Banner-Text
  nicht kopierbar (Art. 7 (2) DSGVO).
* check_consent_history: prueft auf 'Meine Einwilligungen' / Consent-
  Historie / Datenschutz-Cockpit. MEDIUM wenn keine sichtbare Historie
  (Art. 7 (3) — Widerruf muss so einfach wie Erteilung sein).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-21 17:27:55 +02:00
parent 309c10c203
commit 6dc427a754
2 changed files with 157 additions and 18 deletions
@@ -303,6 +303,87 @@ def check_banner_vs_cmp_partner_count(
}
def check_banner_copyability(banner_result: dict) -> dict | None:
"""P51a — Banner-Text muss kopierbar sein. CSS user-select:none oder
-webkit-user-select:none verhindert das (Article 7(2) DSGVO — verstaendlich
und in einer Form, die spaetere Pruefung ermoeglicht).
"""
if not isinstance(banner_result, dict):
return None
phases = banner_result.get("phases") or {}
initial = phases.get("initial") or phases.get("before_accept") or {}
html = (initial.get("banner_html") or "")[:50000].lower()
if not html:
return None
blocked_signals = [
"user-select:none", "user-select: none",
"-webkit-user-select:none", "-webkit-user-select: none",
"-moz-user-select:none", "pointer-events:none",
"oncopy=\"return false", "onselectstart=\"return false",
]
hits = [s for s in blocked_signals if s in html]
if not hits:
return None
return {
"severity": "MEDIUM",
"code": "banner_not_copyable",
"label": "Banner-Text laesst sich nicht kopieren "
"(user-select:none / oncopy disabled)",
"detail": (
f'Im Banner-HTML gefunden: {", ".join(hits[:3])}. Der Nutzer '
"kann den Banner-Text nicht in eine Mail / Doku einfuegen, was "
"die spaetere Pruefung erschwert. Empfehlung: das CSS entfernen "
"oder explizit auf 'auto' setzen."
),
"legal_basis": "Art. 7 (1)+(2) DSGVO + EDPB 5/2020 — Einwilligungen "
"muessen in verstaendlicher und zugaenglicher Form "
"erteilt werden; eine spaetere Pruefung darf nicht "
"technisch erschwert werden.",
}
def check_consent_history(banner_result: dict) -> dict | None:
"""P51b — Es muss eine Moeglichkeit geben, die eigene Einwilligungs-
Historie einzusehen (Art. 7 (3) — Widerruf muss so einfach wie die
Erteilung sein; das setzt voraus dass man WEISS was man einwilligt hat).
"""
if not isinstance(banner_result, dict):
return None
phases = banner_result.get("phases") or {}
blob_parts: list[str] = []
for ph in phases.values():
if isinstance(ph, dict):
blob_parts.append((ph.get("banner_text") or "")[:5000])
blob_parts.append((ph.get("banner_html") or "")[:20000])
blob = " ".join(blob_parts).lower()
if not blob:
return None
history_signals = [
"meine einwilligung", "consent-historie", "consent history",
"einwilligungshistorie", "einwilligungs-historie",
"ihre einwilligungen", "datenschutz-cockpit",
"privacy dashboard", "einwilligungs-protokoll",
"consent record", "consent log",
]
if any(s in blob for s in history_signals):
return None
return {
"severity": "MEDIUM",
"code": "consent_history_missing",
"label": "Keine sichtbare Consent-Historie / 'Meine Einwilligungen'-Ansicht",
"detail": (
"Im Banner und in den verlinkten Footer-Bereichen ist keine "
"Moeglichkeit erkennbar, die eigene Einwilligungs-Historie "
"einzusehen oder zu exportieren. Empfehlung: einen "
"'Meine Einwilligungen'-Bereich verlinken (Borlabs / Cookiebot / "
"Usercentrics bieten dafuer fertige Komponenten)."
),
"legal_basis": "Art. 7 (3) DSGVO + EDPB 5/2020 — der Widerruf muss "
"ebenso einfach sein wie die Erteilung, was eine "
"Sichtbarmachung der eigenen Einwilligungen voraussetzt.",
}
def run_all(banner_result: dict, cookie_doc_text: str | None = None,
cmp_vendors: list | None = None,
doc_texts: dict[str, str] | None = None) -> list[dict]:
@@ -331,6 +412,18 @@ def run_all(banner_result: dict, cookie_doc_text: str | None = None,
findings.append(f4)
except Exception as e:
logger.warning("P33 three_source_vendor failed: %s", e)
try:
f5 = check_banner_copyability(banner_result)
if f5:
findings.append(f5)
except Exception as e:
logger.warning("P51a copyability failed: %s", e)
try:
f6 = check_consent_history(banner_result)
if f6:
findings.append(f6)
except Exception as e:
logger.warning("P51b consent_history failed: %s", e)
return findings