0326d5baab
Phase 1-3 of the Vendor Contract Assessment:
Backend checklists (Doc-Check L1/L2 engine compatible):
- avv_checks.py: 28 checks (11 L1 + 17 L2) for Art. 28(3) DSGVO
- scc_checks.py: 7 checks for EU SCC 2021 (modules, annexes, TIA)
- tom_annex_checks.py: 12 checks for Art. 32 (8 control objectives)
- sub_processor_checks.py: 7 checks for sub-processor list completeness
Assessment service:
- POST /vendor-compliance/assessments — async contract analysis
- GET /vendor-compliance/assessments/{id} — poll status
- Cross-check engine: detects missing SCC when AVV mentions third-country,
missing TOM annex, missing sub-processor list
All checklists registered in runner.py CHECKLIST_MAP (27 doc_types total).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
172 lines
6.7 KiB
Python
172 lines
6.7 KiB
Python
"""
|
|
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",
|
|
}
|