feat(email): P18 — Critical-Findings-Box + Banner-Deep-Block
CI / detect-changes (push) Successful in 12s
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 15s
CI / loc-budget (push) Successful in 20s
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) Successful in 3m8s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 47s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / detect-changes (push) Successful in 12s
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 15s
CI / loc-budget (push) Successful in 20s
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) Successful in 3m8s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 47s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
Backend wirft 90% der consent-tester-Daten weg — nur 4 Felder von einem
vollen Banner-Scan landeten im Email. Phases (before_consent / after_reject
/ after_accept), banner_checks.violations mit Rechtsgrundlagen,
category_tests, 46 structured_checks, completeness/correctness-Scores
waren alle nicht sichtbar.
Backend: agent_compliance_check_routes leitet jetzt das volle banner_result
durch (15 Felder statt 4).
Renderer (2 neue Module):
1) agent_doc_check_critical.build_critical_findings_html
- ROTER Sofortmassnahmen-Block GANZ OBEN in der Email
- Erkennt: banner-violations (HIGH/CRITICAL), leere Per-Category-Lists,
DSE-Score <30%, fehlende Cookie-Richtlinie, US-Tracker ohne SCC/DPF
- Pro Issue: konkrete Sofortmassnahme + Rechtsgrundlage + Bussgeld-
Praezedenz (CNIL TikTok 5 Mio, LfDI BW 30k, EuGH Schrems II, ...)
- Wird nur gerendert wenn echte Issues vorliegen
2) agent_doc_check_banner.build_banner_deep_html
- Banner-Quality-Score-Cards (Vollstaendigkeit / Korrektheit / Verstoesse)
- 3-Phasen-Cookie-Tabelle: vor Consent / nach Ablehnung / nach Annahme
mit Cookie-Count, Tracker-Count, Auffaelligkeiten
- Per-Category-Tracker-Listing (Statistik/Marketing) — zeigt explizit
wenn eine Kategorie keine Provider listet (Safetykon-Pattern)
- Violations-Liste mit Severity-Badge + Quellen-Hint (LG Rostock, EDPB)
Smoke-Test Safetykon: alle 6 neuen Blocks rendern, kein Regression.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -614,6 +614,9 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
|
|||||||
summary_html = build_management_summary(results)
|
summary_html = build_management_summary(results)
|
||||||
scanned_html = build_scanned_urls_html(doc_entries)
|
scanned_html = build_scanned_urls_html(doc_entries)
|
||||||
providers_html = build_provider_list_html(banner_result, vvt_entries)
|
providers_html = build_provider_list_html(banner_result, vvt_entries)
|
||||||
|
# P18: Deep-Block mit Phases + Quality-Score + Per-Category-Tracker
|
||||||
|
from .agent_doc_check_banner import build_banner_deep_html
|
||||||
|
banner_deep_html = build_banner_deep_html(banner_result)
|
||||||
vvt_html = build_vvt_table_html(cmp_vendors)
|
vvt_html = build_vvt_table_html(cmp_vendors)
|
||||||
|
|
||||||
# MC scorecard aggregated across ALL docs in this run (DSGVO/TDDDG/
|
# MC scorecard aggregated across ALL docs in this run (DSGVO/TDDDG/
|
||||||
@@ -676,6 +679,20 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
|
|||||||
site_name=site_name_for_exec,
|
site_name=site_name_for_exec,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# P18: Critical-Findings-Block (rot oben, mit Sofortmassnahmen +
|
||||||
|
# Quellen + Bussgeld-Praezedenz). Wird nur gerendert wenn echte
|
||||||
|
# kritische Verstoesse vorliegen.
|
||||||
|
critical_html = ""
|
||||||
|
try:
|
||||||
|
from .agent_doc_check_critical import build_critical_findings_html
|
||||||
|
critical_html = build_critical_findings_html(
|
||||||
|
banner_result=banner_result,
|
||||||
|
scorecard=scorecard,
|
||||||
|
results=results,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Critical-findings block skipped: %s", e)
|
||||||
|
|
||||||
# P10: Cookie-Policy-Architecture-Detection (BMW-Pattern erkennen)
|
# P10: Cookie-Policy-Architecture-Detection (BMW-Pattern erkennen)
|
||||||
cookie_arch_html = ""
|
cookie_arch_html = ""
|
||||||
try:
|
try:
|
||||||
@@ -726,10 +743,10 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
|
|||||||
# 7) providers_html + vvt_html (Vendor-Liste)
|
# 7) providers_html + vvt_html (Vendor-Liste)
|
||||||
# 8) report_html (Doc-Pruefung Details)
|
# 8) report_html (Doc-Pruefung Details)
|
||||||
full_html = (
|
full_html = (
|
||||||
exec_summary_html + cookie_arch_html + summary_html
|
critical_html + exec_summary_html + cookie_arch_html
|
||||||
+ scanned_html + profile_html
|
+ summary_html + scanned_html + profile_html
|
||||||
+ scorecard_html + redundancy_html
|
+ scorecard_html + redundancy_html
|
||||||
+ providers_html + vvt_html + report_html
|
+ providers_html + banner_deep_html + vvt_html + report_html
|
||||||
)
|
)
|
||||||
|
|
||||||
# Step 6: Send email — derive site name primarily from entered URL.
|
# Step 6: Send email — derive site name primarily from entered URL.
|
||||||
@@ -753,12 +770,23 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
|
|||||||
"results": [_result_to_dict(r) for r in results],
|
"results": [_result_to_dict(r) for r in results],
|
||||||
"business_profile": profile_dict,
|
"business_profile": profile_dict,
|
||||||
"extracted_profile": extracted_profile,
|
"extracted_profile": extracted_profile,
|
||||||
"banner_result": {
|
# P18: vollen consent-tester-Output durchreichen statt nur 4 Felder.
|
||||||
"detected": banner_result.get("banner_detected", False) if banner_result else False,
|
# phases (before/after-accept/reject) + banner_checks.violations +
|
||||||
"provider": banner_result.get("banner_provider", "") if banner_result else "",
|
# category_tests werden vom Renderer + Critical-Findings-Block genutzt.
|
||||||
"violations": len(banner_result.get("banner_checks", {}).get("violations", [])) if banner_result else 0,
|
"banner_result": ({
|
||||||
|
"detected": banner_result.get("banner_detected", False),
|
||||||
|
"provider": banner_result.get("banner_provider", ""),
|
||||||
|
"violations": len((banner_result.get("banner_checks") or {})
|
||||||
|
.get("violations", [])),
|
||||||
"tcf_vendor_count": len(tcf_vendors),
|
"tcf_vendor_count": len(tcf_vendors),
|
||||||
} if banner_result else None,
|
"completeness_pct": banner_result.get("completeness_pct"),
|
||||||
|
"correctness_pct": banner_result.get("correctness_pct"),
|
||||||
|
"phases": banner_result.get("phases", {}),
|
||||||
|
"banner_checks": banner_result.get("banner_checks", {}),
|
||||||
|
"category_tests": banner_result.get("category_tests", []),
|
||||||
|
"structured_checks": banner_result.get("structured_checks", []),
|
||||||
|
"summary": banner_result.get("summary", {}),
|
||||||
|
} if banner_result else None),
|
||||||
"tcf_vendors": vvt_entries if tcf_vendors else [],
|
"tcf_vendors": vvt_entries if tcf_vendors else [],
|
||||||
"cmp_vendors": cmp_vendors,
|
"cmp_vendors": cmp_vendors,
|
||||||
"total_documents": len(results),
|
"total_documents": len(results),
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
"""
|
||||||
|
P18 — Erweiterter Banner-Block fuer die Email.
|
||||||
|
|
||||||
|
Rendert die Daten aus dem consent-tester die heute weggeworfen wurden:
|
||||||
|
- 3-Phasen-Cookie-Tabelle (before_consent / after_reject / after_accept)
|
||||||
|
- Banner-Quality-Score (completeness/correctness/violations)
|
||||||
|
- Per-Category-Tracker-Listing
|
||||||
|
- Violations-Liste mit Rechtsgrundlagen
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
def _color_for(pct: int) -> str:
|
||||||
|
return ("#16a34a" if pct >= 80 else
|
||||||
|
"#d97706" if pct >= 50 else "#dc2626")
|
||||||
|
|
||||||
|
|
||||||
|
def _short_phase_label(key: str) -> str:
|
||||||
|
return {
|
||||||
|
"before_consent": "Vor Consent",
|
||||||
|
"after_reject": "Nach Ablehnung",
|
||||||
|
"after_accept": "Nach Annahme",
|
||||||
|
}.get(key, key)
|
||||||
|
|
||||||
|
|
||||||
|
def _phase_color(key: str, cookie_count: int) -> str:
|
||||||
|
if key == "before_consent":
|
||||||
|
return "#16a34a" if cookie_count == 0 else "#dc2626"
|
||||||
|
if key == "after_reject":
|
||||||
|
return "#16a34a" if cookie_count <= 1 else "#d97706"
|
||||||
|
return "#94a3b8"
|
||||||
|
|
||||||
|
|
||||||
|
def build_banner_deep_html(banner_result: dict | None) -> str:
|
||||||
|
"""Render: Banner-Quality + Phases + Violations.
|
||||||
|
|
||||||
|
Konsumiert das volle consent-tester-Response. Komplementiert
|
||||||
|
`build_provider_list_html` (das nur Summary + TCF-Vendor-Tabelle macht).
|
||||||
|
"""
|
||||||
|
if not banner_result:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
parts: list[str] = [
|
||||||
|
'<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;'
|
||||||
|
'max-width:700px;margin:0 auto 16px;padding:14px 18px;'
|
||||||
|
'background:#fff;border:1px solid #cbd5e1;border-radius:8px">'
|
||||||
|
'<h3 style="margin:0 0 12px;font-size:14px;color:#0f172a">'
|
||||||
|
'Cookie-Banner — technische Analyse</h3>'
|
||||||
|
]
|
||||||
|
|
||||||
|
# 1) Quality-Score-Cards
|
||||||
|
compl = banner_result.get("completeness_pct")
|
||||||
|
corr = banner_result.get("correctness_pct")
|
||||||
|
summary = banner_result.get("summary") or {}
|
||||||
|
n_critical = summary.get("critical", 0)
|
||||||
|
n_high = summary.get("high", 0)
|
||||||
|
if compl is not None or corr is not None:
|
||||||
|
parts.append(
|
||||||
|
'<table style="width:100%;border-collapse:separate;'
|
||||||
|
'border-spacing:6px;margin-bottom:10px"><tr>'
|
||||||
|
)
|
||||||
|
if compl is not None:
|
||||||
|
c = _color_for(int(compl))
|
||||||
|
parts.append(
|
||||||
|
f'<td style="width:33%;padding:8px 10px;background:#f8fafc;'
|
||||||
|
f'border-radius:5px;border-left:3px solid {c}">'
|
||||||
|
f'<div style="font-size:10px;color:#64748b;text-transform:uppercase">'
|
||||||
|
f'Vollstaendigkeit</div>'
|
||||||
|
f'<div style="font-size:18px;font-weight:700;color:{c}">{compl}%</div>'
|
||||||
|
f'</td>'
|
||||||
|
)
|
||||||
|
if corr is not None:
|
||||||
|
c = _color_for(int(corr))
|
||||||
|
parts.append(
|
||||||
|
f'<td style="width:33%;padding:8px 10px;background:#f8fafc;'
|
||||||
|
f'border-radius:5px;border-left:3px solid {c}">'
|
||||||
|
f'<div style="font-size:10px;color:#64748b;text-transform:uppercase">'
|
||||||
|
f'Korrektheit</div>'
|
||||||
|
f'<div style="font-size:18px;font-weight:700;color:{c}">{corr}%</div>'
|
||||||
|
f'</td>'
|
||||||
|
)
|
||||||
|
viol_c = ("#dc2626" if n_critical + n_high > 0 else
|
||||||
|
"#d97706" if (summary.get("total_violations") or 0) > 0 else
|
||||||
|
"#16a34a")
|
||||||
|
parts.append(
|
||||||
|
f'<td style="width:33%;padding:8px 10px;background:#f8fafc;'
|
||||||
|
f'border-radius:5px;border-left:3px solid {viol_c}">'
|
||||||
|
f'<div style="font-size:10px;color:#64748b;text-transform:uppercase">'
|
||||||
|
f'Verstoesse</div>'
|
||||||
|
f'<div style="font-size:18px;font-weight:700;color:{viol_c}">'
|
||||||
|
f'{summary.get("total_violations", 0)}'
|
||||||
|
f'<span style="font-size:11px;color:#64748b;margin-left:6px">'
|
||||||
|
f'(crit:{n_critical} high:{n_high})</span></div></td>'
|
||||||
|
)
|
||||||
|
parts.append('</tr></table>')
|
||||||
|
|
||||||
|
# 2) 3-Phasen-Tabelle
|
||||||
|
phases = banner_result.get("phases") or {}
|
||||||
|
if phases:
|
||||||
|
parts.append(
|
||||||
|
'<div style="font-size:11px;color:#475569;margin:8px 0 4px;'
|
||||||
|
'font-weight:600">Cookie-Setzungen pro Phase '
|
||||||
|
'(echter Browser-Test):</div>'
|
||||||
|
'<table style="width:100%;border-collapse:collapse;font-size:11px;'
|
||||||
|
'margin-bottom:10px;border:1px solid #e2e8f0">'
|
||||||
|
'<thead><tr style="background:#f1f5f9;color:#475569;text-align:left">'
|
||||||
|
'<th style="padding:5px 8px">Phase</th>'
|
||||||
|
'<th style="padding:5px 8px;text-align:center">Cookies</th>'
|
||||||
|
'<th style="padding:5px 8px;text-align:center">Tracker</th>'
|
||||||
|
'<th style="padding:5px 8px">Auffaelligkeiten</th>'
|
||||||
|
'</tr></thead><tbody>'
|
||||||
|
)
|
||||||
|
for key in ("before_consent", "after_reject", "after_accept"):
|
||||||
|
ph = phases.get(key) or {}
|
||||||
|
if not isinstance(ph, dict): continue
|
||||||
|
cookies = ph.get("cookies") or []
|
||||||
|
trackers = ph.get("tracking_services") or []
|
||||||
|
new_track = ph.get("new_tracking") or []
|
||||||
|
violations = ph.get("violations") or []
|
||||||
|
undoc = ph.get("undocumented") or []
|
||||||
|
color = _phase_color(key, len(cookies))
|
||||||
|
issues_parts = []
|
||||||
|
if violations: issues_parts.append(f"{len(violations)} Verstoss")
|
||||||
|
if new_track: issues_parts.append(f"{len(new_track)} neue Tracker")
|
||||||
|
if undoc: issues_parts.append(f"{len(undoc)} undokumentiert")
|
||||||
|
issues_str = ", ".join(issues_parts) or "—"
|
||||||
|
parts.append(
|
||||||
|
f'<tr style="border-top:1px solid #e2e8f0">'
|
||||||
|
f'<td style="padding:5px 8px;color:#1e293b;font-weight:600">'
|
||||||
|
f'<span style="display:inline-block;width:6px;height:6px;'
|
||||||
|
f'border-radius:50%;background:{color};margin-right:6px"></span>'
|
||||||
|
f'{_short_phase_label(key)}</td>'
|
||||||
|
f'<td style="padding:5px 8px;text-align:center;color:{color};'
|
||||||
|
f'font-weight:600">{len(cookies)}</td>'
|
||||||
|
f'<td style="padding:5px 8px;text-align:center">{len(trackers)}</td>'
|
||||||
|
f'<td style="padding:5px 8px;color:#475569">{issues_str}</td>'
|
||||||
|
f'</tr>'
|
||||||
|
)
|
||||||
|
parts.append('</tbody></table>')
|
||||||
|
|
||||||
|
# 3) Per-Category-Tracker
|
||||||
|
cats = banner_result.get("category_tests") or []
|
||||||
|
if cats:
|
||||||
|
non_essential = [c for c in cats if c.get("category") != "necessary"]
|
||||||
|
if non_essential:
|
||||||
|
parts.append(
|
||||||
|
'<div style="font-size:11px;color:#475569;margin:8px 0 4px;'
|
||||||
|
'font-weight:600">Provider-Listing pro Banner-Kategorie:</div>'
|
||||||
|
'<table style="width:100%;border-collapse:collapse;font-size:11px;'
|
||||||
|
'margin-bottom:10px;border:1px solid #e2e8f0">'
|
||||||
|
'<thead><tr style="background:#f1f5f9;color:#475569;text-align:left">'
|
||||||
|
'<th style="padding:5px 8px">Kategorie</th>'
|
||||||
|
'<th style="padding:5px 8px;text-align:center">Anbieter</th>'
|
||||||
|
'<th style="padding:5px 8px">Hinweis</th>'
|
||||||
|
'</tr></thead><tbody>'
|
||||||
|
)
|
||||||
|
for c in non_essential:
|
||||||
|
n = len(c.get("tracking_services") or [])
|
||||||
|
label = c.get("category_label") or c.get("category", "?")
|
||||||
|
if n == 0:
|
||||||
|
color = "#dc2626"
|
||||||
|
hint = ("Keine Anbieter sichtbar — Nutzer kann nicht "
|
||||||
|
"informiert einwilligen (Art. 7 DSGVO)")
|
||||||
|
else:
|
||||||
|
color = "#16a34a"
|
||||||
|
hint = ""
|
||||||
|
parts.append(
|
||||||
|
f'<tr style="border-top:1px solid #e2e8f0">'
|
||||||
|
f'<td style="padding:5px 8px">{label}</td>'
|
||||||
|
f'<td style="padding:5px 8px;text-align:center;color:{color};'
|
||||||
|
f'font-weight:600">{n}</td>'
|
||||||
|
f'<td style="padding:5px 8px;color:#dc2626;font-size:10px">'
|
||||||
|
f'{hint}</td></tr>'
|
||||||
|
)
|
||||||
|
parts.append('</tbody></table>')
|
||||||
|
|
||||||
|
# 4) Violations mit Rechtsgrundlage
|
||||||
|
violations = (banner_result.get("banner_checks") or {}).get("violations", [])
|
||||||
|
if violations:
|
||||||
|
parts.append(
|
||||||
|
'<div style="font-size:11px;color:#475569;margin:8px 0 4px;'
|
||||||
|
'font-weight:600">Erkannte Banner-Verstoesse:</div>'
|
||||||
|
'<ul style="margin:0 0 8px 18px;padding:0;font-size:11px;color:#1e293b">'
|
||||||
|
)
|
||||||
|
for v in violations[:8]:
|
||||||
|
sev = (v.get("severity") or "MEDIUM").upper()
|
||||||
|
sev_c = ("#dc2626" if sev in ("CRITICAL", "HIGH") else
|
||||||
|
"#d97706" if sev == "MEDIUM" else "#94a3b8")
|
||||||
|
parts.append(
|
||||||
|
f'<li style="margin-bottom:6px">'
|
||||||
|
f'<span style="display:inline-block;background:{sev_c};color:#fff;'
|
||||||
|
f'font-size:9px;padding:1px 5px;border-radius:3px;margin-right:6px">'
|
||||||
|
f'{sev}</span>{v.get("text", "")[:200]}'
|
||||||
|
f'<div style="font-size:10px;color:#94a3b8;margin-top:2px;'
|
||||||
|
f'font-style:italic">Quelle: {v.get("legal_ref", "")}</div></li>'
|
||||||
|
)
|
||||||
|
parts.append('</ul>')
|
||||||
|
|
||||||
|
parts.append('</div>')
|
||||||
|
return "".join(parts)
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# 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, 5k–50k 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": v.get("text", "")[:120],
|
||||||
|
"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: leere Vendor-Liste pro Kategorie (Safetykon-Pattern)
|
||||||
|
cat_tests = br.get("category_tests") or []
|
||||||
|
empty_cats = [c for c in cat_tests
|
||||||
|
if not c.get("tracking_services")
|
||||||
|
and c.get("category") != "necessary"]
|
||||||
|
if empty_cats and len(cat_tests) >= 2:
|
||||||
|
cats = ", ".join(c.get("category_label", c.get("category", "?"))
|
||||||
|
for c in empty_cats)
|
||||||
|
issues.append({
|
||||||
|
"key": "no_provider_per_category",
|
||||||
|
"title": f"Cookie-Banner Kategorien ({cats}) ohne Provider-Listing",
|
||||||
|
"severity": "HIGH",
|
||||||
|
"action": ("Pro Banner-Kategorie eine Liste der eingebundenen "
|
||||||
|
"Anbieter + Cookie-Details (Name, Zweck, Speicherdauer, "
|
||||||
|
"Drittlandtransfer) ergaenzen. Sonst ist die "
|
||||||
|
"Einwilligung nicht 'informiert' nach Art. 7 DSGVO."),
|
||||||
|
"source": "Art. 7 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 Critical-Findings-Box. Leerer String wenn keine Issues."""
|
||||||
|
issues = _detect_critical_issues(banner_result, scorecard, results)
|
||||||
|
if not issues:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for i in issues:
|
||||||
|
items.append(
|
||||||
|
f'<div style="margin-bottom:10px;padding:10px 12px;'
|
||||||
|
f'background:rgba(255,255,255,0.06);border-radius:4px;'
|
||||||
|
f'border-left:3px solid #fca5a5">'
|
||||||
|
f'<div style="font-size:13px;font-weight:700;color:#fff;'
|
||||||
|
f'margin-bottom:4px">'
|
||||||
|
f'<span style="display:inline-block;background:#dc2626;color:#fff;'
|
||||||
|
f'padding:1px 6px;border-radius:3px;font-size:9px;'
|
||||||
|
f'margin-right:6px">{i["severity"]}</span>{i["title"]}</div>'
|
||||||
|
f'<div style="font-size:11px;color:#fecaca;margin-top:4px">'
|
||||||
|
f'<strong>Sofortmassnahme:</strong> {i["action"]}</div>'
|
||||||
|
f'<div style="font-size:10px;color:#fca5a5;margin-top:4px;'
|
||||||
|
f'font-style:italic">Rechtsgrundlage: {i.get("source","")}'
|
||||||
|
+ (f' · Praezedenz: {i["bussgeld"]}'
|
||||||
|
if i.get("bussgeld") else "") +
|
||||||
|
f'</div></div>'
|
||||||
|
)
|
||||||
|
|
||||||
|
n = len(issues)
|
||||||
|
return (
|
||||||
|
'<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;'
|
||||||
|
'max-width:700px;margin:0 auto 18px;padding:18px 22px;'
|
||||||
|
'background:#7f1d1d;border-radius:10px;color:white">'
|
||||||
|
'<div style="font-size:12px;color:#fecaca;text-transform:uppercase;'
|
||||||
|
'letter-spacing:1.5px;margin-bottom:6px;font-weight:700">'
|
||||||
|
'🚨 Sofortmassnahmen erforderlich</div>'
|
||||||
|
f'<h2 style="margin:0 0 10px;font-size:18px;color:white">'
|
||||||
|
f'{n} kritische Compliance-Risiken mit Bussgeldpotenzial</h2>'
|
||||||
|
'<p style="margin:0 0 12px;font-size:12px;color:#fecaca">'
|
||||||
|
'Die folgenden Verstoesse sind durch Tool-Analyse belegt und '
|
||||||
|
'erfordern Sofortmassnahmen. Bussgeldrahmen nach Art. 83 DSGVO: '
|
||||||
|
'<strong>bis 4% des weltweiten Jahresumsatzes</strong>.</p>'
|
||||||
|
+ "".join(items) +
|
||||||
|
'</div>'
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user