"""Vollständiger Cookie-CSV-Export. Eine Zeile pro deklariertem Cookie, mit: - Name + Vendor - Was die Site deklariert (category, lifetime, purpose, country) - Was die 3-Layer-KB sagt (actual_category, typical_lifetime, vendor_country, kb_source) - Alle Findings als FIND_* boolean-Spalten - recommended_action (1-Zeiler aus dem schwersten Finding) Output: bytes (UTF-8 CSV mit BOM für Excel-Kompatibilität). """ from __future__ import annotations import csv import io import logging from .cookie_library_lookup import lookup as kb_lookup logger = logging.getLogger(__name__) COLUMNS = [ "cookie_name", "vendor_declared", "kb_vendor", "kb_layer", "category_declared", "category_kb", "lifetime_declared", "lifetime_kb_typical", "purpose_declared", "country_declared", "country_kb", "optout_kb", "FIND_marketing_as_essential", "FIND_lifetime_too_long_for_essential", "FIND_pseudo_purpose", "FIND_missing_country", "FIND_missing_retention", "FIND_unknown_vendor", "FIND_duplicate_vendor", "FIND_third_country_no_mechanism", "recommended_action", "source_in_audit", ] def _action_for(findings_for_cookie: list[dict]) -> str: """Pick the action from the highest-severity finding.""" if not findings_for_cookie: return "" priority = {"HIGH": 0, "MEDIUM": 1, "LOW": 2, "INFO": 3} sorted_f = sorted( findings_for_cookie, key=lambda f: priority.get((f.get("severity") or "").upper(), 9), ) return sorted_f[0].get("recommended_action", "") or "" def build_cookie_csv(state: dict) -> bytes: """Iterate cmp_vendors + cookies, write CSV bytes.""" cmp_vendors = state.get("cmp_vendors") or [] coherence_findings = state.get("cookie_coherence_findings") or [] # Index findings by cookie_name for fast lookup by_cookie: dict[str, list[dict]] = {} duplicate_vendors: set[str] = set() for f in coherence_findings: cname = f.get("cookie_name") if cname: by_cookie.setdefault(cname, []).append(f) if f.get("check_id") == "COOKIE-COHERENCE-DUP-001": duplicate_vendors.add((f.get("vendor") or "").lower()) buf = io.StringIO() # Excel-compatible BOM so Umlauts render correctly buf.write("") writer = csv.writer(buf, delimiter=";", quoting=csv.QUOTE_MINIMAL) writer.writerow(COLUMNS) written = 0 for v in cmp_vendors: vendor_name = (v.get("name") or "").strip() vendor_src = (v.get("source") or "").strip() vendor_country = (v.get("country") or "").strip() vendor_category = (v.get("category") or "").strip() for c in (v.get("cookies") or []): cname = (c.get("name") or "").strip() if not cname: continue declared_cat = (c.get("category") or vendor_category).strip() declared_purpose = (c.get("purpose") or v.get("purpose") or "").strip() declared_lifetime = (c.get("duration") or c.get("persistence") or c.get("expiry") or "").strip() kb = kb_lookup(cname) kb_vendor = (kb.get("vendor_name") or kb.get("vendor") or "") kb_layer = kb.get("_layer") or "unknown" kb_category = (kb.get("actual_category") or kb.get("consensus_category") or "") kb_country = (kb.get("vendor_country") or "") kb_optout = (kb.get("vendor_opt_out_url") or "") kb_typical_lifetime = (kb.get("typical_lifetime") or "") if not kb_typical_lifetime and kb.get("typical_max_age_seconds"): secs = kb["typical_max_age_seconds"] if secs: days = secs / 86400.0 kb_typical_lifetime = ( f"{int(days)} Tage" if days >= 1 else f"{int(secs / 3600)} h" if secs >= 3600 else f"{int(secs / 60)} min" ) f_cookie = by_cookie.get(cname) or [] check_ids = {fp.get("check_id") for fp in f_cookie} row = [ cname, vendor_name, kb_vendor, kb_layer, declared_cat, kb_category, declared_lifetime, kb_typical_lifetime, declared_purpose[:300], vendor_country, kb_country, kb_optout, "1" if "COOKIE-COHERENCE-MAE-001" in check_ids else "", "1" if "COOKIE-COHERENCE-LIFE-001" in check_ids else "", "1" if "COOKIE-COHERENCE-PURP-001" in check_ids else "", "1" if "COOKIE-COHERENCE-CTRY-001" in check_ids else "", "1" if not declared_lifetime else "", "1" if "COOKIE-COHERENCE-UNK-001" in check_ids else "", "1" if vendor_name.lower() in duplicate_vendors else "", "1" if (kb_country and kb_country.upper() not in ("DE", "EU", "AT", "FR", "NL", "IT", "ES", "BE", "CH", "IE", "DK", "FI", "SE", "NO") and not c.get("transfer_mechanism")) else "", _action_for(f_cookie), vendor_src, ] writer.writerow(row) written += 1 logger.info("cookie-csv export: %d rows", written) return buf.getvalue().encode("utf-8")