feat(cookie): missing_retention — Vendor ohne Speicherdauer/Löschfrist

Vendor-Ebenen-Finding: greift, wenn ein Vendor eine Verarbeitung deklariert
(Kategorie/Zweck), aber KEINE Cookies gelistet sind UND keine persistence
angegeben ist (z.B. Nayoki GmbH — 'necessary' Auftragsverarbeiter ohne
Löschfrist). Die Pro-Cookie-Schleife sah solche Vendors nie (0 Cookies →
0 Findings). Remediation = Ticket-Text 'bitte Löschfrist festlegen'.
Art. 5 Abs. 1 lit. e + Art. 13 Abs. 2 lit. a → Control AUTH-2051-A03.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-11 10:02:59 +02:00
parent 32ba8d16b1
commit 7fa9968ce1
3 changed files with 53 additions and 0 deletions
@@ -46,6 +46,7 @@ const TYPE_LABEL: Record<string, string> = {
missing_purpose: 'Zweck fehlt',
excessive_lifetime: 'Speicherdauer zu lang',
vague_duration: 'Speicherdauer nicht konkret',
missing_retention: 'Keine Speicherdauer/Löschfrist',
third_country: 'Drittland-Transfer',
eu_alternative: 'EU-Alternative verfügbar',
storage_transparency: 'Speichertyp nicht transparent',
@@ -8,6 +8,9 @@ Befund-Typen:
tracker_as_necessary — als notwendig deklariert, laut Library kein techn. Zweck
missing_purpose — kein Zweck deklariert, Library kennt ihn
excessive_lifetime — deklarierte Speicherdauer >> typische (Art. 5(1)(e))
vague_duration — Speicherdauer nicht konkret (Art. 5(1)(e)+13) [je Cookie]
missing_retention — Verarbeitung deklariert, aber keine Speicherdauer/
Löschfrist + keine Cookies gelistet [je Vendor]
third_country — Drittland-Transfer (Schrems II, Art. 44 ff.) [je Vendor]
eu_alternative — EU-Ersatz verfügbar (kommerziell) [je Vendor]
"""
@@ -28,6 +31,7 @@ _TRACKER_CATS = {"marketing", "statistics", "social_media", "targeting"}
# den Controls gepflegt ist). Kette: Regulation → Article → Control → Finding.
_CONTROL_MAP = {
"vague_duration": {"control_id": "AUTH-2051-A03", "regulation": "DSGVO", "article": "Art. 5 Abs. 1 lit. e + Art. 13"},
"missing_retention": {"control_id": "AUTH-2051-A03", "regulation": "DSGVO", "article": "Art. 5 Abs. 1 lit. e + Art. 13 Abs. 2 lit. a"},
"excessive_lifetime": {"control_id": "AUTH-2051-A02", "regulation": "DSGVO", "article": "Art. 5 Abs. 1 lit. e"},
"tracker_as_necessary": {"control_id": "DATA-2851-A05", "regulation": "TDDDG", "article": "§ 25 Abs. 1"},
"missing_purpose": {"control_id": "AUTH-2053-A05", "regulation": "DSGVO", "article": "Art. 13"},
@@ -232,6 +236,27 @@ def analyze_cookies(vendors: list[dict], big_lib: dict | None = None) -> dict:
),
})
# Vendor-Ebene: Verarbeitung deklariert, aber KEINE Speicherdauer. Greift,
# wenn keine Cookies gelistet UND keine persistence (z.B. Nayoki GmbH:
# 'necessary' Auftragsverarbeiter ohne Löschfrist) — Art. 5(1)(e)+13(2)(a).
v_persist = (v.get("persistence") or "").strip()
v_purpose = (v.get("purpose") or "").strip()
if not (v.get("cookies") or []) and not v_persist and (v_purpose or vcat):
findings.append({
"vendor": vname, "cookie": "(keine Cookies gelistet)",
"type": "missing_retention", "severity": "MEDIUM",
"declared": f"{vcat_label} / keine Speicherdauer",
"library_purpose": v_purpose,
"remediation": (
f"Für '{vname}' ist eine Datenverarbeitung deklariert "
f"(Kategorie '{vcat_label}'), aber keine Speicherdauer/Löschfrist "
f"angegeben und keine Cookies gelistet. Art. 5 Abs. 1 lit. e + "
f"Art. 13 Abs. 2 lit. a DSGVO verlangen eine konkrete "
f"Speicherdauer bzw. Löschfrist — bitte für '{vname}' eine "
f"Löschfrist festlegen und in der Cookie-Richtlinie ausweisen."
),
})
# A: jeden Befund an seinen Control + Rechtsgrundlage haengen (auditfest).
for f in findings:
f["control"] = _CONTROL_MAP.get(f["type"], {})
@@ -112,6 +112,33 @@ def test_vague_duration_flagged_concrete_ok():
assert "Art. 5" in vd[0]["remediation"]
def test_missing_retention_vendor_without_cookies_or_duration():
# User-Beispiel Nayoki GmbH: als 'necessary' deklarierter Auftragsverarbeiter,
# KEINE Cookies gelistet, KEINE persistence → Speicherdauer/Löschfrist-Finding.
out = analyze_cookies([{
"name": "Nayoki GmbH — BMW Sport & Kultur Social Wall",
"category": "necessary", "persistence": "",
"purpose": "Verwaltung der Social Wall.",
"cookies": [],
}])
mr = [f for f in out["findings"] if f["type"] == "missing_retention"]
assert len(mr) == 1
assert "Nayoki" in mr[0]["vendor"]
assert "Löschfrist" in mr[0]["remediation"]
assert mr[0]["severity"] == "MEDIUM"
assert mr[0]["control"]["control_id"] == "AUTH-2051-A03"
assert "Art. 13 Abs. 2" in mr[0]["control"]["article"]
def test_no_missing_retention_when_vendor_has_cookies():
# Vendor MIT Cookies (konkrete Dauer) → kein missing_retention.
out = analyze_cookies([{
"name": "X", "category": "necessary", "persistence": "",
"cookies": [{"name": "sess", "purpose": "x", "expiry": "Session"}],
}])
assert not [f for f in out["findings"] if f["type"] == "missing_retention"]
def test_big_library_covers_cookie_not_in_rich_db():
# Cookie nicht in der 35er rich-DB, aber in der grossen 2287er (big_lib).
big = {"bmw_track_de": {