7938e377b6
CI / branch-name (push) Has been skipped
CI / detect-changes (push) Successful in 11s
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 15s
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) Failing after 48s
CI / iace-gt-coverage (push) Successful in 25s
CI / test-python-backend (push) Successful in 43s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
User-Feedback in einer Session: "Wir erzeugen nur Panik. Egal was da steht,
es dauert Wochen. Wir sind Tool an der Seite von CMO/GF/CIO, nicht Gegner."
Memory: feedback_breakpilot_tonalitaet.md (gilt fuer ALLE Module + Marketing).
P89 Critical-Findings-Block ENTFERNT/UMGEBAUT — keine Panik-Rot-Box mehr.
- Statt "🚨 SOFORTMASSNAHMEN ERFORDERLICH" -> "Zusammenfassung fuer
die Geschaeftsfuehrung", blauer dezenter Block
- Statt "VERSTOSSE" -> "Themen zur Besprechung mit DSB, Marketing
und Entwicklung"
- Statt "Bussgeldrahmen 4% Weltumsatz" als Erstes -> realistische
Einordnung (0,1-1%) in dezenter Schluss-Notiz mit Konfidenz-Hinweis
- "Sofortmassnahme" -> "Empfehlung"
- "Themen 1, 2, 3..." statt "HIGH"-Badges (P87-Vorbereitung)
- Explizite Zeitschaetzung "4-8 Wochen (DSB -> Agentur -> Dev -> Freigabe)"
P76 Mercedes-Sekundaer-Buttons (Datenschutzerklaerung + Impressum klein
unter den 3 Haupt-Buttons) erkennen. Walker scant jetzt label-basiert
ALLE klickbaren Elemente im Shadow-DOM (wb7-link, wb7-link-secondary,
wb7-button-text, span[onclick], small a, [role=button], etc.).
Vermeidet Mercedes-Impressum-False-Positive der Phase 1.
P91 VVT-Tabellen-Renderer in neuer Co-Pilot-Tonalitaet. Statt
"Verstoss-Liste mit Bussgeldpotenzial" -> Wahrscheinlichkeits-Aussage:
"Bei Anbieter-Reduktion + Wechsel zu europaeischen Alternativen ist
Reduktion des Tracking-Footprints + Lizenz-Einsparung wahrscheinlich.
Fundierte Bewertung erfordert DSB-Abstimmung."
BMW-Bug B1-B4 (P90) bewusst nicht in diesem Commit: BMW-Lauf hat ePaaS
4x captured im consent-tester, aber Backend bekommt 0 cmp_payloads.
Wiring-Bug zwischen consent-tester /dsi-discovery und Backend
_fetch_text — eigene Diagnose-Session noetig (siehe Task P90).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
250 lines
11 KiB
Python
250 lines
11 KiB
Python
"""
|
||
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
|
||
|
||
|
||
def _truncate_words(text: str, max_chars: int) -> str:
|
||
"""P65: Truncate at word boundary, never mid-word."""
|
||
if not text or len(text) <= max_chars:
|
||
return text
|
||
cut = text[:max_chars]
|
||
last_space = cut.rfind(" ")
|
||
if last_space > max_chars // 2:
|
||
cut = cut[:last_space]
|
||
return cut.rstrip(",;:.") + "…"
|
||
|
||
|
||
# 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": _truncate_words(v.get("text", ""), 260),
|
||
"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 Audit-Zusammenfassung fuer die Geschaeftsfuehrung.
|
||
|
||
P89: Co-Pilot-Tonalitaet statt Panik-Rot.
|
||
- Sachlich blau statt alarmistisch rot
|
||
- "Themen die besprochen werden sollten" statt "VERSTOESSE"
|
||
- Realistische Zeitschaetzung (4-8 Wochen)
|
||
- Buessgeld-Risiko in separater, dezenter Section ganz unten
|
||
- Konfidenz-Hinweis "False-Positives moeglich"
|
||
"""
|
||
issues = _detect_critical_issues(banner_result, scorecard, results)
|
||
if not issues:
|
||
return ""
|
||
|
||
items = []
|
||
for idx, i in enumerate(issues, 1):
|
||
# P87-Vorbereitung: keine HIGH-Badges mehr — wir nummerieren stattdessen
|
||
items.append(
|
||
f'<div style="margin-bottom:10px;padding:10px 14px;'
|
||
f'background:#fff;border-radius:6px;'
|
||
f'border-left:3px solid #2563eb">'
|
||
f'<div style="font-size:13px;font-weight:600;color:#1e293b;'
|
||
f'margin-bottom:4px">'
|
||
f'<span style="display:inline-block;background:#dbeafe;color:#1e40af;'
|
||
f'padding:1px 8px;border-radius:10px;font-size:10px;'
|
||
f'margin-right:8px;font-weight:600">Thema {idx}</span>'
|
||
f'{i["title"]}</div>'
|
||
f'<div style="font-size:11px;color:#475569;margin-top:6px">'
|
||
f'<strong>Empfehlung:</strong> {i["action"]}</div>'
|
||
f'<div style="font-size:10px;color:#94a3b8;margin-top:4px;'
|
||
f'font-style:italic">Hintergrund: {i.get("source","")}</div>'
|
||
f'</div>'
|
||
)
|
||
|
||
n = len(issues)
|
||
plural = "Themen" if n != 1 else "Thema"
|
||
return (
|
||
'<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;'
|
||
'max-width:700px;margin:0 auto 18px;padding:18px 22px;'
|
||
'background:#f0f9ff;border:1px solid #bfdbfe;border-radius:10px">'
|
||
'<div style="font-size:11px;color:#1e40af;text-transform:uppercase;'
|
||
'letter-spacing:1.2px;margin-bottom:4px;font-weight:600">'
|
||
'Zusammenfassung fuer die Geschaeftsfuehrung</div>'
|
||
f'<h2 style="margin:0 0 8px;font-size:18px;color:#1e293b">'
|
||
f'{n} {plural} zur Besprechung mit DSB, Marketing und Entwicklung</h2>'
|
||
'<p style="margin:0 0 14px;font-size:12px;color:#475569;line-height:1.5">'
|
||
'Wir haben Datenschutzerklaerung, Cookie-Banner, Impressum und '
|
||
'eingebundene Anbieter technisch analysiert. Die folgenden Punkte '
|
||
'sollten in den naechsten Wochen geklaert werden — typische '
|
||
'Umsetzungsdauer 4-8 Wochen (DSB-Review → Marketing-Agentur '
|
||
'→ Entwicklung → Freigabe). Detaillierte technische '
|
||
'Analyse mit weiteren Findings finden Sie unten.</p>'
|
||
+ "".join(items) +
|
||
'<div style="margin-top:14px;padding:10px 12px;background:#f1f5f9;'
|
||
'border-radius:6px;font-size:10px;color:#64748b;line-height:1.5">'
|
||
'<strong style="color:#475569">Hinweis:</strong> Automatisierte '
|
||
'Audits enthalten False-Positives. Wo unsicher, bitte mit DSB pruefen '
|
||
'oder uns Feedback geben — wir lernen daraus. '
|
||
'Rechtliche Risiken (Bussgeld-Rahmen Art. 83 DSGVO bis 4 % des '
|
||
'weltweiten Jahresumsatzes, realistisch 0,1-1 % bei Erstverstoss '
|
||
'nach CNIL/LfDI-Massstab) werden weiter unten pro Finding eingeordnet.'
|
||
'</div>'
|
||
'</div>'
|
||
)
|