fix(banner): Footer-Erreichbarkeit prüfen — kein HIGH/MEDIUM bei nicht-blockierendem Banner

User-Bug (BMW): Banner überlagert, aber Footer-Links (Impressum/DSE) bleiben
klickbar → fehlender In-Banner-Link ist dann nur Best Practice, kein Verstoß.
banner_text_checker misst per Playwright-trial-Klick, ob der Footer-Impressum/
DSE-Link trotz Banner erreichbar ist: erreichbar → LOW/Best-Practice (+ Borlabs-
Consent-Historie-Hinweis), blockiert → HIGH/MEDIUM wie bisher. browser_cross_
finding: redundante (nicht footer-bewusste) "Link im Banner fehlt"-Befunde raus.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-13 15:57:07 +02:00
parent d1ea54b378
commit 8d0da710d5
2 changed files with 61 additions and 32 deletions
@@ -126,24 +126,10 @@ def build_cross_findings(matrix: dict | None) -> list[dict]:
"vom Tracking-Schutz des Browsers.", "vom Tracking-Schutz des Browsers.",
}) })
# ── Oberfläche (Banner-Links) durchgängig fehlend ──────────────────── # Hinweis: Impressum/DSE-Link-im-Banner wird NICHT mehr hier als pauschales
if all(not _s(r).get("surface", {}).get("has_impressum_link") for r in data): # MEDIUM geflaggt. Der per-Engine-Check (banner_text_checker) ist footer-
out.append({ # bewusst: ist der Footer-Link trotz Banner erreichbar → LOW/Best Practice
"title": "Impressum-Link im Banner fehlt (alle Browser)", # statt Verstoss. Doppel-/Falsch-Flag hier vermieden.
"detail": "In keiner Engine ist aus dem Banner ein Impressum "
"erreichbar.",
"severity": "MEDIUM", "affected": _labels(data),
"measure": "Impressum-Link im Banner ergänzen (§ 5 DDG).",
})
if all(not _s(r).get("surface", {}).get("has_dse_link") for r in data):
out.append({
"title": "Datenschutz-Link im Banner fehlt (alle Browser)",
"detail": "In keiner Engine ist aus dem Banner die "
"Datenschutzerklärung erreichbar.",
"severity": "MEDIUM", "affected": _labels(data),
"measure": "Link zur Datenschutzerklärung im Banner ergänzen "
"(Art. 13 DSGVO).",
})
# ── Coverage-Hinweis: nicht getestete Browser ──────────────────────── # ── Coverage-Hinweis: nicht getestete Browser ────────────────────────
if missing: if missing:
+51 -8
View File
@@ -279,6 +279,22 @@ def build_cname_cloaking_finding(found: list) -> Violation:
) )
async def _footer_link_reachable(page, selector: str) -> bool:
"""True, wenn ein passender Link (Impressum/DSE im Footer) trotz offenem
Banner anklickbar ist — das Banner ueberlagert, blockiert die Seite aber
nicht. Playwright-trial-Klick = Aktionsbarkeits-Pruefung OHNE echten Klick;
schlaegt fehl, wenn ein blockierendes Overlay den Link abfaengt."""
try:
loc = page.locator(selector).first
if await loc.count() == 0:
return False
await loc.scroll_into_view_if_needed(timeout=1500)
await loc.click(trial=True, timeout=1500)
return True
except Exception:
return False
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.
@@ -378,11 +394,27 @@ async def check_banner_text(page) -> dict:
pass pass
if not has_impressum: if not has_impressum:
# Entscheidend: blockiert das Banner den Footer-Zugriff wirklich?
# Wenn der Footer-Impressum-Link trotz offenem Banner anklickbar ist
# (Banner ueberlagert, blockiert aber nicht), ist der fehlende
# In-Banner-Link nur Best Practice (LOW), kein Verstoss.
if await _footer_link_reachable(
page, 'a[href*="impressum"], a[href*="imprint"]'):
violations.append(Violation( violations.append(Violation(
service="Cookie-Banner", service="Cookie-Banner", severity="LOW",
severity="HIGH", text="Impressum nicht direkt im Cookie-Banner verlinkt — es ist "
text="Impressum nicht aus dem Cookie-Banner erreichbar. " "jedoch ueber den Footer erreichbar (das Banner blockiert "
"Bei ueberlagerndem Banner muss ein Impressum-Link im Banner vorhanden sein (§5 TMG).", "die Seite nicht). Best Practice: Impressum- und DSE-Link "
"zusaetzlich direkt im Banner anbieten (Borlabs ermoeglicht "
"zudem eine sichtbare Einwilligungs-Historie).",
legal_ref="Best Practice (§5 TMG via Footer erfuellt)",
))
else:
violations.append(Violation(
service="Cookie-Banner", severity="HIGH",
text="Impressum nicht erreichbar: kein Link im Banner UND das "
"ueberlagernde Banner blockiert den Footer-Zugriff. Dann "
"muss ein Impressum-Link im Banner vorhanden sein (§5 TMG).",
legal_ref="§5 TMG, LG Rostock Az. 3 O 22/19", legal_ref="§5 TMG, LG Rostock Az. 3 O 22/19",
)) ))
@@ -394,11 +426,22 @@ async def check_banner_text(page) -> dict:
for l in banner_links for l in banner_links
) )
if not has_dse: if not has_dse:
if await _footer_link_reachable(
page, 'a[href*="datenschutz"], a[href*="privacy"], a[href*="dsgvo"]'):
violations.append(Violation( violations.append(Violation(
service="Cookie-Banner", service="Cookie-Banner", severity="LOW",
severity="MEDIUM", text="Datenschutzerklaerung nicht direkt im Banner verlinkt — sie "
text="Kein Link zur Datenschutzerklaerung im Cookie-Banner. " "ist jedoch ueber den Footer erreichbar (Banner blockiert "
"Nutzer sollten vor der Einwilligung die DSE einsehen koennen.", "nicht). Best Practice: DSE-Link direkt im Banner anbieten, "
"damit Nutzer vor der Einwilligung informiert entscheiden.",
legal_ref="Best Practice (Art. 13 DSGVO via Footer erfuellt)",
))
else:
violations.append(Violation(
service="Cookie-Banner", severity="MEDIUM",
text="Kein Link zur Datenschutzerklaerung im Cookie-Banner und das "
"Banner blockiert den Footer. Nutzer koennen vor der "
"Einwilligung die DSE nicht einsehen.",
legal_ref="Art. 13 DSGVO, ErwGr. 42 DSGVO (informierte Einwilligung)", legal_ref="Art. 13 DSGVO, ErwGr. 42 DSGVO (informierte Einwilligung)",
)) ))