diff --git a/backend-compliance/compliance/services/browser_cross_finding.py b/backend-compliance/compliance/services/browser_cross_finding.py index cde91912..08a11a93 100644 --- a/backend-compliance/compliance/services/browser_cross_finding.py +++ b/backend-compliance/compliance/services/browser_cross_finding.py @@ -126,24 +126,10 @@ def build_cross_findings(matrix: dict | None) -> list[dict]: "vom Tracking-Schutz des Browsers.", }) - # ── Oberfläche (Banner-Links) durchgängig fehlend ──────────────────── - if all(not _s(r).get("surface", {}).get("has_impressum_link") for r in data): - out.append({ - "title": "Impressum-Link im Banner fehlt (alle Browser)", - "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).", - }) + # Hinweis: Impressum/DSE-Link-im-Banner wird NICHT mehr hier als pauschales + # MEDIUM geflaggt. Der per-Engine-Check (banner_text_checker) ist footer- + # bewusst: ist der Footer-Link trotz Banner erreichbar → LOW/Best Practice + # statt Verstoss. Doppel-/Falsch-Flag hier vermieden. # ── Coverage-Hinweis: nicht getestete Browser ──────────────────────── if missing: diff --git a/consent-tester/services/banner_text_checker.py b/consent-tester/services/banner_text_checker.py index 5967392e..7a61c2a1 100644 --- a/consent-tester/services/banner_text_checker.py +++ b/consent-tester/services/banner_text_checker.py @@ -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: """Check cookie banner text for legal issues. @@ -378,13 +394,29 @@ async def check_banner_text(page) -> dict: pass if not has_impressum: - violations.append(Violation( - service="Cookie-Banner", - severity="HIGH", - text="Impressum nicht aus dem Cookie-Banner erreichbar. " - "Bei ueberlagerndem Banner muss ein Impressum-Link im Banner vorhanden sein (§5 TMG).", - legal_ref="§5 TMG, LG Rostock Az. 3 O 22/19", - )) + # 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( + service="Cookie-Banner", severity="LOW", + text="Impressum nicht direkt im Cookie-Banner verlinkt — es ist " + "jedoch ueber den Footer erreichbar (das Banner blockiert " + "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", + )) # Check 2: DSE link in banner has_dse = any( @@ -394,13 +426,24 @@ async def check_banner_text(page) -> dict: for l in banner_links ) if not has_dse: - violations.append(Violation( - service="Cookie-Banner", - severity="MEDIUM", - text="Kein Link zur Datenschutzerklaerung im Cookie-Banner. " - "Nutzer sollten vor der Einwilligung die DSE einsehen koennen.", - legal_ref="Art. 13 DSGVO, ErwGr. 42 DSGVO (informierte Einwilligung)", - )) + if await _footer_link_reachable( + page, 'a[href*="datenschutz"], a[href*="privacy"], a[href*="dsgvo"]'): + violations.append(Violation( + service="Cookie-Banner", severity="LOW", + text="Datenschutzerklaerung nicht direkt im Banner verlinkt — sie " + "ist jedoch ueber den Footer erreichbar (Banner blockiert " + "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)", + )) # Check 3: Wrong wording — "Zustimmung zur Datenschutzerklärung" wrong_dse_consent_patterns = [