Files
breakpilot-compliance/backend-compliance/compliance/services/check_replay.py
T
Benjamin Admin 3207acea3e
CI / detect-changes (push) Successful in 10s
CI / branch-name (push) Has been skipped
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 16s
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) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 44s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
fix(audit): Replay-Pipeline um P35/P77/P78/P36 Signals-Block ergaenzen
check_replay.py rendert jetzt auch die Textsignal-Findings (Save-Label-
Ambiguitaet, Cookies-in-DSE-Akzeptanz, JC-Klausel positiv, Social-Embeds).
Damit hat der Replay-Test parity mit der echten Mail-Pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:04:02 +02:00

220 lines
8.1 KiB
Python

"""
P80 — Replay-Pipeline (Mini-Version v1).
Lädt einen persistierten Snapshot und rendert die Audit-Mail mit dem
AKTUELLEN Mail-Render-Code neu. Nutzbar fuer:
* Mail-Layout-Aenderungen (P63-P67, P82 1-Pager, P84 Diff-Mode) testen
* Action-Recipes anpassen
* Disclaimer-Text iterieren
* Pattern-Notice-Logik tunen
NICHT enthalten (kommt in v2):
* MC-Scorecard re-run mit aktuellem scope_doc_type-Filter (P72) —
erfordert MC-Pipeline-Refactoring aus _run_compliance_check
* Vendor-Redundancy-Analyse re-run
Effekt v1: 7min Re-Scan -> 2-5 Sek fuer Mail-Layout-Iterationen.
Effekt v2 (spaeter): auch fuer MC-Filter-Tests.
"""
from __future__ import annotations
import logging
from typing import Any
from sqlalchemy.orm import Session
from compliance.services.check_snapshot import load_snapshot
logger = logging.getLogger(__name__)
def replay_from_snapshot(
db: Session,
snapshot_id: str,
recipient: str | None = None,
dry_run: bool = False,
) -> dict:
"""Replay audit mail render from snapshot.
Args:
db: SQLAlchemy session
snapshot_id: UUID of snapshot to replay
recipient: Override email recipient. None = skip send.
dry_run: If True, render HTML but do not send mail.
Returns:
{"snapshot_id", "html_size", "sections", "mail_sent", "preview"}
"""
snap = load_snapshot(db, snapshot_id)
if not snap:
return {"error": "snapshot not found", "snapshot_id": snapshot_id}
doc_entries = snap.get("doc_entries") or []
banner_result = snap.get("banner_result") or {}
profile_dict = snap.get("profile") or {}
cmp_vendors = snap.get("cmp_vendors") or []
site_label = snap.get("site_label") or snap.get("site_domain")
# Reconstruct doc_texts mapping (was the input to mail-render).
# Snapshot-Schema speichert text unter "text" (nicht full_text).
doc_texts: dict[str, str] = {}
for e in doc_entries:
dt = e.get("doc_type", "")
txt = (e.get("text") or e.get("full_text") or e.get("text_preview") or "").strip()
if dt and txt:
doc_texts[dt] = txt
# Build results list mock (just enough for mail-render)
def _dict_to_result(d: dict) -> Any:
"""Best-effort reconstruction. Snapshot didn't persist DocCheckResult
so we fake minimal fields. For real MC-replay (v2) we'd re-run the
check_document_completeness function against the snapshot text."""
return type("R", (), {
"doc_type": d.get("doc_type", "other"),
"label": d.get("doc_type", "Dokument"),
"completeness_pct": d.get("completeness_pct", 0),
"correctness_pct": d.get("correctness_pct"),
"checks": [],
"error": d.get("error", ""),
})()
results = [_dict_to_result(e) for e in doc_entries]
# Render mail sections
section_sizes: dict[str, int] = {}
parts: list[str] = []
# P82: GF-1-Pager zuerst (5-Bullet-Summary)
try:
from compliance.services.gf_one_pager import build_gf_one_pager_html
gf_html = build_gf_one_pager_html(
site_name=site_label or "",
scorecard=None, # Snapshot enthaelt keine MC-Scorecard
banner_result=banner_result,
library_mismatch_findings=None, # wird unten gefuellt
scan_context=snap.get("scan_context"),
)
parts.append(gf_html)
section_sizes["gf_one_pager"] = len(gf_html)
except Exception as e:
logger.warning("Replay: GF-1-pager failed: %s", e)
try:
from compliance.api.agent_doc_check_critical import build_critical_findings_html
critical_html = build_critical_findings_html(banner_result, None, results) or ""
parts.append(critical_html)
section_sizes["critical"] = len(critical_html)
except Exception as e:
logger.warning("Replay: critical-block failed: %s", e)
try:
from compliance.api.scope_disclaimer import build_scope_disclaimer_html
disclaimer = build_scope_disclaimer_html()
parts.append(disclaimer)
section_sizes["disclaimer"] = len(disclaimer)
except Exception as e:
logger.warning("Replay: disclaimer failed: %s", e)
try:
from compliance.api.agent_doc_check_banner import build_banner_deep_html
banner_html = build_banner_deep_html(banner_result) or ""
parts.append(banner_html)
section_sizes["banner"] = len(banner_html)
except Exception as e:
logger.warning("Replay: banner-block failed: %s", e)
try:
from compliance.api.vvt_table_renderer import build_vvt_table_html
vvt_html = build_vvt_table_html(cmp_vendors) or ""
parts.append(vvt_html)
section_sizes["vvt"] = len(vvt_html)
except Exception as e:
logger.warning("Replay: vvt failed: %s", e)
# P35 + P77 + P78 + P36: Textsignale (Save-Label, Cookies-in-DSE,
# JC-Klausel, Social-Embeds)
try:
from compliance.services.doc_text_signals import (
run_all as run_signal_checks,
build_signals_block_html,
)
cookie_doc_missing = not bool(doc_texts.get("cookie"))
sig_findings = run_signal_checks(
banner_result, doc_texts, cookie_doc_missing,
)
if sig_findings:
sig_html = build_signals_block_html(sig_findings)
parts.append(sig_html)
section_sizes["signals"] = len(sig_html)
except Exception as e:
logger.warning("Replay: signals block failed: %s", e)
# P92 + P94: Banner-Konsistenz
try:
from compliance.services.banner_consistency_checks import (
run_all as run_consistency_checks,
build_consistency_block_html,
)
cookie_doc_for_check = doc_texts.get("cookie") or doc_texts.get("dse") or ""
cons = run_consistency_checks(
banner_result or {}, cookie_doc_for_check, cmp_vendors,
)
if cons:
cons_html = build_consistency_block_html(cons)
parts.append(cons_html)
section_sizes["consistency"] = len(cons_html)
except Exception as e:
logger.warning("Replay: consistency block failed: %s", e)
# P102: Cookie-Klassifikations-Pruefung
try:
from compliance.services.cookie_library_mismatch import (
detect_mismatches, build_mismatch_block_html,
)
cookies_seen: list[str] = []
for ph in (banner_result.get("phases") or {}).values():
if isinstance(ph, dict):
for ck in (ph.get("cookies") or []):
if isinstance(ck, str):
cookies_seen.append(ck)
elif isinstance(ck, dict) and ck.get("name"):
cookies_seen.append(ck["name"])
doc_for_check = doc_texts.get("cookie") or doc_texts.get("dse") or ""
if cookies_seen and doc_for_check:
mm = detect_mismatches(db, cookies_seen, doc_for_check)
if mm:
mm_html = build_mismatch_block_html(mm)
parts.append(mm_html)
section_sizes["library_mismatch"] = len(mm_html)
except Exception as e:
logger.warning("Replay: mismatch block failed: %s", e)
full_html = "".join(parts)
result = {
"snapshot_id": snapshot_id,
"check_id": snap.get("check_id"),
"site_domain": snap.get("site_domain"),
"html_size": len(full_html),
"sections": section_sizes,
"mail_sent": False,
"preview": full_html[:500] + "..." if len(full_html) > 500 else full_html,
}
if recipient and not dry_run:
try:
from compliance.services.smtp_sender import send_email
email_res = send_email(
recipient=recipient,
subject=f"[REPLAY] {site_label} (Snapshot {snapshot_id[:8]})",
body_html=full_html,
)
result["mail_sent"] = (email_res.get("status") == "sent")
result["mail_status"] = email_res.get("status")
except Exception as e:
logger.warning("Replay: mail send failed: %s", e)
result["mail_send_error"] = str(e)[:200]
return result