From 3853a0838a9f7cacee9418787a69ed9c05c2ed89 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 7 May 2026 10:49:32 +0200 Subject: [PATCH] feat: Art. 26 Joint Controller + DSFA checklists for Social Media sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New checklists: - JOINT_CONTROLLER_CHECKLIST (Art. 26 DSGVO, 7 checks): Joint parties, arrangement, contact point, processing split, data categories, third-country transfer (USA), rights - DSFA_CHECKLIST (Art. 35 DSGVO, 5 checks): Description, necessity, risk assessment, measures, DSB involvement Section detection: 'Datenschutzerklaerung fuer Social Media' → social_media, 'Datenschutzfolgeabschaetzung/Risikoanalyse' → dsfa classify_document_type: DSFA and social_media detected before generic DSE Frontend: DOC_TYPES dropdown + ChecklistView labels updated Co-Authored-By: Claude Opus 4.6 (1M context) --- .../sdk/agent/_components/ChecklistView.tsx | 1 + .../app/sdk/agent/_components/DocCheckTab.tsx | 2 + .../compliance/api/agent_doc_check_routes.py | 5 +- .../services/dsi_document_checker.py | 63 +++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/admin-compliance/app/sdk/agent/_components/ChecklistView.tsx b/admin-compliance/app/sdk/agent/_components/ChecklistView.tsx index 2279a29..e91b7b7 100644 --- a/admin-compliance/app/sdk/agent/_components/ChecklistView.tsx +++ b/admin-compliance/app/sdk/agent/_components/ChecklistView.tsx @@ -24,6 +24,7 @@ interface DocResult { const DOC_TYPE_LABELS: Record = { dse: 'DSI', agb: 'AGB', impressum: 'Impressum', cookie: 'Cookie', widerruf: 'Widerruf', other: 'Sonstiges', + social_media: 'Social Media', dsfa: 'DSFA', joint_controller: 'Art. 26', } export function ChecklistView({ results }: { results: DocResult[] }) { diff --git a/admin-compliance/app/sdk/agent/_components/DocCheckTab.tsx b/admin-compliance/app/sdk/agent/_components/DocCheckTab.tsx index f4e7897..8ba9c88 100644 --- a/admin-compliance/app/sdk/agent/_components/DocCheckTab.tsx +++ b/admin-compliance/app/sdk/agent/_components/DocCheckTab.tsx @@ -12,6 +12,8 @@ interface DocEntry { const DOC_TYPES = [ { id: 'dse', label: 'DSI (Datenschutzinformation)' }, + { id: 'social_media', label: 'DSE Social Media (Art. 26)' }, + { id: 'dsfa', label: 'DSFA (Art. 35)' }, { id: 'agb', label: 'AGB / Nutzungsbedingungen' }, { id: 'impressum', label: 'Impressum' }, { id: 'cookie', label: 'Cookie-Richtlinie' }, diff --git a/backend-compliance/compliance/api/agent_doc_check_routes.py b/backend-compliance/compliance/api/agent_doc_check_routes.py index 9e1815b..47cccee 100644 --- a/backend-compliance/compliance/api/agent_doc_check_routes.py +++ b/backend-compliance/compliance/api/agent_doc_check_routes.py @@ -263,9 +263,8 @@ SECTION_TYPE_MAP = [ (r"widerrufsrecht|widerrufsbelehrung", "widerruf"), # Widerruf → §355 BGB (r"^impressum$", "impressum"), # Impressum → §5 TMG (r"^(?:agb|allgemeine geschäftsbedingungen|nutzungsbedingungen)$", "agb"), - # NOTE: Social Media, DSFA, Datensicherheit, Betroffenenrechte are NOT - # separate documents — they are sections within the parent DSI. - # DSFA needs its own checklist (RAG-based) — Phase 2. + (r"datenschutzerkl.*social|datenschutz.*social\s*media", "social_media"), + (r"datenschutzfolge|dsfa|risikoanalyse.*social", "dsfa"), ] diff --git a/backend-compliance/compliance/services/dsi_document_checker.py b/backend-compliance/compliance/services/dsi_document_checker.py index 524cd6f..c261f23 100644 --- a/backend-compliance/compliance/services/dsi_document_checker.py +++ b/backend-compliance/compliance/services/dsi_document_checker.py @@ -196,6 +196,58 @@ COOKIE_CHECKLIST = [ "patterns": [r"(?:widerspruch|opt.?out|ablehnen|deaktivieren).*cookie", r"cookie.*(?:ablehnen|deaktivieren|loeschen)"]}, ] +# Art. 26 DSGVO Joint Controller (Social Media DSE) +JOINT_CONTROLLER_CHECKLIST = [ + {"id": "joint_parties", "label": "Gemeinsam Verantwortliche benannt (Art. 26(1))", + "patterns": [r"gemeinsam.*verantwortlich", r"joint.*controller", r"gemeinsame\s+verantwortlichkeit", + r"art\.\s*26", r"mitverantwortlich"]}, + {"id": "arrangement", "label": "Vereinbarung nach Art. 26 DSGVO", + "patterns": [r"vereinbarung.*art\.\s*26", r"art\.\s*26.*vereinbarung", + r"page\s*controller", r"fanpage", r"insights", + r"gemeinsame.*verantwortung.*(?:vertrag|vereinbarung)"]}, + {"id": "contact_point", "label": "Anlaufstelle fuer Betroffene (Art. 26(1) S.3)", + "patterns": [r"anlaufstelle", r"kontaktstelle", r"ansprechpartner.*betroffene", + r"rechte.*(?:gegenueber|gegenüber)\s+(?:uns|beiden)", + r"rechte.*(?:sowohl|grundsaetzlich|grundsätzlich).*(?:uns|als auch)"]}, + {"id": "processing_split", "label": "Verarbeitungsaufteilung (wer macht was)", + "patterns": [r"(?:wir|betreiber).*(?:verarbeiten|erheben|nutzen).*(?:daten|informationen)", + r"(?:facebook|meta|google|youtube|instagram|linkedin|twitter|x\.com).*(?:verarbeit|erhebt|nutzt|speichert)", + r"bei\s+besuch\s+(?:unserer|der)\s+(?:seite|fanpage|profil)"]}, + {"id": "social_data_types", "label": "Kategorien verarbeiteter Daten", + "patterns": [r"(?:nutzungsstatistik|insight|reichweite|interaktion|klick|aufruf)", + r"(?:ip.?adresse|standort|browser|geraet|alter|geschlecht).*(?:social|profil|seite|fanpage)", + r"(?:personenbezogen|daten).*(?:social|netzwerk|plattform)"]}, + {"id": "third_country", "label": "Drittlandtransfer (USA bei Social Media)", + "patterns": [r"(?:usa|vereinigte\s+staaten|drittland|drittstaaten).*(?:social|facebook|meta|google)", + r"(?:social|facebook|meta|google).*(?:usa|drittland|drittstaaten)", + r"privacy\s+shield|data\s+privacy\s+framework|angemessenheitsbeschluss", + r"standardvertragsklausel|standard.*contractual"]}, + {"id": "rights", "label": "Betroffenenrechte (Art. 15-21)", + "patterns": [r"recht\s+auf\s+auskunft", r"recht\s+auf\s+l(?:oe|ö)schung", + r"art\.\s*1[5-9]", r"betroffenenrecht", + r"ihre\s+rechte", r"rechte.*betroffene"]}, +] + +# DSFA minimal checklist (Art. 35 DSGVO) — basic structure checks +DSFA_CHECKLIST = [ + {"id": "description", "label": "Beschreibung der Verarbeitungsvorgaenge (Art. 35(7)(a))", + "patterns": [r"beschreibung.*verarbeitung", r"verarbeitungsvorg(?:ae|ä)ng", + r"systematische\s+beschreibung", r"gegenstand.*verarbeitung"]}, + {"id": "necessity", "label": "Notwendigkeit und Verhaeltnismaessigkeit (Art. 35(7)(b))", + "patterns": [r"notwendigkeit", r"verh(?:ae|ä)ltnism(?:ae|ä)ssigkeit", + r"erforderlichkeit", r"zweckbindung"]}, + {"id": "risks", "label": "Risikobewertung fuer Betroffene (Art. 35(7)(c))", + "patterns": [r"risiko.*(?:bewertung|analyse|einsch(?:ae|ä)tzung)", + r"risiken.*(?:rechte|freiheit)", r"eintrittswahrscheinlichkeit", + r"schwere.*(?:risiko|auswirkung)"]}, + {"id": "measures", "label": "Abhilfemassnahmen (Art. 35(7)(d))", + "patterns": [r"abhilfe", r"massnahmen.*risiko", r"schutzma(?:ss|ß)nahm", + r"(?:technisch|organisatorisch).*massnahm", r"tom"]}, + {"id": "stakeholders", "label": "Einbeziehung des DSB (Art. 35(2))", + "patterns": [r"datenschutzbeauftragt.*(?:einbez|konsult|beteilig|rat)", + r"dsb.*(?:konsult|einbez|rat)", r"stellungnahme.*dsb"]}, +] + def check_document_completeness( text: str, @@ -255,6 +307,12 @@ def check_document_completeness( elif doc_type in ("cookie",): checklist = COOKIE_CHECKLIST label = "§25 TDDDG" + elif doc_type in ("social_media", "joint_controller"): + checklist = JOINT_CONTROLLER_CHECKLIST + label = "Art. 26 DSGVO" + elif doc_type in ("dsfa",): + checklist = DSFA_CHECKLIST + label = "Art. 35 DSGVO" else: checklist = ART13_CHECKLIST # Default: check as DSE label = "Art. 13 DSGVO" @@ -323,6 +381,11 @@ def classify_document_type(title: str, url: str) -> str: """Classify a document by its title/URL into a legal document type.""" combined = f"{title} {url}".lower() + if any(kw in combined for kw in ["datenschutzfolge", "dsfa", "risikoanalyse für nutzung"]): + return "dsfa" + if any(kw in combined for kw in ["social media", "facebook", "instagram", "linkedin", "fanpage"]): + if any(kw in combined for kw in ["datenschutzerkl", "datenschutz für", "datenschutzinformation"]): + return "social_media" if any(kw in combined for kw in ["datenschutz", "privacy", "dsgvo", "data protection", "données"]): return "dse" if any(kw in combined for kw in ["widerruf", "withdrawal", "rétractation", "desistimiento"]):