""" A — Audit-Transparenz / Audit-Quality-Checks. Wenn der Crawler nicht alles gefunden hat, MUSS die Mail das prominent zeigen — sonst denkt der User 'alles gut' obwohl die Datenlage Luecken hat. Erkennt 4 Quality-Failures: 1. banner_detected=False trotz vorhandenem Cookie-Doc → CMP-Tool ungeladen 2. cookie_doc >= 30k chars aber cmp_vendors < 10 → Vendor-Extract unvollstaendig 3. doc_text submitted aber 0 chars geladen → Crawler-Failure 4. cmp_vendors > 0 aber alle aus llm_cascade ohne Library-Match → vermutl. unvollstaendig Diese Findings landen IMMER im GF-1-Pager (auch wenn kein anderes HIGH-Finding da ist) — sie sagen "die Datenlage ist unvollstaendig, manuelle Pruefung empfohlen". """ from __future__ import annotations import logging logger = logging.getLogger(__name__) def _word_count(text: str | None) -> int: if not text: return 0 return len(text.split()) def check_banner_not_detected( banner_result: dict | None, cookie_doc_text: str | None, ) -> dict | None: """1) Banner nicht geladen aber Cookie-Doc vorhanden → CMP-Tool kaputt.""" if not isinstance(banner_result, dict): return None detected = banner_result.get("banner_detected") if detected is None or detected is True: return None if not cookie_doc_text or len(cookie_doc_text) < 5000: return None return { "severity": "HIGH", "code": "audit_banner_not_detected", "label": "Audit-Vorbehalt: Cookie-Banner konnte vom Crawler nicht " "geladen werden", "area": "Cookie-Banner", "owner": "DSB + Marketing/CMP-Admin", "detail": ( "Unser Crawler konnte das CMP-Tool dieser Site nicht analysieren — " "weder Vendor-Liste noch Cookie-Verhalten konnten geprueft werden. " "Moegliche Ursachen: Anti-Bot-Schutz (Akamai/Cloudflare/DataDome) " "blockiert Playwright; das CMP-Skript laed nur fuer bestimmte " "Geo-Regionen; ein neues CMP-Tool das wir noch nicht unterstuetzen. " "Empfehlung: manuelle Pruefung des Banners durch DSB, alternativ " "Cookie-Tabelle im Audit-Tool direkt einfuegen (Copy-Paste-Modus)." ), "legal_basis": "Art. 5 (2) DSGVO Rechenschaftspflicht — der Audit-" "Befund muss transparent zwischen 'geprueft & OK' und " "'nicht pruefbar' unterscheiden.", } def check_vendor_extract_incomplete( cookie_doc_text: str | None, cmp_vendors: list | None, ) -> dict | None: """2) Cookie-Doc gross aber wenig Vendors → Extract unvollstaendig. Dynamische Schwelle nach Doc-Groesse: * 3k-6k Wörter → mind. 10 Vendors erwartet * 6k-10k Wörter → mind. 20 Vendors * 10k-15k Wörter → mind. 30 Vendors * 15k+ Wörter → mind. 40 Vendors """ wc = _word_count(cookie_doc_text) n_vendors = len(cmp_vendors or []) if wc < 3000: return None # Erwartete Vendor-Anzahl heuristisch nach Doc-Groesse if wc >= 15000: expected = 40 elif wc >= 10000: expected = 30 elif wc >= 6000: expected = 20 else: expected = 10 if n_vendors >= expected: return None return { "severity": "HIGH" if wc >= 8000 else "MEDIUM", "code": "audit_vendor_extract_thin", "label": ( f"Audit-Vorbehalt: Cookie-Richtlinie hat {wc:,} Wörter, " f"erwartet ~{expected} Vendors, extrahiert nur {n_vendors}" ).replace(",", "."), "area": "Vendor-Liste / VVT", "owner": "DSB + Marketing", "detail": ( f"Bei einer Cookie-Richtlinie mit {wc:,} Woertern erwarten wir " f"typischerweise {expected}+ unique Vendors. Die extrahierte Zahl " f"({n_vendors}) ist auffaellig niedrig — entweder hat unser " "Parser/LLM die Tabelle nicht vollstaendig erfasst oder " "Vendors wurden zu konservativ erkannt. Empfehlung: Cookie-" "Tabelle im Copy-Paste-Modus einreichen (Frontend-Toggle " "'Text einfuegen' pro Cookie-Doc-Zeile) — dort parsen wir " "Spalten deterministisch." ).replace(",", "."), "legal_basis": "Art. 13(1)(e) DSGVO — die Empfaengerliste muss " "vollstaendig sein; ein unvollstaendiger Audit darf " "nicht als vollstaendig dargestellt werden.", } def check_url_fetch_failed(doc_entries: list | None) -> list[dict]: """3) Submitted URL aber 0 oder Mini-Text → Crawler-Failure pro Doc.""" out: list[dict] = [] for e in (doc_entries or []): if not isinstance(e, dict): continue url = (e.get("url") or "").strip() text = (e.get("text") or "").strip() if not url or len(text) >= 200 or e.get("auto_discovered"): continue dt = e.get("doc_type", "doc") rejected = e.get("rejected_url") or "" out.append({ "severity": "MEDIUM", "code": f"audit_url_fetch_failed_{dt}", "label": ( f"Audit-Vorbehalt: {dt}-URL konnte nicht geladen werden " f"({len(text)} Zeichen extrahiert)" ), "area": dt, "owner": "DSB + Web-Team", "detail": ( f"Die eingegebene URL {url[:120]} lieferte weniger als 200 " "Zeichen. Moegliche Ursachen: 404, JS-only Render, Anti-Bot, " "Cookie-Wall. Auto-Discovery hat versucht eine Alternative " "auf der Homepage zu finden — ohne Erfolg. Empfehlung: " "korrekte URL pruefen oder den Text direkt einfuegen " "(Copy-Paste-Modus)." ), "legal_basis": "Art. 5 (2) DSGVO Rechenschaftspflicht.", }) return out def run_all( banner_result: dict | None, cookie_doc_text: str | None, cmp_vendors: list | None, doc_entries: list | None, ) -> list[dict]: findings: list[dict] = [] try: f1 = check_banner_not_detected(banner_result, cookie_doc_text) if f1: findings.append(f1) except Exception as e: logger.warning("audit_banner_not_detected failed: %s", e) try: f2 = check_vendor_extract_incomplete(cookie_doc_text, cmp_vendors) if f2: findings.append(f2) except Exception as e: logger.warning("audit_vendor_extract_thin failed: %s", e) try: findings.extend(check_url_fetch_failed(doc_entries)) except Exception as e: logger.warning("audit_url_fetch_failed failed: %s", e) return findings def build_audit_quality_block_html(findings: list[dict]) -> str: if not findings: return "" items: list[str] = [] for f in findings: sev = f.get("severity", "MEDIUM") sev_color = "#dc2626" if sev == "HIGH" else "#d97706" items.append( f'
  • ' f'[{sev}] {f.get("label","")}' f'
    {f.get("detail","")}
    ' f'
    ' f'{f.get("legal_basis","")}
    ' f'
  • ' ) return ( '
    ' '
    ' 'Audit-Vorbehalt — Datenlage unvollstaendig
    ' f'

    ' f'{len(findings)} Punkt' f'{"e" if len(findings) != 1 else ""} bei denen der Audit selbst ' f'an Grenzen gestossen ist

    ' '

    ' 'Die folgenden Punkte betreffen NICHT die Compliance Ihrer Website, ' 'sondern die Vollstaendigkeit unserer Pruefung. Bei diesen Bereichen ' 'sollten Sie den Audit nicht als "alles ok" werten, sondern manuell ' 'oder im Copy-Paste-Modus nachpruefen.' '

    ' '
    ' )