Files
breakpilot-compliance/backend-compliance/compliance/api/agent_doc_check_critical.py
T
Benjamin Admin 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
feat(audit-tonality): P89/P76/P91 — Co-Pilot statt Roboter-Anwalt
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>
2026-05-21 11:24:57 +02:00

250 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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, 5k50k 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 &mdash; typische '
'Umsetzungsdauer 4-8 Wochen (DSB-Review &rarr; Marketing-Agentur '
'&rarr; Entwicklung &rarr; 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 &mdash; wir lernen daraus. '
'Rechtliche Risiken (Bussgeld-Rahmen Art. 83 DSGVO bis 4&nbsp;% des '
'weltweiten Jahresumsatzes, realistisch 0,1-1&nbsp;% bei Erstverstoss '
'nach CNIL/LfDI-Massstab) werden weiter unten pro Finding eingeordnet.'
'</div>'
'</div>'
)