""" Vendor Assessment Pruefprotokoll — HTML report builder. Generates a professional assessment report styled like a real DSB Pruefprotokoll for vendor contract analysis (Art. 28 DSGVO). """ from datetime import datetime, timezone def build_pruefprotokoll(result: dict) -> str: """Build HTML Pruefprotokoll from assessment result.""" vendor = result.get("vendor_name", "Unbekannt") docs = result.get("documents", []) findings = result.get("findings", []) cross = result.get("cross_check_findings", []) cat_scores = result.get("category_scores", {}) overall = result.get("overall_score", 0) checked_at = result.get("checked_at", datetime.now(timezone.utc).isoformat()) verdict = _verdict(overall) now_str = _format_date(checked_at) protocol_nr = f"VP-{datetime.now().strftime('%Y')}-{abs(hash(vendor)) % 10000:04d}" html = [_style(), '
'] # ── 1. Kopfdaten ──────────────────────────────────────────────── html.append(f'''

Pruefprotokoll

Auftragsverarbeitung gem. Art. 28 DSGVO

Protokoll-Nr.{protocol_nr}
Pruefungsdatum{now_str}
Auftragsverarbeiter{vendor}
Pruefungsumfang{len(docs)} Dokument(e)
PrueferAutomatisierte Pruefung (BreakPilot Compliance SDK)
FreigabeAusstehend
''') # ── 2. Zusammenfassung ────────────────────────────────────────── critical_count = sum(1 for f in findings if _get(f, "severity") == "CRITICAL") critical_count += sum(1 for f in cross if f.get("severity") == "CRITICAL") total_findings = len(findings) + len(cross) html.append(f'''
{overall}%
{verdict["label"]}
{len(docs)}Dokumente
{total_findings}Findings
{critical_count}Kritisch
''') # ── Kategorie-Scores ──────────────────────────────────────────── if cat_scores: html.append('

Kategorie-Uebersicht

') html.append('') for cat, score in sorted(cat_scores.items(), key=lambda x: x[1]): status = _cat_status(score) html.append(f'''''') html.append('
KategorieScoreStatus
{_cat_label(cat)} {_bar(score)} {status["label"]}
') # ── 3. Gepruefte Dokumente ────────────────────────────────────── html.append('

Gepruefte Dokumente

') for i, doc in enumerate(docs): _render_document(html, doc, i + 1) # ── 4. Cross-Check Findings ───────────────────────────────────── if cross: html.append('

Dokumenten-Cross-Check

') for f in cross: sev = f.get("severity", "MEDIUM") html.append(f'''
{sev} {f.get("label", "")}

{f.get("hint", "")}

''') # ── 5. Findings ───────────────────────────────────────────────── if findings: html.append('

Findings (sortiert nach Schweregrad)

') sorted_findings = sorted(findings, key=lambda f: {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}.get( _get(f, "severity"), 4)) for f in sorted_findings: sev = _get(f, "severity") html.append(f'''
{sev} {_get(f, "title")}
{_get(f, "category")} | {_get(f, "document_label")}

{_get(f, "description")}

''') # ── 6. Freigabe-Block ─────────────────────────────────────────── html.append(f'''

Freigabe

Datum: _______________
Unterschrift DSB: _______________
''') html.append('
') return "\n".join(html) def _render_document(html: list[str], doc: dict, num: int): """Render a single document section with L1/L2 checks.""" label = doc.get("label", "Dokument") dtype = doc.get("doc_type", "").upper() comp = doc.get("completeness_pct", 0) corr = doc.get("correctness_pct", 0) error = doc.get("error", "") checks = doc.get("checks", []) html.append(f'''

{num}. {label} {dtype}

''') if error: html.append(f'
{error}
') return html.append(f'''
Vollstaendigkeit: {_bar(comp)} Korrektheit: {_bar(corr)}
''') # L1/L2 hierarchy l1_checks = [c for c in checks if c.get("level") == 1] l2_by_parent = {} for c in checks: if c.get("level") == 2 and c.get("parent"): l2_by_parent.setdefault(c["parent"], []).append(c) if l1_checks: html.append('') for c in l1_checks: passed = c.get("passed", False) skipped = c.get("skipped", False) icon = _icon(passed, skipped) sev = c.get("severity", "") html.append(f'''''') if not passed and not skipped and c.get("hint"): html.append(f'') for l2 in l2_by_parent.get(c.get("id", ""), []): l2_passed = l2.get("passed", False) l2_icon = _icon(l2_passed, l2.get("skipped", False)) html.append(f'''''') html.append('
{icon} {c.get("label", "")} {sev}
{c["hint"]}
{l2_icon} {l2.get("label", "")}
') html.append('') # ── Helpers ───────────────────────────────────────────────────────── def _get(obj, key, default=""): """Get from dict or Pydantic model.""" if isinstance(obj, dict): return obj.get(key, default) return getattr(obj, key, default) def _verdict(score: int) -> dict: if score >= 80: return {"label": "Bestanden", "class": "green"} if score >= 50: return {"label": "Bedingt bestanden — Nachbesserung erforderlich", "class": "yellow"} return {"label": "Nicht bestanden", "class": "red"} def _cat_status(score: int) -> dict: if score >= 80: return {"label": "Bestanden", "class": "green"} if score >= 50: return {"label": "Teilweise", "class": "yellow"} return {"label": "Mangelhaft", "class": "red"} _CAT_LABELS = { "INSTRUCTION": "Weisungsgebundenheit (Art. 28(3)(a))", "CONFIDENTIALITY": "Vertraulichkeit (Art. 28(3)(b))", "TOM": "Technische/Org. Massnahmen (Art. 32)", "SUBPROCESSOR": "Unterauftragsverarbeitung (Art. 28(3)(d))", "DATA_SUBJECT_RIGHTS": "Betroffenenrechte (Art. 28(3)(e))", "DELETION": "Loeschung/Rueckgabe (Art. 28(3)(g))", "AUDIT_RIGHTS": "Audit-/Inspektionsrechte (Art. 28(3)(h))", "INCIDENT": "Datenschutzverletzungen (Art. 33)", "TRANSFER": "Drittlandtransfer (Art. 44-49)", "LIABILITY": "Haftung (Art. 82)", "AVV_CONTENT": "AVV Inhalt (Art. 28(3))", "GENERAL": "Allgemein", } def _cat_label(cat: str) -> str: return _CAT_LABELS.get(cat, cat) def _bar(pct: int) -> str: color = "#22c55e" if pct >= 80 else "#eab308" if pct >= 50 else "#ef4444" return ( f'
' f'
' f'
{pct}%' ) def _icon(passed: bool, skipped: bool = False) -> str: if skipped: return '' if passed: return '' return '' def _format_date(iso: str) -> str: try: dt = datetime.fromisoformat(iso.replace("Z", "+00:00")) return dt.strftime("%d.%m.%Y %H:%M") except (ValueError, AttributeError): return datetime.now().strftime("%d.%m.%Y %H:%M") def _style() -> str: return ''''''