Files
breakpilot-compliance/backend-compliance/compliance/services/vendor_assessment_cross_check.py
T
Benjamin Admin 0326d5baab feat(vendor-assessment): AVV/SCC/TOM/Sub-Processor checklists + assessment service
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>
2026-05-12 23:14:54 +02:00

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",
}