Files
breakpilot-compliance/backend-compliance/compliance/services/doc_checks/impressum_checks.py
T
Benjamin Admin 0d37822b7c
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / detect-changes (push) Successful in 10s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 16s
CI / loc-budget (push) Failing after 16s
CI / go-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
fix(impressum): P9 — 7 False-Positive-Fixes in Pflichtangaben-Checks
#1 Name des Anbieters: \b Word-Boundary verhindert "ag" in "samstag",
   plus "aktiengesellschaft" als Volltreffer.
#2 Vertretungsberechtigte: Klammer-Liste-Pattern erkennt jetzt BMW-
   Format "Vorstand (Milan Nedeljkovic, Jochen Goller, ...)" plus
   "Vorsitzender des Aufsichtsrats: Name".
#3 V.i.S.d.P.: war schon INFO, OK.
#4 OS-Plattform/VSBG: bei no_direct_sales=True (OEM-Pattern) jetzt als
   "Nicht anwendbar" skipped statt 0/1 fail. Profile fliesst neu durch
   check_document_completeness -> runner.
#5 Zustaendige Kammer: IHK + Handwerkskammer + Tieraerztekammer in
   Pattern aufgenommen + severity LOW -> INFO (konditional).
#6 Stammkapital: war schon INFO, OK.
#7 Link-Disclaimer: neue Check-Eigenschaft "invert"=True. Anti-Pattern
   ist passed wenn NICHT gefunden, fail wenn gefunden. Vorher feuerte
   das Finding immer, jetzt nur wenn ein illegaler Disclaimer im Text
   ist.

Plus: L2-INFO-Checks (z.B. profession_chamber) zaehlen nicht mehr in
correctness-pct und erzeugen keine DSI-DETAIL-Findings. Konsistent
mit P8-Modell: INFO = "selbst pruefen", nicht "fail".

Verifiziert mit BMW-Impressum-Text — alle 7 Faelle korrekt klassifiziert:
  name=passed, representative_person=passed, profession_chamber=INFO,
  illegal_disclaimer=passed (kein Disclaimer im Text),
  dispute_resolution=skipped (no_direct_sales),
  editorial_visdp=INFO, share_capital=INFO.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:52:03 +02:00

327 lines
16 KiB
Python

