""" P18 — Critical-Findings-Block fuer die Executive-Summary. Analysiert die echten Daten (banner_checks, phases, scorecard, results) und rendert einen ROTEN Sofortmassnahmen-Block GANZ OBEN in der Email — mit Quellenangaben (DSK, EDPB, EuGH, Behoerden-Buessgeld-Faelle) und konkreten Sofortmassnahmen. Regel: Block wird nur gerendert wenn echte kritische Verstoesse vorliegen. Bei sauberen Sites bleibt er weg. """ from __future__ import annotations # Bekannte Buessgeld-Praezedenzfaelle als Quellen-Hint _BUSSGELD_REFS = { "no_provider_per_category": "CNIL France 2023 — TikTok 5 Mio EUR (fehlende Vendor-Transparenz)", "dse_unvollstaendig": "BayLDA 2024 — diverse Mittelstand-Faelle, 5k–50k EUR", "cookie_doc_missing": "LfDI BW 2023 — fehlende Cookie-Erklaerung, 30k EUR", "dark_pattern_reject": "EDPB Guidelines 3/2022 + DSK 2024 — Bussgeldrahmen Art. 83 DSGVO", "schrems_ii": "EuGH C-311/18 (Schrems II) — Bussgeldrahmen bis 4% Konzern-Umsatz", "impressum_im_banner": "LG Rostock 3 O 22/19 — Impressum-Pflicht ueberlagernder Banner", } def _detect_critical_issues( banner_result: dict | None, scorecard: dict | None, results: list, ) -> list[dict]: """Erkenne kritische Verstoesse aus den vorliegenden Daten.""" issues: list[dict] = [] br = banner_result or {} sc = scorecard or {} # 1) Banner-Violations (HIGH/CRITICAL) aus consent-tester for v in (br.get("banner_checks") or {}).get("violations", []): sev = (v.get("severity") or "").upper() if sev in ("CRITICAL", "HIGH"): issues.append({ "key": "banner_violation", "title": v.get("text", "")[:120], "severity": sev, "action": _action_for_banner_violation(v), "source": v.get("legal_ref", ""), "bussgeld": _BUSSGELD_REFS.get("impressum_im_banner") if "impressum" in (v.get("text") or "").lower() else _BUSSGELD_REFS.get("dark_pattern_reject"), }) # 2) Category-Tests: Banner zeigt keine Provider-Details pro Kategorie. # Bevorzugt das echte Signal aus dem Click-Through-Test (P19): # provider_details_visible. Fallback: leere tracking_services. cat_tests = br.get("category_tests") or [] cats_without_details = [ c for c in cat_tests if c.get("category") != "necessary" and (c.get("provider_details_visible") is False or (c.get("provider_details_visible") is None and not c.get("tracking_services"))) ] if cats_without_details and len(cat_tests) >= 2: cats = ", ".join(c.get("category_label", c.get("category", "?")) for c in cats_without_details) issues.append({ "key": "no_provider_per_category", "title": f"Cookie-Banner: Kategorien ({cats}) zeigen keine " f"Provider-/Cookie-Details", "severity": "HIGH", "action": ("Pro Banner-Kategorie eine Liste der eingebundenen " "Anbieter + Cookie-Details (Name, Zweck, Speicherdauer, " "Drittlandtransfer) sichtbar machen — am besten als " "ausklappbares Detail-Panel. Sonst ist die " "Einwilligung nicht 'informiert' nach Art. 7 DSGVO " "und gilt als unwirksam."), "source": "Art. 7 Abs. 1 DSGVO, EDPB Guidelines 2/2023, DSK 2024", "bussgeld": _BUSSGELD_REFS["no_provider_per_category"], }) # 3) DSGVO/TDDDG-Score < 30%: DSE rechtswidrig pct = int((sc.get("totals") or {}).get("pct", 100)) if pct and pct < 30: issues.append({ "key": "dse_unvollstaendig", "title": f"Datenschutzerklaerung erfuellt nur {pct}% der Pflichten", "severity": "HIGH", "action": ("Vollstaendig nach Art. 13 DSGVO ueberarbeiten: " "Verantwortlicher, Zwecke, Rechtsgrundlage, " "Speicherdauer, Drittland-Transfers, alle Betroffenen-" "rechte, konkrete Aufsichtsbehoerde."), "source": "Art. 13 DSGVO + Art. 14 (alternativ), DSK-OH Telemedien 2024", "bussgeld": _BUSSGELD_REFS["dse_unvollstaendig"], }) # 4) Cookie-Richtlinie fehlt komplett (nicht erreichbar) cookie_missing = any( (r.doc_type == "cookie" if hasattr(r, "doc_type") else r.get("doc_type") == "cookie") and ((r.error if hasattr(r, "error") else r.get("error", "")) or "") .startswith("Auf der Website nicht gefunden") for r in (results or []) ) cookie_deduped = any( (r.doc_type == "cookie" if hasattr(r, "doc_type") else r.get("doc_type") == "cookie") and "Nicht separat vorhanden" in ((r.error if hasattr(r, "error") else r.get("error", "")) or "") for r in (results or []) ) if cookie_missing or cookie_deduped: issues.append({ "key": "cookie_doc_missing", "title": ("Keine eigenstaendige Cookie-Richtlinie" if cookie_deduped else "Cookie-Richtlinie nicht auffindbar"), "severity": "HIGH", "action": ("Separate Cookie-Richtlinie-Seite erstellen mit " "tabellarischer Auflistung aller Cookies (Name, " "Anbieter, Zweck, Speicherdauer, Drittlandtransfer). " "Direkt aus dem Banner verlinken."), "source": "Art. 13 DSGVO, §25 TDDDG, DSK-OH Telemedien 2024", "bussgeld": _BUSSGELD_REFS["cookie_doc_missing"], }) # 5) Schrems-II-Risiko: Google/Meta/Microsoft im Banner, aber keine SCC/DPF # Detection: pre-/post-consent-cookies in den phases enthalten US-Tracker phases = br.get("phases") or {} has_us_tracker = False for ph in phases.values(): if not isinstance(ph, dict): continue for t in (ph.get("tracking_services") or []): if isinstance(t, dict): name = (t.get("name", "") or "").lower() else: name = str(t).lower() if any(w in name for w in ("google", "meta", "facebook", "microsoft", "linkedin", "tiktok")): has_us_tracker = True break if has_us_tracker: issues.append({ "key": "schrems_ii", "title": "US-Tracker geladen — Schrems-II-Risiko", "severity": "HIGH", "action": ("Pro Drittland-Anbieter dokumentieren: SCC (Art. 46 " "DSGVO) ODER DPF-Zertifizierung pruefen + in der " "Datenschutzerklaerung explizit benennen."), "source": "Art. 44 ff. DSGVO, EuGH C-311/18 (Schrems II)", "bussgeld": _BUSSGELD_REFS["schrems_ii"], }) return issues def _action_for_banner_violation(v: dict) -> str: text = (v.get("text") or "").lower() if "impressum" in text: return ("Impressum-Link direkt im Banner ergaenzen — bei " "ueberlagerndem Banner Pflicht nach §5 TMG.") if "ablehnen" in text or "dark pattern" in text: return ("'Ablehnen'-Button visuell gleichwertig zu 'Akzeptieren' " "gestalten (gleiche Groesse, Farbe, Position).") if "widerruf" in text or "cookie-einstellungen" in text: return ("Floating-Icon oder Footer-Link 'Cookie-Einstellungen' " "permanent einblenden — Widerruf so einfach wie Erteilung.") return ("Banner-Verstoss beheben gemaess der genannten Rechtsgrundlage.") def build_critical_findings_html( banner_result: dict | None, scorecard: dict | None, results: list, ) -> str: """Render der Critical-Findings-Box. Leerer String wenn keine Issues.""" issues = _detect_critical_issues(banner_result, scorecard, results) if not issues: return "" items = [] for i in issues: items.append( f'
' f'
' f'{i["severity"]}{i["title"]}
' f'
' f'Sofortmassnahme: {i["action"]}
' f'
Rechtsgrundlage: {i.get("source","")}' + (f' · Praezedenz: {i["bussgeld"]}' if i.get("bussgeld") else "") + f'
' ) n = len(issues) return ( '
' '
' '🚨 Sofortmassnahmen erforderlich
' f'

' f'{n} kritische Compliance-Risiken mit Bussgeldpotenzial

' '

' 'Die folgenden Verstoesse sind durch Tool-Analyse belegt und ' 'erfordern Sofortmassnahmen. Bussgeldrahmen nach Art. 83 DSGVO: ' 'bis 4% des weltweiten Jahresumsatzes.

' + "".join(items) + '
' )