d0e3621192
Mail Render V2 (compliance/services/mail_render_v2/) — 11-Modul-Subpackage
das einen einheitlichen Audit-Mail-Output erzeugt mit:
- Header + KPI-Kacheln (Score / Findings / Docs / Vendors)
- TOC + Sprung-Links
- 3-Bucket-Trennung: Kritische Befunde / Manuelle Prüfung / Interne Reminder
- Cookie-Inventar (Name·Vendor·Kategorie·Speicherdauer·Löschfrist·Sitzland·Quelle·Status)
- Sofortmaßnahmen-Aggregator ("Sitzland ergänzen für 11 Cookies")
- 24 Legacy-Wrappers — alle alten build_*_html in V2-Sections
- Scope-Filter: FIN/GOV/MED/INS/EDU/LEG aus Berichten wenn nicht relevant
- Hint/Action-Dedup: keine doppelten Sätze pro Card mehr
Aktiviert via env MAIL_RENDER_V2=true (Default: legacy renderer).
5 neue deterministische Findings als Phase D-2b/B4/B5/B6/B7/B8:
B4 vendor_consistency_check — Cross-Doc-Provider-Widerspruch
(Elli: DSE nennt Vertex AI für Chatbot, /de/cookies nennt Iadvize → HIGH).
6 Service-Types: chatbot/analytics/tag_manager/pixel/cdn/cmp.
B5 ai_act_transparency_check — AI Act Art. 50 Transparenzpflicht
(Elli: Vertex AI vorhanden ohne Pre-Chat-Disclosure → HIGH).
Plus B5-Erweiterung: Rechtsgrundlage Art-6-Abs-1-lit-f bei AI → MED
(Einwilligung empfehlen).
B6 cross_doc_dpo_check — DPO in DSE genannt, nicht im Impressum (LOW).
B7 doc_staleness_check — Datum-Extraktion aus DSE/AGB/Nutzungsbedingungen.
Cap: AGB/NB 3y, DSE 2y. Älter → MEDIUM (Elli NB Stand 2018 → HIGH).
B8 cmp_fingerprint_check — Banner detected, aber CMP-Provider generic
(kein Usercentrics/OneTrust/Cookiebot/etc → MED).
B3-Erweiterung detect_intra_doc_contradictions — Widersprüchliche
Speicherdauer im SELBEN Doc (Elli: Logfile 7d vs 30d → HIGH).
LLM-Plausibility-Phase (Phase D-2b, finding_plausibility_check.py):
- Läuft AFTER MC pipeline, BEFORE D3 render
- Prompt mit Beispiel-IDs + 3-Phase-Mapping: exact-ID / position-fallback /
fuzzy-tail-match
- Stempelt llm_title / llm_severity / llm_recommendation / llm_drop auf
jeden FAIL CheckItem
- V2-Render zeigt "🤖 LLM-Plausibility:" Box pro Finding wenn gestempelt
- KNOWN ISSUE: qwen3:30b-a3b liefert oft empty content auf format='json' +
8000-char-excerpt prompts. Pipeline läuft mit stamped=0 weiter. Task #16.
Coverage gegen Elli Ground Truth (zeroclaw/docs/ground-truth/elli_eco_2026-06-06.json,
13 expected findings via WebFetch-Agent-Crawl):
- 4/4 HIGH-Findings ✓ (COOKIE-CONSENT-UX-001 + WIDERRUFSBELEHRUNG-001 +
VENDOR-CONSISTENCY-001 + AI-ACT-TRANSPARENCY-001)
- 4/6 MEDIUM ✓
- 2/3 LOW ✓
- Total: 10/13 = 77% (Sprung von 4/13 = 31%)
Restliche 3 Gaps als Task #17: IMPRESSUM-001 (multi-entity USt-IdNr),
TRANSFER-001 (Vendor-Mechanismus DPF/SCC), TH-RETENTION-002 (AI-Retention
pro Datenkategorie).
V2-Mail-Preview in Mailpit: 'v2all@local.test' Subject '[V2 ALL] ELLI'.
Backend healthy, B1+B3+B4+B5+B6+B7+B8 alle live im Orchestrator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
67 lines
2.1 KiB
Python
67 lines
2.1 KiB
Python
"""B6 — DPO in DSE genannt, im Impressum aber nicht verlinkt.
|
|
|
|
Best-Practice-Check nach DSGVO Art. 37 + § 5 TMG-Geist:
|
|
wenn die DSE einen Datenschutzbeauftragten benennt, sollte er
|
|
auch im Impressum referenziert sein (mind. Verweis "DSB siehe DSE")
|
|
— sonst geht die Kontaktmöglichkeit verloren, wenn die DSE separat
|
|
publiziert wird.
|
|
|
|
Severity LOW (nicht zwingend Pflicht), aber relevant für DSBs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Phrasen, die einen DSB / DPO in einem Text als benannt markieren
|
|
_DSB_NAMED_PATTERNS = [
|
|
re.compile(r"datenschutzbeauftrag\w+", re.I),
|
|
re.compile(r"data\s+protection\s+officer\b", re.I),
|
|
re.compile(r"\bdpo\b", re.I),
|
|
re.compile(r"privacy@\S+", re.I),
|
|
re.compile(r"datenschutz@\S+", re.I),
|
|
]
|
|
|
|
|
|
def _names_dsb(text: str) -> list[str]:
|
|
if not text:
|
|
return []
|
|
out: list[str] = []
|
|
for pat in _DSB_NAMED_PATTERNS:
|
|
for m in pat.finditer(text):
|
|
out.append(m.group(0))
|
|
if len(out) >= 3:
|
|
return out
|
|
return out
|
|
|
|
|
|
def check_dpo_cross_doc(state: dict) -> dict | None:
|
|
"""Return a finding when DSE names a DPO but Impressum does not."""
|
|
doc_texts = state.get("doc_texts") or {}
|
|
dse = doc_texts.get("dse") or ""
|
|
imp = doc_texts.get("impressum") or ""
|
|
if not dse or not imp:
|
|
return None
|
|
dse_hits = _names_dsb(dse)
|
|
imp_hits = _names_dsb(imp)
|
|
if dse_hits and not imp_hits:
|
|
finding = {
|
|
"check_id": "IMPRESSUM-DPO-001",
|
|
"severity": "LOW",
|
|
"severity_reason": "incomplete",
|
|
"title": "DSB im Impressum nicht verlinkt",
|
|
"norm": "DSGVO Art. 37 (Best Practice) + § 5 TMG-Geist",
|
|
"evidence_dse": dse_hits[:2],
|
|
"action": (
|
|
"Im Impressum den DSB-Kontakt verlinken oder Verweis "
|
|
"auf die Datenschutzerklärung ergänzen, damit Betroffene "
|
|
"auch über das Impressum den DSB erreichen."
|
|
),
|
|
}
|
|
logger.info("B6 DPO-cross-doc: DSE has DPO, Impressum doesn't")
|
|
return finding
|
|
return None
|