"""
Impressum checks — §5 TMG / §18 MStV.
Level 1: Pflichtangabe erwaehnt?
Level 2: Pflichtangabe korrekt/vollstaendig?
Checks mit severity "INFO" sind kontextabhaengig — sie werden nur
als Hinweis angezeigt, nicht als Finding gewertet. Der Pruefer muss
selbst entscheiden ob sie fuer das geprueefte Unternehmen relevant sind.
"""
IMPRESSUM_CHECKLIST = [
# ── L1: Name des Anbieters ────────────────────────────────────────
{
"id": "name",
"label": "Name des Anbieters",
"level": 1, "parent": None,
"patterns": [
# Word-Boundaries verhindern Falsch-Treffer ("ag" in "samstag")
r"\b(?:gmbh|ag|e\.v\.|ohg|kg|gbr|ug|mbh|inc|ltd|aktiengesellschaft|kommanditgesellschaft|partnerschaft\s+mbb)\b",
r"\bfirma\s+\w+", r"\bunternehmen\s+\w+",
],
"severity": "HIGH",
"hint": "§5(1) Nr.1 TMG: Vollstaendiger Firmenname MIT Rechtsform (z.B. 'Muster GmbH', nicht nur 'Muster'). Bei Einzelunternehmen: Vor- und Nachname plus ggf. Geschaeftsbezeichnung. Haeufiger Abmahngrund: Nur Markenname ohne juristische Person.",
},
# ── L1: Anschrift ─────────────────────────────────────────────────
{
"id": "address",
"label": "Anschrift",
"level": 1, "parent": None,
"patterns": [
r"(?:str(?:asse|\.)|weg|platz|allee)\s*\.?\s*\d",
r"d-\d{5}", r"\d{5}\s+\w+",
],
"severity": "HIGH",
"hint": "§5(1) Nr.1 TMG verlangt eine ladungsfaehige Anschrift fuer Klagezustellungen. Postfach, c/o-Adresse oder nur Ortsangabe genuegen laut BGH (I ZR 228/03) nicht. Erforderlich: Strasse + Hausnummer + PLZ + Ort.",
},
{
"id": "address_zip_city",
"label": "PLZ + Ort vorhanden",
"level": 2, "parent": "address",
"patterns": [
r"(?:d[\-\s]?)?\d{5}\s+[a-z\u00c0-\u017e]\w{2,}",
],
"severity": "MEDIUM",
"hint": "Ohne PLZ und Ort ist die Anschrift nicht ladungsfaehig und damit unvollstaendig i.S.d. §5 TMG.",
},
{
"id": "address_street_number",
"label": "Strasse + Hausnummer vorhanden",
"level": 2, "parent": "address",
"patterns": [
r"[a-z\u00c0-\u017e]\w+(?:str|stra(?:ss|ß)e|weg|platz|allee|gasse|ring|damm|ufer)\s*\.?\s*\d+",
r"\w+\s+(?:str|stra(?:ss|ß)e|weg|platz|allee)\s*\.?\s*\d+",
],
"severity": "MEDIUM",
"hint": "Strasse + Hausnummer fehlen oder sind unvollstaendig. Ohne Hausnummer keine Zustellbarkeit — klassischer Abmahngrund.",
},
# ── L1: Kontaktdaten ──────────────────────────────────────────────
{
"id": "contact",
"label": "Kontaktdaten (E-Mail + Telefon)",
"level": 1, "parent": None,
"patterns": [
r"(?:e-?mail|mail).*@", r"telefon|phone|tel\.",
r"\+?\d[\d\s/\-\(\)]{8,}",
],
"severity": "HIGH",
"hint": "§5(1) Nr.2 TMG verlangt Angaben fuer 'schnelle elektronische Kontaktaufnahme und unmittelbare Kommunikation': E-Mail ist Pflicht. EuGH (C-298/17): Telefon nicht zwingend, aber ein zweiter unmittelbarer Kanal (Telefon, Fax oder Chat) ist erforderlich.",
},
{
"id": "contact_email_format",
"label": "E-Mail-Adresse im korrekten Format",
"level": 2, "parent": "contact",
"patterns": [
r"[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}",
],
"severity": "MEDIUM",
"hint": "E-Mail-Adresse muss direkt im Impressum als Text sichtbar sein. Ein reines Kontaktformular genuegt laut OLG Hamm (4 U 59/20) NICHT.",
},
{
"id": "contact_phone_format",
"label": "Telefonnummer vorhanden",
"level": 2, "parent": "contact",
"patterns": [
r"(?:tel(?:efon)?|phone|fon)\s*[.:]\s*[\+\d][\d\s/\-\(\)]{6,}",
r"\+49\s*[\d\s/\-\(\)]{8,}",
r"0\d{2,4}\s*[/\-\s]\s*[\d\s\-]{4,}",
r"(?:telefon|tel\.?|phone|fon)\s+\d[\d\s/\-]{6,}",
],
"severity": "MEDIUM",
"hint": "Telefonnummer mit Vorwahl angeben (z.B. '+49 30 12345678' oder '0761 / 489 809 01'). Falls kein Telefon: Ein alternativer unmittelbarer Kommunikationskanal ist laut EuGH (C-298/17) noetig.",
},
# ── L1: Handelsregister ───────────────────────────────────────────
{
"id": "register",
"label": "Handelsregister / Registernummer",
"level": 1, "parent": None,
"patterns": [
r"(?:handelsregister|hrb|hra|registergericht|amtsgericht)",
r"register.*(?:nr|nummer)",
r"\bag\s+[a-z\u00c0-\u017e]\w+",
],
"severity": "MEDIUM",
"hint": "§5(1) Nr.4 TMG: Bei Eintragung im Handels-, Vereins-, Partnerschafts- oder Genossenschaftsregister muessen Registergericht UND Registernummer angegeben werden.",
},
{
"id": "register_court",
"label": "Registergericht benannt (Amtsgericht X)",
"level": 2, "parent": "register",
"patterns": [
# "Amtsgericht <Stadt>" or "Registergericht <Stadt>"
# Allow colon/dot/dash between keyword and city (BMW writes
# "registergericht: m\u00fcnchen hrb 42243").
r"(?:amtsgericht|registergericht)[\s:\.\-,]+[a-zA-Z\u00c0-\u017e]\w+",
# "AG <Stadt>" short form
r"\bag\s+[a-zA-Z\u00c0-\u017e]\w+",
# "Handelsregister AG/Amtsgericht <Stadt>"
r"(?:handelsregister|register)\s+(?:ag|amtsgericht)\s+\w+",
# "Sitz und Registergericht: M\u00fcnchen" \u2014 BMW pattern
r"sitz\s+und\s+registergericht[\s:\.\-,]+[a-zA-Z\u00c0-\u017e]\w+",
],
"severity": "LOW",
"hint": "Registergericht benennen (z.B. 'Amtsgericht Freiburg' oder 'AG Freiburg'). Beides ist korrekt.",
},
{
"id": "register_number",
"label": "Registernummer (HRB/HRA + Nummer)",
"level": 2, "parent": "register",
"patterns": [
r"(?:hrb|hra)\s*\d+",
],
"severity": "LOW",
"hint": "Registernummer im Format 'HRB 12345' (Kapitalgesellschaften) oder 'HRA 12345' (Personengesellschaften) angeben.",
},
# ── L1: USt-IdNr ──────────────────────────────────────────────────
{
"id": "vat",
"label": "USt-IdNr.",
"level": 1, "parent": None,
"patterns": [
r"ust[\s.-]*id", r"umsatzsteuer[\s-]*id",
r"umsatzsteuer.*identifikation",
r"vat[\s.-]*id", r"de\s*\d{3}\s*\d{3}\s*\d{3}",
],
"severity": "MEDIUM",
"hint": "§5(1) Nr.6 TMG: Die USt-IdNr. muss angegeben werden, sofern vorhanden. Die Steuernummer ist KEIN Ersatz.",
},
{
"id": "vat_de_format",
"label": "USt-IdNr. im Format DE + 9 Ziffern",
"level": 2, "parent": "vat",
"patterns": [
r"de\s*\d{3}\s*\d{3}\s*\d{3}",
],
"severity": "LOW",
"hint": "Deutsche USt-IdNr.: 'DE' + exakt 9 Ziffern (z.B. DE123456789). Validierung: https://evatr.bff-online.de/",
},
# ── L1: Vertretungsberechtigte ────────────────────────────────────
{
"id": "representative",
"label": "Vertretungsberechtigte",
"level": 1, "parent": None,
"patterns": [
r"vertretungsberechtigt",
r"gesch(?:ae|ä)ftsf(?:ue|ü)hr",
r"vorstand", r"inhaber",
],
"severity": "MEDIUM",
"hint": "§5(1) Nr.1 TMG: Bei juristischen Personen muss der/die Vertretungsberechtigte(n) namentlich benannt werden.",
},
{
"id": "representative_person",
"label": "Name der vertretungsberechtigten Person",
"level": 2, "parent": "representative",
"patterns": [
r"(?:gesch(?:ae|ä)ftsf(?:ue|ü)hr\w*|vorstand|inhaber|aufsichtsrats?)\s*[:\-]?\s*[a-zA-Z\u00c0-\u017e]",
# "Vorstand (Milan Nedeljkovic, ...)" - BMW-Pattern mit Klammer-Liste
r"(?:vorstand|gesch(?:ae|ä)ftsf(?:ue|ü)hrung|aufsichtsrats?)\s*\(\s*[a-zA-Z\u00c0-\u017e]",
r"(?:vertreten\s+durch|repr(?:ae|ä)sentiert)\s*[:\-]?\s*(?:den\s+vorstand\s*\(?|[a-zA-Z\u00c0-\u017e])",
r"(?:gesch(?:ae|ä)ftsf(?:ue|ü)hrung)\s*[:\-]?\s*(?:dr\.?\s+|prof\.?\s+)?[a-zA-Z\u00c0-\u017e]",
# "Vorsitzender des Aufsichtsrats: Nicolas Peter"
r"(?:vorsitzend\w+|stellv\w*\s+vorsitz\w*)\s+(?:des\s+\w+\s*)?[:\-]?\s*[a-zA-Z\u00c0-\u017e]",
],
"severity": "LOW",
"hint": "Voller Vor- und Nachname mit Funktionsbezeichnung erforderlich (z.B. 'Geschaeftsfuehrung: Dr. Max Mustermann').",
},
# ── Kontextabhaengige Checks (INFO — nur Hinweis, kein Finding) ──
{
"id": "editorial_visdp",
"label": "V.i.S.d.P. / Redaktionell Verantwortlicher (§18 MStV)",
"level": 1, "parent": None,
"patterns": [
r"v\.?\s*i\.?\s*s\.?\s*d\.?\s*p",
r"(?:redaktionell|inhaltlich)\s+verantwortlich",
r"§\s*18\s+(?:abs\.?\s*\d+\s+)?m(?:edien)?st(?:aat)?v",
r"verantwortlich\w*\s+i\.?\s*s\.?\s*(?:d\.?\s*)?v\.?",
],
"severity": "INFO",
"hint": "Nur relevant wenn die Website journalistisch-redaktionelle Inhalte hat (Blog, Ratgeber, News, Fachartikel). Reine Unternehmensseiten ohne redaktionelle Inhalte benoetigen keinen V.i.S.d.P. Pruefen Sie, ob die Website einen Blog oder Ratgeber-Bereich hat.",
},
{
"id": "dispute_resolution",
"label": "Verbraucherstreitbeilegung / OS-Plattform",
"level": 1, "parent": None,
"patterns": [
r"verbraucherstreitbeilegung|streitschlichtung",
r"(?:os|odr)[\-\s]plattform",
r"ec\.europa\.eu.*odr",
r"vsbg|verbraucherstreitbeilegungsgesetz",
r"alternative\s+streitbeilegung",
],
"severity": "INFO",
"hint": "Nur relevant fuer B2C-Online-Haendler die Waren oder Dienstleistungen an Verbraucher verkaufen. B2B-Unternehmen ohne Verbrauchergeschaeft sind von §36 VSBG und der ODR-Verordnung nicht betroffen. Pruefen Sie, ob das Unternehmen B2C-Geschaeft betreibt.",
},
{
"id": "regulated_profession",
"label": "Berufsrechtliche Angaben (§5(1) Nr.5 TMG)",
"level": 1, "parent": None,
"patterns": [
r"(?:rechtsanwalt|anwalt|notar|steuerberater|wirtschaftspr(?:ue|ü)fer|arzt|(?:ae|ä)rzt|zahnarzt|apotheker|architekt|ingenieur|psychotherapeut|heilpraktiker)",
r"(?:kammer|berufsordnung|berufsrecht|standesrecht|zulassung)",
r"(?:(?:ae|ä)rztekammer|rechtsanwaltskammer|steuerberaterkammer|architektenkammer|ingenieurkammer)",
],
"severity": "INFO",
"hint": "Nur relevant fuer reglementierte Berufe (Aerzte, Anwaelte, Steuerberater, Architekten, Apotheker). Falls das Unternehmen keinen reglementierten Beruf ausueebt, ist dieser Punkt nicht zutreffend.",
},
{
"id": "profession_chamber",
"label": "Zustaendige Kammer benannt",
"level": 2, "parent": "regulated_profession",
"patterns": [
r"(?:(?:ae|ä)rztekammer|rechtsanwaltskammer|steuerberaterkammer|architektenkammer|ingenieurkammer|apothekerkammer|handwerkskammer|tier(?:ae|ä)rztekammer|psychotherapeutenkammer)",
r"\bihk\b|industrie-?\s+und\s+handelskammer",
r"(?:mitglied|zugelassen|eingetragen)\s+(?:bei|in|der)\s+(?:der\s+)?(?:\w+)?kammer",
],
"severity": "INFO", # P9: konditional - nur kammerpflichtige Berufe
"hint": "Zustaendige Kammer mit vollem Namen und Sitz nennen (z.B. 'Rechtsanwaltskammer Muenchen', 'IHK Muenchen'). Nur relevant fuer kammerpflichtige Berufe.",
},
{
"id": "profession_title",
"label": "Berufsbezeichnung + Verleihungsstaat",
"level": 2, "parent": "regulated_profession",
"patterns": [
r"berufsbezeichnung|gesetzliche\s+berufsbezeichnung",
r"verliehen\s+in|verleihungsstaat",
r"(?:rechtsanwalt|steuerberater|arzt|architekt)\s*(?:\(|,)\s*(?:deutschland|bundesrepublik)",
],
"severity": "LOW",
"hint": "Berufsbezeichnung und Staat der Verleihung angeben.",
},
{
"id": "profession_regulations",
"label": "Berufsrechtliche Regelungen + Zugang",
"level": 2, "parent": "regulated_profession",
"patterns": [
r"(?:berufsordnung|brao|bora|steuerberatungsgesetz|stberg|bundes(?:ae|ä)rzteordnung|heilberufe|mbo|architekteng)",
r"berufsrecht|standesrecht|berufsrechtliche\s+regelung",
],
"severity": "LOW",
"hint": "Berufsrechtliche Regelungen nennen und Link zum Volltext bereitstellen.",
},
{
"id": "share_capital",
"label": "Stammkapital / Grundkapital (GmbH/AG/UG)",
"level": 1, "parent": None,
"patterns": [
r"stammkapital|grundkapital|haftkapital",
r"(?:kapital|einlage)\s*:?\s*(?:eur|euro|\u20ac)\s*[\d\.,]+",
r"[\d\.,]+\s*(?:eur|euro|\u20ac)\s*(?:stammkapital|grundkapital)",
],
"severity": "INFO",
"hint": "§35a GmbHG verlangt die Angabe des Stammkapitals auf Geschaeftsbriefen. Ob dies auch fuer Websites gilt, ist umstritten. In der Praxis wird es selten beanstandet. Bei UG (haftungsbeschraenkt) ist die Angabe empfehlenswert, da das geringe Stammkapital fuer Geschaeftspartner relevant ist.",
},
{
"id": "supervisory_authority",
"label": "Aufsichtsbehoerde (genehmigungspflichtige Taetigkeiten)",
"level": 1, "parent": None,
"patterns": [
r"aufsichtsbeh(?:oe|ö)rde|genehmigungsbeh(?:oe|ö)rde|zulassungsbeh(?:oe|ö)rde",
r"gewerbeaufsicht|gewerbeamt|ordnungsamt",
r"(?:zugelassen|genehmigt|erlaubt)\s+(?:durch|von)\s+(?:der|dem|die)",
r"bafin|§\s*34[cdf]\s+gewo",
],
"severity": "INFO",
"hint": "Nur relevant bei genehmigungspflichtigen Taetigkeiten: Immobilienmakler (§34c GewO), Finanzanlagenvermittler (§34f), Versicherungsvermittler (§34d), Gastronomie, Bewachungsgewerbe. Falls das Unternehmen keine solche Taetigkeit ausueebt, ist dieser Punkt nicht zutreffend.",
},
{
"id": "professional_insurance",
"label": "Berufshaftpflichtversicherung (DL-InfoV §2(1) Nr.11)",
"level": 1, "parent": None,
"patterns": [
r"berufshaftpflicht|haftpflichtversicherung|pflichtversicherung",
r"(?:versicherer|versicherung)\s*:?\s*[a-zA-Z\u00c0-\u017e]",
r"deckungssumme|versicherungsschutz|geltungsbereich",
],
"severity": "INFO",
"hint": "Nur relevant bei gesetzlicher Pflichtversicherung (Aerzte, Anwaelte, Architekten, Steuerberater, Makler). Falls das Unternehmen keine Pflichtversicherung benoetigt, ist dieser Punkt nicht zutreffend.",
},
{
"id": "illegal_disclaimer",
"label": "Rechtswidriger Haftungsausschluss fuer Links",
"level": 1, "parent": None,
"patterns": [
r"haftungsausschluss|disclaimer|haftung\s+f(?:ue|ü)r\s+(?:links|inhalte)",
r"keine\s+(?:gew(?:ae|ä)hr|haftung|verantwortung)\s+f(?:ue|ü)r\s+(?:externe|verlinkte|fremde)",
r"distanzier|macht\s+sich\s+(?:nicht|kein)\s+(?:zu\s+eigen|verantwortlich)",
],
"severity": "LOW",
"invert": True, # Anti-Pattern: passed wenn NICHT gefunden
"hint": "Der klassische Link-Disclaimer ('Wir distanzieren uns von verlinkten Inhalten') ist seit BGH (I ZR 317/01) rechtlich wirkungslos. Empfehlung: Entfernen Sie pauschale Disclaimer — sie schuetzen nicht und koennen kontraproduktiv sein.",
},
]