fix: Context-aware Impressum checks + 3 regex fixes

3 Regex fixes:
- Telefon: matches '0761 / 48 98 09 01' format (spaces around /)
- Registergericht: matches 'AG Freiburg' (not just 'Amtsgericht')
- Vertretung: matches 'Geschaeftsfuehrung:' (not just 'Geschaeftsfuehrer:')

6 checks changed from FAIL to INFO severity:
- V.i.S.d.P.: only relevant if website has editorial content
- Streitbeilegung: only relevant for B2C online shops
- Berufsrecht: only relevant for regulated professions
- Stammkapital: legally required but rarely enforced
- Aufsichtsbehoerde: only for licensed activities
- Berufshaftpflicht: only for mandatory insurance

INFO checks don't count towards completeness percentage.
They appear as hints, not findings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-11 15:23:19 +02:00
parent 916337b503
commit 0c25832b5c
2 changed files with 54 additions and 45 deletions
@@ -3,6 +3,10 @@ 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 = [
@@ -25,7 +29,7 @@ IMPRESSUM_CHECKLIST = [
"label": "Anschrift",
"level": 1, "parent": None,
"patterns": [
r"(?:str(?:asse|\.)|weg|platz|allee)\s*\d",
r"(?:str(?:asse|\.)|weg|platz|allee)\s*\.?\s*\d",
r"d-\d{5}", r"\d{5}\s+\w+",
],
"severity": "HIGH",
@@ -39,7 +43,7 @@ IMPRESSUM_CHECKLIST = [
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. Haeufiger Fehler: Nur Strasse und Hausnummer ohne PLZ/Ort, oder PLZ ohne Ortsangabe.",
"hint": "Ohne PLZ und Ort ist die Anschrift nicht ladungsfaehig und damit unvollstaendig i.S.d. §5 TMG.",
},
{
"id": "address_street_number",
@@ -50,7 +54,7 @@ IMPRESSUM_CHECKLIST = [
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 — das ist ein klassischer Abmahngrund bei Impressumspruefungen nach §5 TMG. Auch 'Am Markt' ohne Nummer genuegt nicht.",
"hint": "Strasse + Hausnummer fehlen oder sind unvollstaendig. Ohne Hausnummer keine Zustellbarkeit — klassischer Abmahngrund.",
},
# ── L1: Kontaktdaten ──────────────────────────────────────────────
@@ -60,7 +64,7 @@ IMPRESSUM_CHECKLIST = [
"level": 1, "parent": None,
"patterns": [
r"(?:e-?mail|mail).*@", r"telefon|phone|tel\.",
r"\+?\d[\d\s/\-]{8,}",
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.",
@@ -73,19 +77,20 @@ IMPRESSUM_CHECKLIST = [
r"[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}",
],
"severity": "MEDIUM",
"hint": "E-Mail-Adresse fehlt oder ist nicht als solche erkennbar. Ein reines Kontaktformular genuegt laut OLG Hamm (4 U 59/20) NICHT als Ersatz — die E-Mail-Adresse muss direkt im Impressum als Text sichtbar sein.",
"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{4,}",
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'). Falls kein Telefon: Ein alternativer unmittelbarer Kommunikationskanal (Chat, Messenger) ist laut EuGH (C-298/17) noetig — Kontaktformular allein genuegt nicht.",
"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 ───────────────────────────────────────────
@@ -96,9 +101,10 @@ IMPRESSUM_CHECKLIST = [
"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. Haeufiger Fehler: GmbH ohne HR-Angabe — das ist abmahnfaehig.",
"hint": "§5(1) Nr.4 TMG: Bei Eintragung im Handels-, Vereins-, Partnerschafts- oder Genossenschaftsregister muessen Registergericht UND Registernummer angegeben werden.",
},
{
"id": "register_court",
@@ -106,10 +112,11 @@ IMPRESSUM_CHECKLIST = [
"level": 2, "parent": "register",
"patterns": [
r"(?:amtsgericht|registergericht)\s+[A-Z\u00c0-\u017e]\w+",
r"ag\s+[A-Z\u00c0-\u017e]\w+",
r"\bag\s+[A-Z\u00c0-\u017e]\w+",
r"(?:handelsregister|register)\s+(?:ag|amtsgericht)\s+\w+",
],
"severity": "LOW",
"hint": "Registernummer ohne Registergericht ist unvollstaendig i.S.d. §5(1) Nr.4 TMG. Korrekt: 'Amtsgericht Muenchen, HRB 12345'. Das Gericht am Sitz der Gesellschaft ist zustaendig — pruefen Sie den aktuellen HR-Auszug.",
"hint": "Registergericht benennen (z.B. 'Amtsgericht Freiburg' oder 'AG Freiburg'). Beides ist korrekt.",
},
{
"id": "register_number",
@@ -119,7 +126,7 @@ IMPRESSUM_CHECKLIST = [
r"(?:hrb|hra)\s*\d+",
],
"severity": "LOW",
"hint": "Registernummer im Format 'HRB 12345' (Kapitalgesellschaften) oder 'HRA 12345' (Personengesellschaften) angeben. Haeufiger Fehler: Steuernummer statt Registernummer — die Steuernummer ersetzt nicht die HR-Angabe nach §5(1) Nr.4 TMG.",
"hint": "Registernummer im Format 'HRB 12345' (Kapitalgesellschaften) oder 'HRA 12345' (Personengesellschaften) angeben.",
},
# ── L1: USt-IdNr ──────────────────────────────────────────────────
@@ -132,7 +139,7 @@ IMPRESSUM_CHECKLIST = [
r"vat.*id", r"de\s*\d{9}",
],
"severity": "MEDIUM",
"hint": "§5(1) Nr.6 TMG: Die USt-IdNr. muss angegeben werden, sofern vorhanden. Haeufiger Fehler: Steuernummer (z.B. '123/456/78901') statt USt-IdNr. (DE123456789) — die Steuernummer ist KEIN Ersatz und sollte aus Datenschutzgruenden nicht im Impressum stehen.",
"hint": "§5(1) Nr.6 TMG: Die USt-IdNr. muss angegeben werden, sofern vorhanden. Die Steuernummer ist KEIN Ersatz.",
},
{
"id": "vat_de_format",
@@ -142,7 +149,7 @@ IMPRESSUM_CHECKLIST = [
r"de\s*\d{9}",
],
"severity": "LOW",
"hint": "Deutsche USt-IdNr.: Laendercode 'DE' + exakt 9 Ziffern (z.B. DE123456789). Haeufiger Fehler: Nur 8 Ziffern, fehlender Laendercode, oder Verwechslung mit Wirtschafts-ID. Validierung: https://evatr.bff-online.de/",
"hint": "Deutsche USt-IdNr.: 'DE' + exakt 9 Ziffern (z.B. DE123456789). Validierung: https://evatr.bff-online.de/",
},
# ── L1: Vertretungsberechtigte ────────────────────────────────────
@@ -151,25 +158,28 @@ IMPRESSUM_CHECKLIST = [
"label": "Vertretungsberechtigte",
"level": 1, "parent": None,
"patterns": [
r"vertretungsberechtigt", r"gesch(?:ae|ä)ftsf(?:ue|ü)hr",
r"vertretungsberechtigt",
r"gesch(?:ae|ä)ftsf(?:ue|ü)hr",
r"vorstand", r"inhaber",
],
"severity": "MEDIUM",
"hint": "§5(1) Nr.1 TMG: Bei juristischen Personen (GmbH, AG, UG, eG) muss der/die Vertretungsberechtigte(n) namentlich benannt werden. Haeufiger Fehler: Nur 'Geschaeftsfuehrung' ohne Personenname — das genuegt nicht, Vor- und Nachname sind Pflicht.",
"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|vorstand|inhaber)\w*\s*:\s*[A-Z\u00c0-\u017e]",
r"(?:gesch(?:ae|ä)ftsf(?:ue|ü)hr\w*|vorstand|inhaber)\s*:?\s*[A-Z\u00c0-\u017e]",
r"(?:vertreten\s+durch|repr(?:ae|ä)sentiert)\s*:?\s*[A-Z\u00c0-\u017e]",
r"(?:gesch(?:ae|ä)ftsf(?:ue|ü)hrung)\s*:?\s*(?:dr\.?\s+|prof\.?\s+)?[A-Z\u00c0-\u017e]",
],
"severity": "LOW",
"hint": "Voller Vor- und Nachname mit Funktionsbezeichnung erforderlich (z.B. 'Geschaeftsfuehrer: Max Mustermann'). Bei mehreren Geschaeftsfuehrern alle nennen. Haeufiger Fehler: Nur Nachname oder nur 'Die Geschaeftsfuehrung' ohne Namen.",
"hint": "Voller Vor- und Nachname mit Funktionsbezeichnung erforderlich (z.B. 'Geschaeftsfuehrung: Dr. Max Mustermann').",
},
# ── Neue L1: Redaktionell Verantwortlicher ────────────────────────
# ── Kontextabhaengige Checks (INFO — nur Hinweis, kein Finding) ──
{
"id": "editorial_visdp",
"label": "V.i.S.d.P. / Redaktionell Verantwortlicher (§18 MStV)",
@@ -179,11 +189,10 @@ IMPRESSUM_CHECKLIST = [
r"(?:redaktionell|inhaltlich)\s+verantwortlich",
r"§\s*18\s+m(?:edien)?st(?:aat)?v",
],
"severity": "LOW",
"hint": "§18(2) MStV: Bei journalistisch-redaktionellen Inhalten (Blog, Ratgeber, News) muss ein V.i.S.d.P. mit Name und Anschrift benannt werden. Gilt auch fuer Unternehmensblogs. Haeufiger Fehler: V.i.S.d.P. fehlt bei Seiten mit Ratgeber-/Blogartikeln.",
"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.",
},
# ── Neue L1: Streitbeilegung ──────────────────────────────────────
{
"id": "dispute_resolution",
"label": "Verbraucherstreitbeilegung / OS-Plattform",
@@ -195,11 +204,10 @@ IMPRESSUM_CHECKLIST = [
r"vsbg|verbraucherstreitbeilegungsgesetz",
r"alternative\s+streitbeilegung",
],
"severity": "LOW",
"hint": "Art. 14(1) ODR-VO + §36 VSBG: Online-Haendler muessen den ODR-Link (https://ec.europa.eu/consumers/odr) als klickbaren Hyperlink einbinden UND erklaeren, ob sie zur Streitbeilegung bereit/verpflichtet sind. Fehlender Link ist abmahnfaehig (LG Bochum, 14 O 21/16).",
"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.",
},
# ── L1: Reglementierte Berufe (§5(1) Nr.5 TMG) ───────────────────
{
"id": "regulated_profession",
"label": "Berufsrechtliche Angaben (§5(1) Nr.5 TMG)",
@@ -209,8 +217,8 @@ IMPRESSUM_CHECKLIST = [
r"(?:kammer|berufsordnung|berufsrecht|standesrecht|zulassung)",
r"(?:(?:ae|ä)rztekammer|rechtsanwaltskammer|steuerberaterkammer|architektenkammer|ingenieurkammer)",
],
"severity": "MEDIUM",
"hint": "§5(1) Nr.5 TMG: Bei reglementierten Berufen (Aerzte, Anwaelte, Steuerberater, Architekten) muessen angegeben werden: (1) zustaendige Kammer, (2) gesetzliche Berufsbezeichnung + Staat der Verleihung, (3) berufsrechtliche Regelungen mit Zugangsmoeglichkeit.",
"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",
@@ -221,7 +229,7 @@ IMPRESSUM_CHECKLIST = [
r"(?:mitglied|zugelassen|eingetragen)\s+(?:bei|in|der)\s+(?:der\s+)?(?:\w+)?kammer",
],
"severity": "LOW",
"hint": "Nennen Sie die zustaendige Kammer mit vollem Namen und Sitz (z.B. 'Rechtsanwaltskammer Muenchen'). Ohne Kammerangabe fehlt die Zuordnung zur Berufsaufsicht — Verstoss gegen §5(1) Nr.5a TMG.",
"hint": "Zustaendige Kammer mit vollem Namen und Sitz nennen (z.B. 'Rechtsanwaltskammer Muenchen').",
},
{
"id": "profession_title",
@@ -233,7 +241,7 @@ IMPRESSUM_CHECKLIST = [
r"(?:rechtsanwalt|steuerberater|arzt|architekt)\s*(?:\(|,)\s*(?:deutschland|bundesrepublik)",
],
"severity": "LOW",
"hint": "§5(1) Nr.5a TMG: Berufsbezeichnung (z.B. 'Rechtsanwalt') und Staat der Verleihung (z.B. 'Bundesrepublik Deutschland') angeben. Bei EU-auslaendischen Qualifikationen ist der Herkunftsstaat besonders wichtig.",
"hint": "Berufsbezeichnung und Staat der Verleihung angeben.",
},
{
"id": "profession_regulations",
@@ -244,10 +252,9 @@ IMPRESSUM_CHECKLIST = [
r"berufsrecht|standesrecht|berufsrechtliche\s+regelung",
],
"severity": "LOW",
"hint": "§5(1) Nr.5c TMG: Berufsrechtliche Regelungen nennen (BRAO/BORA fuer Anwaelte, MBO-Ae fuer Aerzte, StBerG fuer Steuerberater) und Link zum Volltext bereitstellen (z.B. zur BRAK oder Kammer-Website).",
"hint": "Berufsrechtliche Regelungen nennen und Link zum Volltext bereitstellen.",
},
# ── L1: Grundkapital (§5(1) Nr.1 TMG) ────────────────────────────
{
"id": "share_capital",
"label": "Stammkapital / Grundkapital (GmbH/AG/UG)",
@@ -257,11 +264,10 @@ IMPRESSUM_CHECKLIST = [
r"(?:kapital|einlage)\s*:?\s*(?:eur|euro|\u20ac)\s*[\d\.,]+",
r"[\d\.,]+\s*(?:eur|euro|\u20ac)\s*(?:stammkapital|grundkapital)",
],
"severity": "LOW",
"hint": "§5(1) Nr.1 TMG i.V.m. §35a GmbHG / §80 AktG: GmbH/UG muessen das Stammkapital, AG das Grundkapital angeben. Bei nicht voll eingezahltem Kapital: Gesamtbetrag der ausstehenden Einlagen nennen. Besonders bei UG (haftungsbeschraenkt) haeufig vergessen.",
"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.",
},
# ── L1: Aufsichtsbehoerde (§5(1) Nr.3 TMG) ──────────────────────
{
"id": "supervisory_authority",
"label": "Aufsichtsbehoerde (genehmigungspflichtige Taetigkeiten)",
@@ -272,11 +278,10 @@ IMPRESSUM_CHECKLIST = [
r"(?:zugelassen|genehmigt|erlaubt)\s+(?:durch|von)\s+(?:der|dem|die)",
r"bafin|§\s*34[cdf]\s+gewo",
],
"severity": "LOW",
"hint": "§5(1) Nr.3 TMG: Bei genehmigungspflichtigen Taetigkeiten (Immobilienmakler §34c GewO, Finanzanlagenvermittler §34f, Versicherungsvermittler §34d, Gastronomie, Bewachung) muss die zustaendige Aufsichtsbehoerde mit Kontaktdaten angegeben werden.",
"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.",
},
# ── L1: Berufshaftpflichtversicherung (DL-InfoV) ─────────────────
{
"id": "professional_insurance",
"label": "Berufshaftpflichtversicherung (DL-InfoV §2(1) Nr.11)",
@@ -286,11 +291,10 @@ IMPRESSUM_CHECKLIST = [
r"(?:versicherer|versicherung)\s*:?\s*[A-Z\u00c0-\u017e]",
r"deckungssumme|versicherungsschutz|geltungsbereich",
],
"severity": "LOW",
"hint": "DL-InfoV §2(1) Nr.11: Bei gesetzlicher Pflichtversicherung (Aerzte, Anwaelte, Architekten, Steuerberater, Makler nach §34c GewO) muessen Name + Anschrift des Versicherers und raeumlicher Geltungsbereich angegeben werden.",
"severity": "INFO",
"hint": "Nur relevant bei gesetzlicher Pflichtversicherung (Aerzte, Anwaelte, Architekten, Steuerberater, Makler). Falls das Unternehmen keine Pflichtversicherung benoetigt, ist dieser Punkt nicht zutreffend.",
},
# ── L1: Rechtswidrige Haftungsausschluesse ────────────────────────
{
"id": "illegal_disclaimer",
"label": "Rechtswidriger Haftungsausschluss fuer Links",
@@ -301,6 +305,6 @@ IMPRESSUM_CHECKLIST = [
r"distanzier|macht\s+sich\s+(?:nicht|kein)\s+(?:zu\s+eigen|verantwortlich)",
],
"severity": "LOW",
"hint": "Vorsicht: Der klassische Link-Disclaimer ('Wir distanzieren uns von verlinkten Inhalten') ist seit BGH (I ZR 317/01) rechtlich wirkungslos und wird von Gerichten als Zeichen mangelnder Rechtskenntnis gewertet. Empfehlung: Entfernen Sie pauschale Disclaimer — sie schuetzen nicht und koennen kontraproduktiv sein.",
"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.",
},
]
@@ -111,14 +111,19 @@ def check_document_completeness(
passed_l1_ids: set[str] = set()
all_checks: list[dict] = []
l1_present = 0
l1_scoreable = 0 # Exclude INFO checks from score
for check in l1_checks:
is_info = check.get("severity") == "INFO"
match = _match_patterns(check["patterns"], text_lower)
passed = match is not None
if passed:
passed_l1_ids.add(check["id"])
l1_present += 1
else:
if not is_info:
l1_present += 1
if not is_info:
l1_scoreable += 1
if not passed and not is_info:
findings.append({
"code": f"DSI-MISSING-{check['id'].upper()}",
"severity": check.get("severity", "MEDIUM"),
@@ -175,7 +180,7 @@ def check_document_completeness(
})
# ── Summary ───────────────────────────────────────────────────────
l1_total = len(l1_checks)
l1_total = l1_scoreable # Exclude INFO checks from percentage
completeness_pct = round(l1_present / l1_total * 100) if l1_total else 0
correctness_pct = round(l2_passed / l2_total * 100) if l2_total else 0