"""B10 — Drittland-Transfer-Mechanismus-Konsistenz pro Vendor. DSGVO Art. 44 ff. verlangt für Drittland-Transfers EINEN klaren Mechanismus: Angemessenheitsbeschluss / EU-US DPF / SCCs / BCRs / ausdrückliche Einwilligung. Wenn ein Vendor in cmp_vendors als Drittland-Verarbeiter erkannt wird, muss der DSE-Text einen Mechanismus pro Vendor (oder per Vendor-Kategorie) klar benennen. GT-Pattern Elli (TRANSFER-001): - Google/Meta → DPF in DSE genannt ✓ - Salesforce → SCCs ✓ - Webflow als US-Sitz erwähnt aber kein Mechanismus → MEDIUM Heuristik: 1. Aus cmp_vendors die Drittland-Vendors filtern (third_country=True). 2. Im DSE-Text suchen, ob pro Vendor ein Mechanismus erwähnt ist. 3. Wenn ein Drittland-Vendor keinen Mechanismus hat → MEDIUM. """ from __future__ import annotations import logging logger = logging.getLogger(__name__) _MECHANISM_KEYWORDS = ( ("DPF / Data Privacy Framework", ["data privacy framework", "dpf-", "eu-us dpf", "angemessenheitsbeschluss"]), ("Standardvertragsklauseln (SCCs)", ["standardvertragsklauseln", "scc-", "scc ", "standard contractual", "art. 46 abs. 2 lit. c"]), ("Binding Corporate Rules", ["binding corporate rules", "bcr-", "verbindliche unternehmensregeln"]), ("Ausdrückliche Einwilligung", ["ausdrückliche einwilligung nach art. 49", "explicit consent under art. 49"]), ) def _mechanism_for_vendor(vendor_name: str, dse_text: str) -> str | None: if not vendor_name or not dse_text: return None name_lc = vendor_name.lower() text_lc = dse_text.lower() # Find vendor mention in DSE; locate a ±400 char window for # mechanism keywords idx = text_lc.find(name_lc) if idx < 0: return None window = text_lc[max(0, idx - 400): idx + 400] for mech_label, kws in _MECHANISM_KEYWORDS: if any(k in window for k in kws): return mech_label return None def check_transfer_mechanism(state: dict) -> list[dict]: cmp_vendors = state.get("cmp_vendors") or [] doc_texts = state.get("doc_texts") or {} dse = doc_texts.get("dse") or "" if not cmp_vendors or not dse: return [] findings: list[dict] = [] for v in cmp_vendors: country = (v.get("country") or "").upper().strip() name = (v.get("name") or "").strip() if not name: continue # Skip EU/EEA if country in ("DE", "AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "IS", "LI", "NO", "CH"): continue # Either flagged as third_country OR country not in EU mech = _mechanism_for_vendor(name, dse) if mech is None: findings.append({ "check_id": "TRANSFER-MECH-001", "vendor": name, "country": country or "UNKNOWN", "severity": "MEDIUM", "severity_reason": "missing", "title": ( f"Drittland-Transfer-Mechanismus für {name} " f"({country or 'Drittland'}) fehlt in DSE" ), "norm": "DSGVO Art. 44 + Art. 46 / Art. 49", "action": ( f"Im DSE-Abschnitt zu {name} den Transfermechanismus " "angeben (DPF / SCCs / BCRs / Einwilligung) und ggf. " "Vertragsdokument referenzieren." ), }) if findings: logger.info("B10 transfer-mechanism: %d findings", len(findings)) return findings