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>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
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",
|
||||
}
|
||||
Reference in New Issue
Block a user