feat(audit): P92 CMP-Tool-Verfuegbarkeit + P94 Banner-vs-Cookie-Doc-Konsistenz
CI / detect-changes (push) Successful in 11s
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 16s
CI / loc-budget (push) Failing after 17s
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 42s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / detect-changes (push) Successful in 11s
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 16s
CI / loc-budget (push) Failing after 17s
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 42s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
P92 — Wenn der Nutzer 'Anpassen'/'Einstellungen' klickt und der CMP-Settings-Bereich kein Fehlerfreies Laden zeigt (Error, Timeout, <80 Zeichen ohne Kategorien, keine Toggles), ist das ein HIGH- Finding. Granulare Wahl formal vorhanden, faktisch nicht funktionsfaehig (Art. 7 (3) DSGVO + EDPB 03/2022). P94 — Cookie-Liste im Banner-Settings vs Cookie-Richtlinie. Heuristik extrahiert Cookie-Namen aus dem Cookie-Doc-Text (regex auf typische camelCase/_underscored Patterns + Vendor-Prefixes _ga/_gid/ot_/uc_). Wenn |only_in_doc| >= 5 ODER |only_in_banner| >= 3 → MEDIUM-Finding. |only_in_doc| >= 15 UND |only_in_banner| >= 5 → HIGH. Beide Findings landen im neuen Mail-Block 'Banner-Konsistenz-Pruefung' (amber-yellow) zwischen Mismatch-Block und VVT. Auch in check_replay.py eingehaengt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1081,6 +1081,24 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("P102 mismatch detection failed: %s", e)
|
logger.warning("P102 mismatch detection failed: %s", e)
|
||||||
|
|
||||||
|
# P92 + P94: Banner-Konsistenz (CMP-Tool kaputt / Banner-vs-Doc-Diff)
|
||||||
|
consistency_html = ""
|
||||||
|
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_findings = run_consistency_checks(
|
||||||
|
banner_result or {}, cookie_doc_for_check,
|
||||||
|
)
|
||||||
|
if cons_findings:
|
||||||
|
consistency_html = build_consistency_block_html(cons_findings)
|
||||||
|
logger.info("P92/P94: %d Konsistenz-Findings", len(cons_findings))
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("P92/P94 consistency-check failed: %s", e)
|
||||||
|
|
||||||
# P82: GF-1-Pager ganz oben in der Mail — 5-Bullet-Zusammenfassung
|
# P82: GF-1-Pager ganz oben in der Mail — 5-Bullet-Zusammenfassung
|
||||||
# damit die GF nicht 124k Char lesen muss.
|
# damit die GF nicht 124k Char lesen muss.
|
||||||
gf_one_pager_html = ""
|
gf_one_pager_html = ""
|
||||||
@@ -1103,6 +1121,7 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
|
|||||||
+ cookie_arch_html + summary_html + scanned_html + profile_html
|
+ cookie_arch_html + summary_html + scanned_html + profile_html
|
||||||
+ scorecard_html + redundancy_html
|
+ scorecard_html + redundancy_html
|
||||||
+ providers_html + banner_deep_html + library_mismatch_html
|
+ providers_html + banner_deep_html + library_mismatch_html
|
||||||
|
+ consistency_html
|
||||||
+ vvt_html + report_html
|
+ vvt_html + report_html
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
"""
|
||||||
|
P92 + P94 — Banner-Konsistenz-Checks (Post-hoc auf banner_result).
|
||||||
|
|
||||||
|
P92 — CMP-Tool-Verfuegbarkeit:
|
||||||
|
Wenn "Anpassen"/"Einstellungen" angeklickt wurde und das Tool laed
|
||||||
|
nicht (Network-Error, Timeout, weisse Seite, fehlende
|
||||||
|
consent-Elemente nach Klick), ist das ein HIGH-Verstoss — der
|
||||||
|
Nutzer hat formal die Moeglichkeit zur granularen Wahl, aber sie
|
||||||
|
funktioniert nicht.
|
||||||
|
|
||||||
|
P94 — Banner-Init-vs-Cookie-Footer-Konsistenz:
|
||||||
|
Cookie-Liste im Initial-Banner-Settings darf nicht von der Liste
|
||||||
|
im permanenten Cookie-Richtlinien-Dokument abweichen. Wenn Banner
|
||||||
|
12 Cookies nennt, die Cookie-Doc aber 47, ist mindestens eine der
|
||||||
|
beiden Quellen unvollstaendig → MEDIUM-Finding.
|
||||||
|
|
||||||
|
Beide liefern dict mit shape:
|
||||||
|
{"severity": "HIGH"|"MEDIUM", "code": str, "label": str, "detail": str}
|
||||||
|
oder None, wenn der Check nicht greift.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_ANPASSEN_KEYS = (
|
||||||
|
"anpassen", "einstellungen", "customize", "preferences",
|
||||||
|
"settings", "individuelle", "auswahl", "manage",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _phases(banner_result: dict) -> dict:
|
||||||
|
if not isinstance(banner_result, dict):
|
||||||
|
return {}
|
||||||
|
return banner_result.get("phases") or {}
|
||||||
|
|
||||||
|
|
||||||
|
def check_cmp_tool_availability(banner_result: dict) -> dict | None:
|
||||||
|
"""P92 — Anpassen-Klick aber Settings-Tool defekt / leer."""
|
||||||
|
phases = _phases(banner_result)
|
||||||
|
settings_ph = phases.get("settings") or phases.get("after_settings_click")
|
||||||
|
if not isinstance(settings_ph, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
initial_ph = phases.get("initial") or phases.get("before_accept") or {}
|
||||||
|
initial_text = (initial_ph.get("banner_text") or "").lower()
|
||||||
|
if not any(k in initial_text for k in _ANPASSEN_KEYS):
|
||||||
|
return None # Wenn kein Anpassen-Button gar nicht im Initial-Banner,
|
||||||
|
# ist das P100s Job — nicht hier doppelt melden.
|
||||||
|
|
||||||
|
error = settings_ph.get("error") or settings_ph.get("status_error")
|
||||||
|
settings_text = (settings_ph.get("banner_text") or "").strip()
|
||||||
|
has_categories = bool(
|
||||||
|
settings_ph.get("categories")
|
||||||
|
or settings_ph.get("category_tests")
|
||||||
|
or (settings_ph.get("structured_checks") or [])
|
||||||
|
)
|
||||||
|
has_toggles = bool(re.search(r"checkbox|toggle|switch|aria-checked",
|
||||||
|
(settings_ph.get("banner_html") or ""), re.I))
|
||||||
|
timed_out = bool(settings_ph.get("timeout"))
|
||||||
|
|
||||||
|
failure_signals: list[str] = []
|
||||||
|
if error:
|
||||||
|
failure_signals.append(f'Fehler: {str(error)[:120]}')
|
||||||
|
if timed_out:
|
||||||
|
failure_signals.append('Zeitueberschreitung beim Laden')
|
||||||
|
if len(settings_text) < 80 and not has_categories:
|
||||||
|
failure_signals.append(
|
||||||
|
f'Settings-Bereich nur {len(settings_text)} Zeichen, '
|
||||||
|
'keine Kategorien sichtbar'
|
||||||
|
)
|
||||||
|
if not has_toggles and not has_categories:
|
||||||
|
failure_signals.append(
|
||||||
|
'Keine Checkboxen / Toggles im Settings-Bereich'
|
||||||
|
)
|
||||||
|
|
||||||
|
if not failure_signals:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"severity": "HIGH",
|
||||||
|
"code": "cmp_tool_unavailable",
|
||||||
|
"label": 'Cookie-Einstellungen ueber "Anpassen" formal vorhanden, '
|
||||||
|
'Tool laed aber nicht oder ist leer',
|
||||||
|
"detail": " | ".join(failure_signals),
|
||||||
|
"legal_basis": "Art. 7 (3) DSGVO + EDPB 03/2022 — die Moeglichkeit "
|
||||||
|
"zur granularen Auswahl muss tatsaechlich funktionieren.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_cookie_names(items) -> set[str]:
|
||||||
|
out: set[str] = set()
|
||||||
|
if not items:
|
||||||
|
return out
|
||||||
|
for it in items:
|
||||||
|
if isinstance(it, str):
|
||||||
|
name = it.strip()
|
||||||
|
elif isinstance(it, dict):
|
||||||
|
name = (it.get("name") or it.get("cookie") or it.get("id") or "").strip()
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
if name and len(name) <= 120:
|
||||||
|
out.add(name.lower())
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def check_init_banner_vs_cookie_doc(
|
||||||
|
banner_result: dict,
|
||||||
|
cookie_doc_text: str | None,
|
||||||
|
) -> dict | None:
|
||||||
|
"""P94 — Cookie-Liste im Init-Banner vs in der Cookie-Richtlinie."""
|
||||||
|
if not cookie_doc_text or len(cookie_doc_text) < 500:
|
||||||
|
return None
|
||||||
|
|
||||||
|
phases = _phases(banner_result)
|
||||||
|
banner_cookies = _normalize_cookie_names(
|
||||||
|
(phases.get("settings") or {}).get("cookies") or []
|
||||||
|
) | _normalize_cookie_names(
|
||||||
|
(phases.get("initial") or phases.get("before_accept") or {}).get("cookies") or []
|
||||||
|
)
|
||||||
|
|
||||||
|
# Aus dem Cookie-Doc-Text: Cookie-Namen sind typischerweise
|
||||||
|
# camelCase oder _underscored, 4-40 Zeichen, ohne Leerzeichen.
|
||||||
|
candidates = set(re.findall(
|
||||||
|
r"\b([A-Za-z_][A-Za-z0-9_\-\.]{3,40})\b", cookie_doc_text
|
||||||
|
))
|
||||||
|
# Filter: heuristisch wahrscheinliche Cookie-Namen
|
||||||
|
doc_cookies: set[str] = set()
|
||||||
|
for c in candidates:
|
||||||
|
cl = c.lower()
|
||||||
|
if any(p in cl for p in (
|
||||||
|
"_ga", "_gid", "_gcl", "_fbp", "uc_", "ot_",
|
||||||
|
"cookieconsent", "sessionid", "csrf", "ajs_", "amp_",
|
||||||
|
"datadome", "incap_", "_pk_", "wp-", "yt-",
|
||||||
|
)):
|
||||||
|
doc_cookies.add(cl)
|
||||||
|
elif re.match(r"^[a-z][a-z0-9_]{3,30}$", cl) and (
|
||||||
|
"cookie" in cl or "consent" in cl or "track" in cl or "session" in cl
|
||||||
|
):
|
||||||
|
doc_cookies.add(cl)
|
||||||
|
|
||||||
|
if len(doc_cookies) < 5 or not banner_cookies:
|
||||||
|
return None # Datenlage zu duenn fuer sinnvolle Aussage.
|
||||||
|
|
||||||
|
only_in_doc = doc_cookies - banner_cookies
|
||||||
|
only_in_banner = banner_cookies - doc_cookies
|
||||||
|
|
||||||
|
if len(only_in_doc) < 5 and len(only_in_banner) < 3:
|
||||||
|
return None # Tolerable Abweichung.
|
||||||
|
|
||||||
|
severity = "MEDIUM"
|
||||||
|
# HIGH wenn beide Seiten massiv abweichen — dann fehlt klar
|
||||||
|
# die Cross-Reference.
|
||||||
|
if len(only_in_doc) >= 15 and len(only_in_banner) >= 5:
|
||||||
|
severity = "HIGH"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"severity": severity,
|
||||||
|
"code": "banner_cookie_doc_mismatch",
|
||||||
|
"label": (
|
||||||
|
f"Cookie-Liste im Banner-Einstellungen ({len(banner_cookies)}) "
|
||||||
|
f"weicht von Cookie-Richtlinie ({len(doc_cookies)}) ab"
|
||||||
|
),
|
||||||
|
"detail": (
|
||||||
|
f"Nur im Cookie-Dokument: {len(only_in_doc)} Cookies (Beispiele: "
|
||||||
|
f"{', '.join(sorted(only_in_doc)[:5])}). "
|
||||||
|
f"Nur im Banner: {len(only_in_banner)} Cookies. "
|
||||||
|
"Empfehlung: eine der beiden Quellen als Single-Source-of-Truth "
|
||||||
|
"definieren und die andere automatisch generieren."
|
||||||
|
),
|
||||||
|
"legal_basis": (
|
||||||
|
"Art. 13(1)(c) DSGVO + Art. 12 DSGVO — Informationen ueber die "
|
||||||
|
"Verarbeitung muessen vollstaendig und konsistent sein."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_all(banner_result: dict, cookie_doc_text: str | None = None) -> list[dict]:
|
||||||
|
findings: list[dict] = []
|
||||||
|
try:
|
||||||
|
f1 = check_cmp_tool_availability(banner_result)
|
||||||
|
if f1:
|
||||||
|
findings.append(f1)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("P92 cmp_tool_availability failed: %s", e)
|
||||||
|
try:
|
||||||
|
f2 = check_init_banner_vs_cookie_doc(banner_result, cookie_doc_text)
|
||||||
|
if f2:
|
||||||
|
findings.append(f2)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("P94 init_vs_cookie_doc failed: %s", e)
|
||||||
|
return findings
|
||||||
|
|
||||||
|
|
||||||
|
def build_consistency_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'<li style="margin-bottom:10px;font-size:11px;line-height:1.5">'
|
||||||
|
f'<strong style="color:{sev_color}">[{sev}] {f.get("label","")}</strong>'
|
||||||
|
f'<div style="color:#475569;margin-top:3px">{f.get("detail","")}</div>'
|
||||||
|
f'<div style="color:#94a3b8;margin-top:2px;font-style:italic">'
|
||||||
|
f'{f.get("legal_basis","")}</div>'
|
||||||
|
f'</li>'
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
'<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;'
|
||||||
|
'max-width:760px;margin:0 auto 16px;padding:14px 18px;'
|
||||||
|
'background:#fef3c7;border:1px solid #fcd34d;border-radius:8px">'
|
||||||
|
'<div style="font-size:11px;color:#92400e;text-transform:uppercase;'
|
||||||
|
'letter-spacing:1.2px;margin-bottom:4px;font-weight:600">'
|
||||||
|
'Banner-Konsistenz-Pruefung</div>'
|
||||||
|
f'<h3 style="margin:0 0 6px;font-size:14px;color:#1e293b">'
|
||||||
|
f'{len(findings)} Konsistenz-Finding{"s" if len(findings) != 1 else ""} '
|
||||||
|
'zwischen Banner-UI und Cookie-Richtlinie</h3>'
|
||||||
|
'<ul style="margin:8px 0 0 18px;padding:0">'
|
||||||
|
+ "".join(items) +
|
||||||
|
'</ul></div>'
|
||||||
|
)
|
||||||
@@ -132,6 +132,21 @@ def replay_from_snapshot(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Replay: vvt failed: %s", e)
|
logger.warning("Replay: vvt 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)
|
||||||
|
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
|
# P102: Cookie-Klassifikations-Pruefung
|
||||||
try:
|
try:
|
||||||
from compliance.services.cookie_library_mismatch import (
|
from compliance.services.cookie_library_mismatch import (
|
||||||
|
|||||||
Reference in New Issue
Block a user