diff --git a/admin-compliance/app/sdk/agent/_components/_document_types.ts b/admin-compliance/app/sdk/agent/_components/_document_types.ts new file mode 100644 index 00000000..6d4570a4 --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/_document_types.ts @@ -0,0 +1,21 @@ +/** + * DOCUMENT_TYPES — canonical compliance-doc taxonomy for the + * /sdk/agent ComplianceCheckTab form. + * + * Each entry maps to a doc_type that the backend Phase-A discovery / + * Phase-B per-doc-check pipeline recognises. + */ + +export const DOCUMENT_TYPES = [ + { id: 'dse', label: 'DSI (Datenschutzinformation)', required: true }, + { id: 'impressum', label: 'Impressum', required: true }, + { id: 'social_media', label: 'Social Media DSE', required: false }, + { id: 'cookie', label: 'Cookie-Richtlinie', required: false }, + { id: 'agb', label: 'AGB', required: false }, + { id: 'nutzungsbedingungen', label: 'Nutzungsbedingungen', required: false }, + { id: 'widerruf', label: 'Widerrufsbelehrung', required: false }, + { id: 'dsb', label: 'DSB-Kontakt', required: false }, + { id: 'news', label: 'Blog/Newsroom (für § 18 MStV)', required: false }, +] as const + +export type DocTypeId = typeof DOCUMENT_TYPES[number]['id'] diff --git a/backend-compliance/compliance/services/specialist_agents/impressum_agent.py b/backend-compliance/compliance/services/specialist_agents/impressum_agent.py index e0adec28..0b914488 100644 --- a/backend-compliance/compliance/services/specialist_agents/impressum_agent.py +++ b/backend-compliance/compliance/services/specialist_agents/impressum_agent.py @@ -29,9 +29,20 @@ PFLICHTANGABEN = { "label": "Name + Anschrift des Anbieters", "norm": "§ 5 Abs. 1 Nr. 1 TMG", "patterns": [ + # Label-Form: "Anbieter:", "Diensteanbieter:", "Verantwortlicher:" re.compile(r"\b(?:Anbieter|Diensteanbieter|" r"Verantwortlich(?:er Anbieter)?)\s*[:.\s]", re.IGNORECASE), + # Label-frei: Firma (Rechtsform) + Strasse + PLZ — Tesla- + # Pattern. Wenn das Impressum ohne explizites "Anbieter:"- + # Label direkt mit Firma+Adresse loslegt, gilt das auch. + re.compile( + r"\b[A-ZÄÖÜ][\w\-\& ]{1,80}?\s+" + r"(?:GmbH|AG|UG|KG|SE|GbR|OHG|Limited|Ltd|LLC)\s*" + r"[\s\S]{0,400}?" + r"\b\d{5}\s+[A-ZÄÖÜ]", + re.IGNORECASE, + ), ], "severity_if_missing": "HIGH", }, @@ -75,21 +86,76 @@ PFLICHTANGABEN = { "label": "Vertretungsberechtigte Person", "norm": "§ 5 Abs. 1 Nr. 1 TMG (juristische Personen)", "patterns": [ - re.compile(r"(?:Geschäftsführer|Vertretungsberechtigt|" - r"vertreten\s+durch)\s*[:.\s]", + # Korrekte Label: "Geschäftsführer:", "Vertretungsberechtigt:" + re.compile(r"(?:Gesch(?:ae|ä)ftsf(?:ue|ü)hrer|" + r"Vertretungsberechtigt|vertreten\s+durch)" + r"\s*[:.\s]", + re.IGNORECASE), + # US-Konzern-Habit: "Management" als nicht-rechtskonformes + # Alternativlabel (Tesla, Apple, etc.). Erkennen als + # vorhanden — separater Sub-Finding meldet die Label- + # Korrekturpflicht. + re.compile(r"\bManagement\s*[:.\s]\s*[A-ZÄÖÜ]", re.IGNORECASE), + re.compile(r"\bDirector(?:s|en)?\s*[:.\s]\s*[A-ZÄÖÜ]", re.IGNORECASE), ], "severity_if_missing": "HIGH", }, + "vertretungsberechtigte_label_korrekt": { + "label": "Korrekte Bezeichnung 'Geschäftsführer' statt 'Management'", + "norm": "§ 5 Abs. 1 Nr. 1 TMG (Deutsch-Pflicht, gerichtsfest)", + "patterns": [ + # PASSED nur wenn DEUTSCHES Label vorhanden. + re.compile(r"(?:Gesch(?:ae|ä)ftsf(?:ue|ü)hrer|" + r"Vorstand|" + r"Vertretungsberechtigt|vertreten\s+durch)" + r"\s*[:.\s]", + re.IGNORECASE), + ], + "severity_if_missing": "MEDIUM", + }, "aufsichtsbehoerde": { "label": "Aufsichtsbehörde (regulierte Branchen)", "norm": "§ 5 Abs. 1 Nr. 3 TMG (Branchen-bedingt)", "patterns": [ re.compile(r"Aufsichtsbeh(?:ö|oe)rde\s*[:.\s]", re.IGNORECASE), - re.compile(r"\bBAFin\b|\bBNetzA\b|\bLKA\b", re.IGNORECASE), + re.compile( + r"\bBAFin\b|\bBNetzA\b|\bLKA\b|\bKBA\b|" + r"Kraftfahrt-?Bundesamt|Bundesnetzagentur|" + r"Bundesanstalt\s+f(?:ü|ue)r", + re.IGNORECASE, + ), ], "severity_if_missing": "LOW", }, + "verantwortlicher_redaktion": { + "label": "Verantwortlicher § 18 MStV (journalistisch-redaktionell)", + "norm": "§ 18 MStV (bei Blog/News/Magazin/Newsroom Pflicht)", + "patterns": [ + re.compile( + r"(?:Verantwortlich(?:er|e)?\s+(?:f(?:ue|ü)r|i\.S\.d\.|" + r"nach|gem(?:ae|ä)ß)\s+§\s*18|" + r"V\.i\.S\.d\.\s*§?\s*18|" + r"redaktionell\s+Verantwortlich)", + re.IGNORECASE, + ), + ], + "severity_if_missing": "MEDIUM", + }, + "verbraucher_streitbeilegung": { + "label": "Verbraucher-Streitbeilegung-Hinweis", + "norm": "§ 36 VSBG (B2C-Anbieter Pflicht)", + "patterns": [ + re.compile( + r"(?:Verbraucherschlichtungs|VSBG|" + r"Streitbeilegung|" + r"Schlichtungsstelle|" + r"alternative\s+Streit(?:beilegung|schlichtung))", + re.IGNORECASE, + ), + ], + "severity_if_missing": "MEDIUM", + }, "berufsangaben": { "label": "Berufsbezeichnung + Berufsrechtliche Angaben", "norm": "§ 5 Abs. 1 Nr. 5 TMG (Kammerberufe)", @@ -122,23 +188,63 @@ def evaluate(impressum_text: str, return [] business_scope = business_scope or set() findings: list[dict] = [] + # Auto-detect KFZ-Hersteller / Auto-Direktvertrieb für KBA-Hint. + # Erst: explizite Begriffe (Fahrzeug, Automobil, …). + # Dann: bekannte Hersteller-Namen (BMW, Tesla, Mercedes, …). + is_automotive = bool(re.search( + r"\b(?:KFZ|Fahrzeug(?:e|herstellung|verkauf)?|Automobil|" + r"E-Auto|Elektroauto|Auto-?Konfigurator|" + r"Elektrofahrzeug|Hybrid-?Fahrzeug)\b", + impressum_text, re.IGNORECASE, + )) or bool(re.search( + r"\b(?:Tesla|BMW|Mercedes-?Benz|Audi|Volkswagen|Porsche|" + r"Volvo|Stellantis|Skoda|Seat|Cupra|MINI|Smart|" + r"Opel|Ford\s+Deutschland|Hyundai|Kia|Toyota|Mazda|" + r"Nissan|Honda|Subaru|Lexus|Polestar|NIO|BYD|Rivian|" + r"Lucid)\s+(?:Germany|Deutschland|Group|Holding|AG|" + r"GmbH|S(?:E|\.A\.))\b", + impressum_text, re.IGNORECASE, + )) for field_id, spec in PFLICHTANGABEN.items(): # Skip context-dependent fields when scope doesn't match if field_id == "odr_link" and "ecommerce" not in business_scope: continue - if field_id == "aufsichtsbehoerde" and ( - "regulated_profession" not in business_scope - and "financial_services" not in business_scope - and "insurance" not in business_scope + if field_id == "aufsichtsbehoerde" and not ( + "regulated_profession" in business_scope + or "financial_services" in business_scope + or "insurance" in business_scope + or is_automotive ): continue if field_id == "berufsangaben" and ( "regulated_profession" not in business_scope ): continue + if field_id == "verantwortlicher_redaktion" and ( + "editorial" not in business_scope + ): + continue + if field_id == "verbraucher_streitbeilegung" and ( + "ecommerce" not in business_scope + and "b2c" not in business_scope + ): + continue found = any(p.search(impressum_text) for p in spec["patterns"]) if found: continue + # Context-aware action: KBA-Hint bei Auto-Branche + action = ( + f"{spec['label']} im Impressum ergänzen " + f"(Pflichtangabe nach {spec['norm']})." + ) + if field_id == "aufsichtsbehoerde" and is_automotive: + action = ( + "Aufsichtsbehörde im Impressum benennen. Für " + "KFZ-Hersteller/-Vertrieb typisch: Kraftfahrt-" + "Bundesamt (KBA), Fördestraße 16, 24944 Flensburg, " + "www.kba.de. Bei Ladestrom-Vertrieb zusätzlich " + "Bundesnetzagentur (BNetzA)." + ) findings.append({ "check_id": f"IMPRESSUM-AGENT-{field_id.upper()}", "agent": "impressum_agent_v1", @@ -147,10 +253,7 @@ def evaluate(impressum_text: str, "severity_reason": "missing", "title": f"Pflichtangabe '{spec['label']}' fehlt im Impressum", "norm": spec["norm"], - "action": ( - f"{spec['label']} im Impressum ergänzen " - f"(Pflichtangabe nach {spec['norm']})." - ), + "action": action, }) if findings: logger.info(