""" Vendor Assessment Cross-Check — checks consistency BETWEEN documents. Analogous to banner_cookie_cross_check.py but for vendor contracts: - AVV references SCC → is SCC document present? - AVV references TOM annex → is TOM document uploaded? - AVV mentions sub-processors → does sub-processor list match? - AVV mentions third-country transfer → is transfer mechanism documented? Returns CheckItem-compatible dicts (same format as cross_check_vendors_vs_dsi). """ import logging import re logger = logging.getLogger(__name__) def cross_check_documents( doc_texts: dict[str, str], vendor_name: str, ) -> list[dict]: """Cross-check consistency between uploaded vendor documents. Args: doc_texts: Mapping of doc_type → extracted text vendor_name: The vendor being assessed Returns: List of CheckItem-compatible finding dicts. """ findings: list[dict] = [] avv_text = _get_text(doc_texts, ["avv", "auftragsverarbeitung", "dpa"]) scc_text = _get_text(doc_texts, ["scc", "standardvertragsklauseln"]) tom_text = _get_text(doc_texts, ["tom_annex", "tom_anlage", "tom"]) sub_text = _get_text(doc_texts, ["sub_processor_list", "sub_processor", "unterauftragnehmer"]) if not avv_text: # No AVV → biggest gap findings.append(_finding( "cross-no-avv", f"Kein AVV fuer '{vendor_name}' vorhanden", "CRITICAL", f"Ohne Auftragsverarbeitungsvertrag (AVV) nach Art. 28 DSGVO ist " f"jede Verarbeitung personenbezogener Daten durch '{vendor_name}' " f"rechtswidrig. Dies ist der schwerwiegendste Mangel.", )) return findings avv_lower = avv_text.lower() # ── AVV references TOM but no TOM document ────────────────────── tom_referenced = bool(re.search( r"(?:tom|technische[\s\S]{0,10}organisatorische|art(?:ikel)?\s*\.?\s*32|" r"anlage[\s\S]{0,20}(?:tom|sicherheit|massnahmen))", avv_lower, )) if tom_referenced and not tom_text: findings.append(_finding( "cross-tom-missing", "AVV verweist auf TOM-Anlage — Dokument fehlt", "HIGH", "Der AVV verweist auf technische und organisatorische Massnahmen " "(Art. 32 DSGVO), aber es wurde keine TOM-Anlage hochgeladen. " "Ohne TOM-Nachweis kann die Angemessenheit der Sicherheitsmassnahmen " "nicht beurteilt werden.", )) # ── AVV mentions third-country but no SCC ─────────────────────── third_country = bool(re.search( r"(?:drittland|third\s+country|usa|united\s+states|nicht[\s-]?(?:eu|ewr)|" r"ausserhalb\s+(?:des\s+)?(?:eu|ewr|europaeischen))", avv_lower, )) scc_referenced = bool(re.search( r"(?:standardvertragsklausel|scc|standard\s+contractual|2021/914)", avv_lower, )) if third_country and not scc_text and not scc_referenced: findings.append(_finding( "cross-scc-missing", "AVV erwaehnt Drittlandtransfer — keine SCC vorhanden", "CRITICAL", "Der AVV erwaehnt Datenverarbeitung in einem Drittland, " "aber es wurden keine Standardvertragsklauseln (SCC) hochgeladen " "und der AVV verweist auch nicht auf ein Data Privacy Framework. " "Ohne Transfermechanismus ist die Uebermittlung rechtswidrig " "(Art. 44-49 DSGVO, EuGH Schrems II C-311/18).", )) # ── AVV mentions sub-processors but no list ───────────────────── sub_mentioned = bool(re.search( r"(?:unterauftragnehmer|sub[\s-]?processor|unterauftragsverarbeiter|" r"weitere[rn]?\s+auftragsverarbeiter)", avv_lower, )) if sub_mentioned and not sub_text: findings.append(_finding( "cross-sub-list-missing", "AVV erwaehnt Unterauftragnehmer — Liste fehlt", "HIGH", "Der AVV regelt den Einsatz von Unterauftragsverarbeitern, " "aber es wurde keine Sub-Processor-Liste hochgeladen. " "Art. 28(2) DSGVO verlangt, dass der Verantwortliche " "ueber alle Unterauftragnehmer informiert ist.", )) # ── SCC present but no TIA referenced ─────────────────────────── if scc_text: scc_lower = scc_text.lower() tia_present = bool(re.search( r"(?:transfer\s+impact|uebermittlungs[\s-]?risiko|" r"klausel\s+14|clause\s+14|risikobewertung[\s\S]{0,50}drittland)", scc_lower, )) if not tia_present: findings.append(_finding( "cross-scc-no-tia", "SCC vorhanden aber kein Transfer Impact Assessment (TIA)", "HIGH", "Standardvertragsklauseln wurden hochgeladen, aber ein " "Transfer Impact Assessment (TIA) fehlt oder wird nicht " "referenziert. Klausel 14 der SCC 2021 verlangt eine " "Bewertung der Rechtslage im Zielland (EuGH Schrems II).", )) # ── TOM present but incomplete ────────────────────────────────── if tom_text: tom_lower = tom_text.lower() encryption_mentioned = bool(re.search( r"(?:verschl(?:ue|ü)sselung|encryption|aes|tls|ssl)", tom_lower, )) if not encryption_mentioned: findings.append(_finding( "cross-tom-no-encryption", "TOM-Anlage erwaehnt keine Verschluesselung", "HIGH", "Die TOM-Anlage beschreibt keine Verschluesselungsmassnahmen. " "Art. 32(1)(a) DSGVO nennt Verschluesselung explizit als " "Sicherheitsmassnahme. Fehlende Verschluesselung ist einer " "der haeufigsten Beanstandungspunkte bei Audits.", )) logger.info("Cross-check: %d findings for vendor '%s'", len(findings), vendor_name) return findings def _get_text(doc_texts: dict[str, str], keys: list[str]) -> str: """Get text for the first matching doc_type key.""" for k in keys: if k in doc_texts: return doc_texts[k] return "" def _finding(id: str, label: str, severity: str, hint: str) -> dict: """Create a CheckItem-compatible finding dict.""" return { "id": id, "label": label, "passed": False, "severity": severity, "level": 1, "parent": None, "skipped": False, "matched_text": "", "hint": hint, "source": "contract_cross_check", }