""" Cookie Table Generator — renders {{COOKIE_TABLE}} placeholder content. Generates structured Markdown tables grouped by cookie category, showing: Cookie/Tool | Anbieter | Zweck | Typ | Dauer | Drittland Data sources: 1. BannerVendorConfigDB per site (if site_config_id provided) 2. Service Registry (fallback for unconfiguered sites) 3. First-party defaults (session, consent cookie) """ import logging from typing import Optional from sqlalchemy.orm import Session from compliance.db.banner_models import ( BannerSiteConfigDB, BannerVendorConfigDB, ) logger = logging.getLogger(__name__) # First-party cookies that every site has FIRST_PARTY_COOKIES = [ { "name": "session_id", "provider": "Eigener Server", "purpose": "Session-Verwaltung", "category": "necessary", "retention": "Session", "third_country": "Nein", }, { "name": "bp_consent", "provider": "Eigener Server", "purpose": "Speicherung der Cookie-Einwilligung", "category": "necessary", "retention": "12 Monate", "third_country": "Nein", }, { "name": "bp_lang", "provider": "Eigener Server", "purpose": "Spracheinstellung", "category": "functional", "retention": "12 Monate", "third_country": "Nein", }, ] # Known cookies per service (from service registry) SERVICE_COOKIES = { "Google Analytics": { "cookies": ["_ga", "_ga_*", "_gid", "_gat"], "purpose": "Nutzungsanalyse und Reichweitenmessung", "provider": "Google LLC", "third_country": "USA (DPF)", }, "Google Tag Manager": { "cookies": ["_gcl_au"], "purpose": "Tag-Verwaltung und Conversion-Tracking", "provider": "Google LLC", "third_country": "USA (DPF)", }, "Google Ads": { "cookies": ["_gcl_aw", "_gcl_dc", "IDE"], "purpose": "Werbe-Conversion-Messung", "provider": "Google LLC", "third_country": "USA (DPF)", }, "Facebook Pixel": { "cookies": ["_fbp", "_fbc", "fr"], "purpose": "Werbe-Tracking und Remarketing", "provider": "Meta Platforms Inc.", "third_country": "USA (DPF)", }, "Hotjar": { "cookies": ["_hj*", "_hjid", "_hjSession*"], "purpose": "Heatmaps und Session-Recording", "provider": "Hotjar Ltd.", "third_country": "Nein (EU — Malta)", }, "Matomo": { "cookies": ["_pk_id.*", "_pk_ses.*"], "purpose": "Datenschutzfreundliche Nutzungsanalyse", "provider": "InnoCraft Ltd.", "third_country": "Nein (EU/Self-Hosted)", }, "Stripe": { "cookies": ["__stripe_mid", "__stripe_sid"], "purpose": "Zahlungsabwicklung", "provider": "Stripe Inc.", "third_country": "USA (DPF)", }, "PayPal": { "cookies": ["PYPF", "tsrce"], "purpose": "Zahlungsabwicklung", "provider": "PayPal (Europe) S.a.r.l.", "third_country": "Nein (EU — Luxemburg)", }, "Klarna": { "cookies": ["klarna_*"], "purpose": "Zahlungsabwicklung (BNPL)", "provider": "Klarna Bank AB", "third_country": "Nein (EU — Schweden)", }, "YouTube": { "cookies": ["YSC", "VISITOR_INFO1_LIVE", "CONSENT"], "purpose": "Video-Einbettung", "provider": "Google LLC", "third_country": "USA (DPF)", }, "Cloudflare": { "cookies": ["__cf_bm", "cf_clearance"], "purpose": "CDN und DDoS-Schutz", "provider": "Cloudflare Inc.", "third_country": "USA (DPF)", }, "Usercentrics": { "cookies": ["uc_*"], "purpose": "Consent-Management", "provider": "Usercentrics GmbH", "third_country": "Nein (EU — Deutschland)", }, "Cookiebot": { "cookies": ["CookieConsent", "CookieConsentBulkTicket"], "purpose": "Consent-Management", "provider": "Cybot A/S", "third_country": "Nein (EU — Daenemark)", }, "HubSpot": { "cookies": ["__hstc", "__hssc", "hubspotutk"], "purpose": "CRM und Marketing-Automation", "provider": "HubSpot Inc.", "third_country": "USA (DPF)", }, "LinkedIn Insight": { "cookies": ["_li_ss", "bcookie", "li_sugr"], "purpose": "B2B-Marketing und Conversion-Tracking", "provider": "LinkedIn Ireland UC", "third_country": "Nein (EU — Irland)", }, "Sentry": { "cookies": ["sentry-sc"], "purpose": "Fehlererfassung und Monitoring", "provider": "Sentry (Functional Software Inc.)", "third_country": "USA (DPF)", }, } CATEGORY_LABELS = { "necessary": "Technisch notwendig", "functional": "Funktional", "statistics": "Analyse / Statistik", "marketing": "Marketing / Tracking", } CATEGORY_ORDER = ["necessary", "functional", "statistics", "marketing"] def _format_retention(days: int) -> str: if days == 0: return "Session" if days <= 1: return "1 Tag" if days <= 30: return f"{days} Tage" if days <= 365: months = round(days / 30) return f"{months} Monat{'e' if months > 1 else ''}" years = round(days / 365, 1) if years == int(years): return f"{int(years)} Jahr{'e' if int(years) > 1 else ''}" return f"{years} Jahre" def generate_cookie_table_from_vendors( db: Session, tenant_id: str, site_id: str, ) -> str: """Generate {{COOKIE_TABLE}} content from vendor configs in the database.""" import uuid as _uuid tid = _uuid.UUID(tenant_id) config = ( db.query(BannerSiteConfigDB) .filter(BannerSiteConfigDB.tenant_id == tid, BannerSiteConfigDB.site_id == site_id) .first() ) rows_by_category: dict[str, list[dict]] = {cat: [] for cat in CATEGORY_ORDER} # First-party cookies for fp in FIRST_PARTY_COOKIES: cat = fp["category"] if cat in rows_by_category: rows_by_category[cat].append(fp) # Vendor cookies from DB if config: vendors = ( db.query(BannerVendorConfigDB) .filter(BannerVendorConfigDB.site_config_id == config.id, BannerVendorConfigDB.is_active) .all() ) for v in vendors: cat = v.category_key if cat not in rows_by_category: rows_by_category[cat] = [] known = SERVICE_COOKIES.get(v.vendor_name, {}) cookies = known.get("cookies", v.cookie_names or []) rows_by_category[cat].append({ "name": ", ".join(cookies) if cookies else v.vendor_name, "provider": known.get("provider", v.description_de or v.vendor_name), "purpose": known.get("purpose", v.description_de or ""), "category": cat, "retention": _format_retention(v.retention_days or 365), "third_country": known.get("third_country", ""), }) return _render_tables(rows_by_category) def generate_cookie_table_from_registry() -> str: """Generate {{COOKIE_TABLE}} from service registry (no DB needed).""" from compliance.services.vendor_banner_sync import ( CATEGORY_MAP, RETENTION_DEFAULTS, SERVICE_RETENTION, ) from compliance.services.service_registry import SERVICE_REGISTRY rows_by_category: dict[str, list[dict]] = {cat: [] for cat in CATEGORY_ORDER} for fp in FIRST_PARTY_COOKIES: rows_by_category[fp["category"]].append(fp) for _pattern, meta in SERVICE_REGISTRY.items(): service_id = meta.get("id", "") if service_id == "cmp": continue category = meta.get("category", "other") banner_cat = CATEGORY_MAP.get(category, "functional") name = meta["name"] known = SERVICE_COOKIES.get(name, {}) retention = SERVICE_RETENTION.get(service_id, RETENTION_DEFAULTS.get(banner_cat, 365)) if banner_cat not in rows_by_category: rows_by_category[banner_cat] = [] rows_by_category[banner_cat].append({ "name": ", ".join(known.get("cookies", [])) if known.get("cookies") else name, "provider": known.get("provider", meta.get("provider", "")), "purpose": known.get("purpose", f"{name} ({meta.get('provider', '')})"), "category": banner_cat, "retention": _format_retention(retention), "third_country": known.get("third_country", _infer_third_country(meta)), }) return _render_tables(rows_by_category) def _infer_third_country(meta: dict) -> str: country = meta.get("country", "") eu_adequate = meta.get("eu_adequate", False) if not country: return "" if country == "US": return "USA (DPF)" if eu_adequate else "USA" if country in ("DE", "AT", "FR", "NL", "IE", "SE", "DK", "FI", "BE", "IT", "ES", "MT", "LU"): return f"Nein (EU)" return country def _render_tables(rows_by_category: dict[str, list[dict]]) -> str: parts = [] for cat in CATEGORY_ORDER: rows = rows_by_category.get(cat, []) if not rows: continue label = CATEGORY_LABELS.get(cat, cat) parts.append(f"#### {label}\n") parts.append("| Cookie/Tool | Anbieter | Zweck | Typ | Dauer | Drittland |") parts.append("|---|---|---|---|---|---|") for r in rows: parts.append( f"| {r['name']} | {r['provider']} | {r['purpose']} | " f"{CATEGORY_LABELS.get(r['category'], r['category'])} | " f"{r['retention']} | {r['third_country']} |" ) parts.append("") return "\n".join(parts)