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:
@@ -46,6 +46,7 @@ const TYPE_LABEL: Record<string, string> = {
|
|||||||
missing_purpose: 'Zweck fehlt',
|
missing_purpose: 'Zweck fehlt',
|
||||||
excessive_lifetime: 'Speicherdauer zu lang',
|
excessive_lifetime: 'Speicherdauer zu lang',
|
||||||
vague_duration: 'Speicherdauer nicht konkret',
|
vague_duration: 'Speicherdauer nicht konkret',
|
||||||
|
missing_retention: 'Keine Speicherdauer/Löschfrist',
|
||||||
third_country: 'Drittland-Transfer',
|
third_country: 'Drittland-Transfer',
|
||||||
eu_alternative: 'EU-Alternative verfügbar',
|
eu_alternative: 'EU-Alternative verfügbar',
|
||||||
storage_transparency: 'Speichertyp nicht transparent',
|
storage_transparency: 'Speichertyp nicht transparent',
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ Befund-Typen:
|
|||||||
tracker_as_necessary — als notwendig deklariert, laut Library kein techn. Zweck
|
tracker_as_necessary — als notwendig deklariert, laut Library kein techn. Zweck
|
||||||
missing_purpose — kein Zweck deklariert, Library kennt ihn
|
missing_purpose — kein Zweck deklariert, Library kennt ihn
|
||||||
excessive_lifetime — deklarierte Speicherdauer >> typische (Art. 5(1)(e))
|
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]
|
third_country — Drittland-Transfer (Schrems II, Art. 44 ff.) [je Vendor]
|
||||||
eu_alternative — EU-Ersatz verfügbar (kommerziell) [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.
|
# den Controls gepflegt ist). Kette: Regulation → Article → Control → Finding.
|
||||||
_CONTROL_MAP = {
|
_CONTROL_MAP = {
|
||||||
"vague_duration": {"control_id": "AUTH-2051-A03", "regulation": "DSGVO", "article": "Art. 5 Abs. 1 lit. e + Art. 13"},
|
"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"},
|
"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"},
|
"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"},
|
"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).
|
# A: jeden Befund an seinen Control + Rechtsgrundlage haengen (auditfest).
|
||||||
for f in findings:
|
for f in findings:
|
||||||
f["control"] = _CONTROL_MAP.get(f["type"], {})
|
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"]
|
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():
|
def test_big_library_covers_cookie_not_in_rich_db():
|
||||||
# Cookie nicht in der 35er rich-DB, aber in der grossen 2287er (big_lib).
|
# Cookie nicht in der 35er rich-DB, aber in der grossen 2287er (big_lib).
|
||||||
big = {"bmw_track_de": {
|
big = {"bmw_track_de": {
|
||||||
|
|||||||
Reference in New Issue
Block a user