From 7fa9968ce1c8533ea7d285b8c9feb8d2cf1df857 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 11 Jun 2026 10:02:59 +0200 Subject: [PATCH] =?UTF-8?q?feat(cookie):=20missing=5Fretention=20=E2=80=94?= =?UTF-8?q?=20Vendor=20ohne=20Speicherdauer/L=C3=B6schfrist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../agent/_components/CookieLibraryPanel.tsx | 1 + .../services/cookie_library_check.py | 25 +++++++++++++++++ .../tests/test_cookie_library_check.py | 27 +++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/admin-compliance/app/sdk/agent/_components/CookieLibraryPanel.tsx b/admin-compliance/app/sdk/agent/_components/CookieLibraryPanel.tsx index c939588c..bc824546 100644 --- a/admin-compliance/app/sdk/agent/_components/CookieLibraryPanel.tsx +++ b/admin-compliance/app/sdk/agent/_components/CookieLibraryPanel.tsx @@ -46,6 +46,7 @@ const TYPE_LABEL: Record = { 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', diff --git a/backend-compliance/compliance/services/cookie_library_check.py b/backend-compliance/compliance/services/cookie_library_check.py index 38566b86..2be9b715 100644 --- a/backend-compliance/compliance/services/cookie_library_check.py +++ b/backend-compliance/compliance/services/cookie_library_check.py @@ -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"], {}) diff --git a/backend-compliance/compliance/tests/test_cookie_library_check.py b/backend-compliance/compliance/tests/test_cookie_library_check.py index 2067154b..1b094973 100644 --- a/backend-compliance/compliance/tests/test_cookie_library_check.py +++ b/backend-compliance/compliance/tests/test_cookie_library_check.py @@ -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": {