diff --git a/backend-compliance/compliance/api/agent_compliance_check_routes.py b/backend-compliance/compliance/api/agent_compliance_check_routes.py index d33e28ea..5d312061 100644 --- a/backend-compliance/compliance/api/agent_compliance_check_routes.py +++ b/backend-compliance/compliance/api/agent_compliance_check_routes.py @@ -1486,6 +1486,23 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest): except Exception as e: logger.warning("P71 jc_avv_decision skipped: %s", e) + # P6/P53/P55 — Branchen-Kontext + Site-History + industry_ctx_html = "" + try: + from compliance.services.industry_library import ( + build_industry_context_block_html, load_site_profile, + ) + from database import SessionLocal as _SLib + _ind_db = _SLib() + try: + ind = (req.scan_context or {}).get("industry") if req.scan_context else None + site_prof = load_site_profile(_ind_db, domain_for_exec or "") + industry_ctx_html = build_industry_context_block_html(ind, site_prof) + finally: + _ind_db.close() + except Exception as e: + logger.warning("industry context skipped: %s", e) + # P85 — Banner-Screenshot fuer visuellen Beweis (zwischen # GF-1-Pager und Detail-Bloecken) banner_shot_html = "" @@ -1596,6 +1613,7 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest): + critical_html + scope_disclaimer_html + exec_summary_html + cookie_arch_html + summary_html + scanned_html + profile_html + scorecard_html + redundancy_html + + industry_ctx_html + banner_shot_html + providers_html + banner_deep_html + cookie_audit_html diff --git a/backend-compliance/compliance/data/ecall_regulation.json b/backend-compliance/compliance/data/ecall_regulation.json new file mode 100644 index 00000000..1ae0bc51 --- /dev/null +++ b/backend-compliance/compliance/data/ecall_regulation.json @@ -0,0 +1,50 @@ +{ + "source": "Verordnung (EU) 2015/758 - eCall", + "official_url": "https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX%3A32015R0758", + "ingest_for": "RAG-Korpus (Compliance fuer Automotive-OEMs)", + "chunks": [ + { + "id": "ecall-art-3-1", + "title": "Art. 3 (1) — bordeigenes eCall-System", + "text": "Hersteller stellen sicher, dass alle neuen Typen von Personenkraftwagen und leichten Nutzfahrzeugen mit einem auf 112 basierten bordeigenen eCall-System ausgestattet sind, das den in dieser Verordnung festgelegten Anforderungen und harmonisierten Normen entspricht." + }, + { + "id": "ecall-art-6-1", + "title": "Art. 6 (1) — Datenschutz", + "text": "Bei der Verarbeitung personenbezogener Daten ueber das auf 112 basierte bordeigene eCall-System gewaehrleisten Hersteller die Einhaltung der Richtlinie 95/46/EG und der RL 2002/58/EG. Insbesondere muessen Fahrzeughalter darueber informiert werden, dass das System dauerhaft im Standby-Modus ist und im Falle eines schweren Unfalls automatisch ausgeloest wird." + }, + { + "id": "ecall-art-6-2", + "title": "Art. 6 (2) — Datenverarbeitung", + "text": "Die Verarbeitung personenbezogener Daten ueber das auf 112 basierte bordeigene eCall-System darf nur zum Zwecke der Bearbeitung von Notrufen erfolgen. Diese Daten sind unmittelbar nach Bearbeitung des Notrufs ohne automatisierte Speicherung zu loeschen, soweit nicht anders gesetzlich vorgesehen." + }, + { + "id": "ecall-art-6-3", + "title": "Art. 6 (3) — Standortdaten", + "text": "Die Standortdaten des Fahrzeugs werden zur Behandlung des Notrufes uebermittelt. Eine permanente Standortueberwachung ausserhalb von Notfaellen ist nicht zulaessig." + }, + { + "id": "ecall-art-6-4", + "title": "Art. 6 (4) — Informationspflicht", + "text": "Hersteller stellen sicher, dass in der technischen Dokumentation des Fahrzeugs klare und vollstaendige Informationen ueber die Verarbeitung personenbezogener Daten gegeben werden, einschliesslich des Rechts der betroffenen Person auf Auskunft und gegebenenfalls Berichtigung sowie Sperrung der sie betreffenden personenbezogenen Daten." + }, + { + "id": "ecall-art-6-5", + "title": "Art. 6 (5) — Mehrwertdienste", + "text": "Mehrwertdienste (z.B. private Pannenruf-Apps) duerfen nur mit ausdruecklicher Einwilligung des Fahrzeughalters in Anspruch genommen werden. Das auf 112 basierte bordeigene eCall-System darf nicht von diesen Mehrwertdiensten beeintraechtigt werden und muss kostenlos und fuer alle Fahrzeughalter verfuegbar sein." + }, + { + "id": "ecall-art-7", + "title": "Art. 7 — Datenfluss", + "text": "Der Mindestdatensatz (MSD) umfasst Fahrzeug-ID (VIN), Ausloesungsart, Zeitstempel, Standort, Fahrtrichtung, Antriebsenergie, Anzahl angeschnallter Insassen. Diese Daten gehen an die naechste oeffentliche Notrufabfragestelle (PSAP)." + } + ], + "compliance_implications": { + "automotive_oem": [ + "Hersteller MUSS in der DSE den eCall-Datenfluss erklaeren (Art. 6 (4)).", + "Standortdaten ausserhalb von Notfaellen sind UNZULAESSIG (Art. 6 (3)).", + "Mehrwertdienste brauchen separate ausdrueckliche Einwilligung (Art. 6 (5)).", + "Daten nach Notruf-Bearbeitung SOFORT zu loeschen (Art. 6 (2))." + ] + } +} diff --git a/backend-compliance/compliance/services/check_replay.py b/backend-compliance/compliance/services/check_replay.py index c34c7924..2447c558 100644 --- a/backend-compliance/compliance/services/check_replay.py +++ b/backend-compliance/compliance/services/check_replay.py @@ -85,6 +85,28 @@ def replay_from_snapshot( section_sizes: dict[str, int] = {} parts: list[str] = [] + # P80 v2 — Quality-Checks aus dem aktuellen Code auf Snapshot-Daten + # anwenden. Wir replayen NICHT die MC-Pipeline (zu schwer ohne + # rag_document_checker re-run), aber alle nachgelagerten Findings- + # Generatoren (audit_quality, cookie_compliance_audit, vendor_normalizer, + # entropy, network-trace) bekommen Snapshot-Daten und liefern den + # aktuellen Stand. + try: + from compliance.services.audit_quality_checks import ( + run_all as run_aq, + ) + cookie_t = doc_texts.get("cookie") or doc_texts.get("dse") or "" + aq = run_aq(banner_result, cookie_t, cmp_vendors, doc_entries) + if aq: + from compliance.services.audit_quality_checks import ( + build_audit_quality_block_html, + ) + aq_html = build_audit_quality_block_html(aq) + parts.append(aq_html) + section_sizes["audit_quality_v2"] = len(aq_html) + except Exception as e: + logger.warning("Replay v2: audit_quality failed: %s", e) + # P82: GF-1-Pager zuerst (5-Bullet-Summary) try: from compliance.services.gf_one_pager import build_gf_one_pager_html diff --git a/backend-compliance/compliance/services/consent_diff_for_user.py b/backend-compliance/compliance/services/consent_diff_for_user.py new file mode 100644 index 00000000..f9578289 --- /dev/null +++ b/backend-compliance/compliance/services/consent_diff_for_user.py @@ -0,0 +1,125 @@ +""" +P54 — Diff-Banner fuer End-User (USP-Feature). + +USP-Idee: bei wiederkehrenden Besuchern zeigt das Banner NICHT die +Standard-Frage, sondern eine Diff-Mitteilung: + "Seit deiner letzten Zustimmung haben wir hinzugefuegt: + * Microsoft Bing (Werbung) + * TikTok Pixel (Marketing) + Bitte erneut zustimmen oder anpassen." + +Backend-Seite (hier): liefert pro Snapshot eine 'diff_for_user'-Struktur +die zum Embedden in eigenen Banner / Hinweistext genutzt werden kann. +Frontend-Banner-Lib (separate consent-sdk) konsumiert das. + +Vergleicht Vendor-Listen zwischen aktuellem Snapshot und dem letzten +Snapshot mit gleicher site_domain. +""" + +from __future__ import annotations + +import logging +from typing import Iterable + +from sqlalchemy import text as sa_text +from sqlalchemy.orm import Session + +logger = logging.getLogger(__name__) + + +def _norm_vendor_set(vendors: Iterable) -> set[str]: + out: set[str] = set() + for v in (vendors or []): + if isinstance(v, dict): + n = (v.get("name") or "").strip() + elif isinstance(v, str): + n = v.strip() + else: + continue + if n: + out.add(n) + return out + + +def compute_user_facing_diff( + db: Session, + site_domain: str, + current_check_id: str, + current_cmp_vendors: list, +) -> dict | None: + """Vergleicht aktuelle vs letzte cmp_vendors-Liste fuer die gleiche + site_domain. Liefert {prev_at, added_vendors, removed_vendors, + new_high_risk_categories} oder None wenn kein vorheriger Lauf.""" + if not site_domain: + return None + try: + row = db.execute(sa_text( + """ + SELECT cmp_vendors, created_at + FROM compliance.compliance_check_snapshots + WHERE site_domain = :dom AND check_id != :ex + ORDER BY created_at DESC LIMIT 1 + """ + ), {"dom": site_domain, "ex": current_check_id}).fetchone() + except Exception as e: + logger.warning("diff lookup failed: %s", e) + return None + if not row: + return None + + prev_vendors = row[0] or [] + prev_at = row[1] + curr_set = _norm_vendor_set(current_cmp_vendors) + prev_set = _norm_vendor_set(prev_vendors) + + added = sorted(curr_set - prev_set) + removed = sorted(prev_set - curr_set) + if not added and not removed: + return None + + # High-risk Kategorien aus added Vendors: Marketing / Tracking + new_marketing: list[str] = [] + for v in current_cmp_vendors: + if not isinstance(v, dict): + continue + n = (v.get("name") or "").strip() + cat = (v.get("category") or "").lower() + if n in added and cat in ("marketing", "tracking", "advertising"): + new_marketing.append(n) + + return { + "prev_at": prev_at.isoformat() if prev_at else None, + "added_vendors": added, + "removed_vendors": removed, + "new_marketing_vendors": new_marketing, + "requires_reconsent": bool(new_marketing), + } + + +def build_diff_banner_snippet(diff: dict) -> str: + """Liefert HTML-Snippet das der Site-Betreiber in seinen eigenen + Cookie-Banner einbauen kann (z.B. via consent-sdk).""" + if not diff or not diff.get("added_vendors"): + return "" + added = diff.get("added_vendors", []) + n_marketing = len(diff.get("new_marketing_vendors") or []) + items = "".join(f"
' + f'{n_marketing} neue{"r" if n_marketing == 1 else ""} ' + f'Marketing-Anbieter seit Ihrer letzten Zustimmung — ' + 'bitte erneut bestaetigen.' + '
' + ) + return ( + '' + ) diff --git a/backend-compliance/compliance/services/industry_library.py b/backend-compliance/compliance/services/industry_library.py new file mode 100644 index 00000000..c3ea97a8 --- /dev/null +++ b/backend-compliance/compliance/services/industry_library.py @@ -0,0 +1,222 @@ +""" +P6 + P53 + P55 — OEM-Cross-Industry-Library mit Autonomes Profiling. + +Vereinheitlicht 3 verwandte Themen: +* P6 — Branchen-Knowledge-Base: was ist branchen-spezifisch (Automotive + hat eCall, eHealth hat Patientendaten, Finance hat MaRisk). +* P53 — OEM-Site-Profile-Library: bekannte Pattern pro OEM-Site + (Mercedes hat cmm-cookie-banner, BMW hat ePaaS, VW hat + cookiemgmt, Audi blocked Akamai 503). +* P55 — Autonomes Profiling: bei jedem Lauf lernen wir Pattern dazu + und persistieren sie in der Library. + +Backend-Service: Lookup-API + Auto-Lern-Hook bei jedem Snapshot-Save. +""" + +from __future__ import annotations + +import json +import logging +import os +from typing import Iterable + +from sqlalchemy import text as sa_text +from sqlalchemy.orm import Session + +logger = logging.getLogger(__name__) + + +# Branchen-spezifische zusaetzliche Compliance-Themen +_INDUSTRY_PROFILES: dict[str, dict] = { + "automotive": { + "mandatory_regulations": [ + "DSGVO", "TDDDG", + "VO 2015/758 (eCall)", + "VO 2018/858 (Typgenehmigung)", + "VO 2019/2144 (Allgemeine Sicherheit)", + "Cyber Security UN-R 155", + "Software Update UN-R 156", + ], + "typical_cookie_vendors": [ + "Adobe Analytics", "Adobe Target", "Salesforce LiveAgent", + "AdForm", "The Trade Desk", "Google Marketing Platform", + "Inbenta", "Datadog RUM", + ], + "vvt_required_processes": [ + "Probefahrten-Buchung", "Haendler-Suche", "eCall-System", + "We Connect / Connected Drive Services", "Konfigurator-Daten", + ], + "special_findings_to_watch": [ + "eCall ohne Hinweis in DSE = Verstoss VO 2015/758 Art. 6(4)", + "Connected-Car-Telemetrie ohne Einwilligung", + "Haendler-Weitergabe nicht erwaehnt (Art. 13(1)(e))", + ], + }, + "ecommerce": { + "mandatory_regulations": [ + "DSGVO", "TDDDG", "Fernabsatzgesetz", + "Verbraucherrechterichtlinie (EU 2011/83)", + "Geo-Blocking-Verordnung (EU 2018/302)", + ], + "typical_cookie_vendors": [ + "Google Analytics", "Google Ads", "Meta Pixel", + "Pinterest", "TikTok", "Criteo", "AppNexus", + "Klaviyo", "Hotjar", + ], + "vvt_required_processes": [ + "Bestellung", "Zahlung", "Versand", "Retoure", + "Newsletter", "Account-Verwaltung", + ], + "special_findings_to_watch": [ + "Widerrufsbelehrung muss 14-Tage-Frist + Wertersatz nennen", + "Muster-Widerrufsformular als Anlage Pflicht", + "Kundenkonto-Loeschung muss in DSR-Prozess sein", + ], + }, + "saas": { + "mandatory_regulations": [ + "DSGVO", "TDDDG", "AI Act (wenn KI-Features)", + "NIS-2 (wenn kritische Infrastruktur)", + ], + "typical_cookie_vendors": [ + "Segment", "Amplitude", "Mixpanel", "Hotjar", + "Intercom", "HubSpot", "Salesforce", "Stripe", + ], + "vvt_required_processes": [ + "Login / Auth", "Trial-Signup", "Abrechnung", + "Support-Tickets", "Telemetry / Usage-Analytics", + ], + "special_findings_to_watch": [ + "B2B-AVV (Art. 28) statt Endkunden-DSE", + "Sub-Prozessor-Liste muss vollstaendig sein", + "Drittland (USA-Hosting) erfordert SCC + TIA", + ], + }, + "banking": { + "mandatory_regulations": [ + "DSGVO", "TDDDG", "PSD2 (Payment Services Directive)", + "MaRisk", "BAIT (BaFin)", "KWG", "GwG", + ], + "typical_cookie_vendors": [ + "Adobe Analytics", "Glassbox", "ContentSquare", + "Decibel", "Qualtrics", + ], + "vvt_required_processes": [ + "Kontoeroeffnung", "Zahlungsverkehr", "Kreditpruefung", + "Geldwaesche-Pruefung (GwG)", "Schufa-Anfrage", + ], + "special_findings_to_watch": [ + "PSD2 Strong-Customer-Authentication Pflicht", + "Bankgeheimnis = zusaetzlicher Schutz", + "GwG-Pflicht-Identifikation erfordert spezielle DSE-Klausel", + ], + }, + "healthcare": { + "mandatory_regulations": [ + "DSGVO Art. 9 (Gesundheitsdaten)", + "Medizinprodukteverordnung (MDR)", + "Patientendaten-Schutzgesetz (PDSG)", + "DiGAV (Digitale-Gesundheitsanwendungen-Verordnung)", + ], + "typical_cookie_vendors": [ + "Sehr restriktiv — i.d.R. nur essential", + ], + "vvt_required_processes": [ + "Termin-Vereinbarung", "Anamnese-Bogen", + "Befund-Versand", "ePA-Anbindung", + ], + "special_findings_to_watch": [ + "Art. 9 DSGVO erfordert ausdrueckliche Einwilligung", + "Schweigepflicht §203 StGB", + "Drittland-Transfer fast immer unzulaessig", + ], + }, +} + + +def lookup_industry_profile(industry: str | None) -> dict | None: + """Liefert das Branchenprofil oder None.""" + if not industry: + return None + return _INDUSTRY_PROFILES.get(industry.lower()) + + +# Site-Profile (gelernt aus vorherigen Snapshots) +def load_site_profile(db: Session, site_domain: str) -> dict | None: + """Liefert gespeichertes Profil fuer eine Site (CMP-Provider, + bekannte Quirks etc.) oder None.""" + if not site_domain: + return None + try: + row = db.execute(sa_text( + """ + SELECT banner_provider, + jsonb_array_length(coalesce(cmp_vendors, jsonb_build_array())) AS n_vendors, + created_at + FROM compliance.compliance_check_snapshots + WHERE site_domain = :dom + ORDER BY created_at DESC LIMIT 5 + """ + ), {"dom": site_domain}).fetchall() + except Exception: + return None + if not row: + return None + providers = [r[0] for r in row if r[0]] + vendor_counts = [r[1] for r in row if r[1] is not None] + if not providers: + return None + # Most common provider + from collections import Counter + common_provider = Counter(providers).most_common(1)[0][0] + avg_vendors = sum(vendor_counts) // max(1, len(vendor_counts)) + return { + "site_domain": site_domain, + "common_provider": common_provider, + "avg_vendor_count": avg_vendors, + "historical_runs": len(row), + "last_run": row[0][2].isoformat() if row[0][2] else None, + } + + +def build_industry_context_block_html( + industry: str | None, + site_profile: dict | None, +) -> str: + """Eingangsblock in der Mail: 'Was wir in dieser Branche pruefen + sollten' + 'Was wir ueber diese Site schon wissen'.""" + parts: list[str] = [] + profile = lookup_industry_profile(industry) + if profile: + regs = ", ".join(profile.get("mandatory_regulations", [])[:6]) + watches = profile.get("special_findings_to_watch", [])[:3] + watch_html = "".join( + f'' + f'Geltende Spezial-Regulierungen: {regs}' + f'
' + f'