integrity_scanner_fuer_stat.../scanner/plain.py
Dieter Schlüter 3ab313e733 fix: Report und Alarm-Mail konsistent (gemeinsamer Renderer)
Behebt vier vom Nutzer gemeldete Inkonsistenzen zwischen Report und E-Mail:

1. E-Mail ⊊ Report: Es gab zwei divergierende Renderer (alerter._format_body
   und __main__._render_markdown). Jetzt EIN gemeinsamer Renderer
   (scanner/report.py:render_markdown), den beide nutzen → identischer Inhalt,
   keine Divergenz mehr möglich.

2. KI im Report unsichtbar: Der Report rief klartext_befunde OHNE ai_result auf
   (KI-Satz fehlte im "Was bedeutet das?") und die Zusammenfassungs-Tabelle
   hatte keine KI-Zeile. Jetzt: KI-Satz im Klartext + Zeilen "KI-Funde" und
   "KI nicht geprüft" in der Tabelle.

3. Fehlende URL in Kommandos: approve/report/ai-dismiss standen ohne Target da
   (nicht ausführbar). Neuer Helfer plain.scanner_cmd(target, rest) injiziert
   das Target überall → "python -m scanner https://site approve --all".

4. "Level: YELLOW" aber "Score: 0": Der angezeigte Score war nur der Inhalts-
   Diff-Score; KI-/Cloak-/Asset-Scores flossen ins Level, aber nicht in den
   Score. cmd_scan/cmd_check aggregieren jetzt alle Teil-Scores → Score und
   Level passen zusammen.

Tests: 254 grün (+4: Body==Report, Kommandos-mit-Target, Score-Anzeige,
Level-Badge). alerter._format_body delegiert nur noch an render_markdown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 03:35:05 +02:00

228 lines
9.2 KiB
Python

"""
Klartext-Schicht: übersetzt technische Befunde in Sätze, die auch ein
Website-Betreiber ohne IT-Wissen versteht.
Alle Texte hier sind bewusst laienverständlich. Fachbegriffe (Score, Canonical,
JSON-LD, Meta-Refresh, Hash) gehören NICHT in dieses Modul — die technischen
Details stehen weiterhin im Detail-Abschnitt des Reports.
"""
# Ampel-Überschrift je Risiko-Level
_AMPEL = {
"green": ("🟢", "Alles in Ordnung."),
"yellow": ("🟡", "Bitte einmal nachschauen."),
"red": ("🔴", "Achtung — bitte zeitnah prüfen oder Ihren Dienstleister informieren."),
}
def ampel(level: str) -> tuple[str, str]:
"""Symbol und Überschrift für ein Risiko-Level (green/yellow/red)."""
return _AMPEL.get(level, ("", "Status unbekannt."))
def scanner_cmd(target: str, rest: str) -> str:
"""Baut ein direkt ausführbares Kommando mit Target, z. B.
'python -m scanner https://meine-seite.de approve --all'."""
return f"python -m scanner {target} {rest}".strip() if target else f"python -m scanner {rest}"
def ampel_zeile(level: str) -> str:
"""Einzeilige Ampel-Darstellung, z. B. '🟡 Bitte einmal nachschauen.'"""
symbol, satz = ampel(level)
return f"{symbol} {satz}"
def _zaehle_seiten_befunde(diff: dict) -> dict:
"""Zählt die Befund-Kategorien über alle geänderten Seiten zusammen."""
z = {
"hidden": 0, "meta_refresh": 0, "canonical": 0,
"comment_links": 0, "scripts": 0, "grosser_text": 0,
}
for pd in diff.get("page_diffs", []):
if pd.get("new_hidden_content"):
z["hidden"] += 1
if pd.get("new_meta_refresh"):
z["meta_refresh"] += 1
if pd.get("canonical_changed"):
z["canonical"] += 1
if pd.get("new_comment_links"):
z["comment_links"] += 1
if pd.get("new_inline_scripts"):
z["scripts"] += 1
if pd.get("added_chars", 0) > 200:
z["grosser_text"] += 1
return z
# Laienverständliche Beschreibung je KI-Kategorie.
_KI_KATEGORIE_TEXT = {
"pornography": "sexuell anstößige Inhalte",
"propaganda": "politische Propaganda",
"defamation_illegal": "möglicherweise strafbare oder verleumderische Aussagen",
"hidden_spam": "versteckten Werbe-/Spam-Text",
"off_topic_commercial": "Werbung, die nicht zum Thema Ihrer Website passt",
"contradiction": "in sich widersprüchliche Aussagen",
}
def klartext_befunde(
diff: dict | None = None,
cloak_diff: dict | None = None,
asset_diff: dict | None = None,
ext_links: dict | None = None,
ai_result: dict | None = None,
) -> list[str]:
"""
Erzeugt eine Liste laienverständlicher Sätze aus den strukturierten Befunden.
Leere Befunde → eine beruhigende Meldung.
"""
diff = diff or {}
saetze: list[str] = []
# --- Eindeutige Angriffszeichen zuerst ---
if diff.get("suspicious_filenames"):
saetze.append(
"Es wurde auf eine Datei mit verdächtigem Namen verlinkt, wie sie Angreifer "
"für Schadprogramme verwenden. Das ist ein ernstes Warnzeichen."
)
if cloak_diff:
bot_links = sum(len(f.get("extra_links", [])) for f in cloak_diff.get("findings", []))
bot_text = any(f.get("extra_text_chars", 0) > 100 for f in cloak_diff.get("findings", []))
if bot_links:
saetze.append(
"Ihre Website zeigt der Google-Suchmaschine andere Links als normalen "
"Besuchern. Das ist eine typische Methode von Spam-Angriffen."
)
elif bot_text:
saetze.append(
"Ihre Website zeigt der Google-Suchmaschine zusätzlichen Text, den normale "
"Besucher nicht sehen. Bitte prüfen lassen."
)
# --- Verdächtige Inhaltsänderungen ---
z = _zaehle_seiten_befunde(diff)
if z["hidden"]:
saetze.append(
f"Auf {z['hidden']} Seite(n) wurde unsichtbarer Text oder unsichtbare Links "
"gefunden. So versteckt man üblicherweise Spam vor dem Auge des Besuchers."
)
if z["meta_refresh"]:
saetze.append(
f"{z['meta_refresh']} Seite(n) leiten Besucher jetzt automatisch auf eine "
"andere Adresse weiter. Wenn Sie das nicht selbst eingerichtet haben, ist es verdächtig."
)
if z["scripts"]:
saetze.append(
f"Auf {z['scripts']} Seite(n) wurde neuer, auffälliger Programmcode eingebaut. "
"Bitte von Ihrem Dienstleister prüfen lassen."
)
if z["comment_links"]:
saetze.append(
f"Auf {z['comment_links']} Seite(n) wurden versteckte Links im Quelltext gefunden."
)
if z["canonical"]:
saetze.append(
f"Bei {z['canonical']} Seite(n) wurde die hinterlegte 'Original-Adresse' geändert. "
"Damit versuchen Angreifer manchmal, Ihre Seite bei Google umzuleiten."
)
# --- Fremde Adressen / neue Seiten ---
neue_domains = diff.get("new_external_domains", [])
if neue_domains:
beispiele = ", ".join(neue_domains[:3])
mehr = f" (und {len(neue_domains) - 3} weitere)" if len(neue_domains) > 3 else ""
saetze.append(
f"Es tauchen Verweise auf neue fremde Internet-Adressen auf: {beispiele}{mehr}. "
"Das kann harmlos sein (z. B. ein neuer Partner-Link) oder ein Hinweis auf einen Eingriff."
)
neue_seiten = diff.get("new_internal_urls", [])
if neue_seiten:
saetze.append(
f"Es sind {len(neue_seiten)} neue Unterseite(n) hinzugekommen. Haben Sie oder Ihr "
"Dienstleister die angelegt?"
)
fehlende = diff.get("missing_internal_urls", [])
if fehlende:
saetze.append(
f"{len(fehlende)} Seite(n), die es früher gab, sind nicht mehr auffindbar."
)
if z["grosser_text"]:
saetze.append(
f"Auf {z['grosser_text']} Seite(n) wurde ein größerer Textblock neu hinzugefügt."
)
# --- Datei-Veränderungen (Assets) ---
if asset_diff and asset_diff.get("changed"):
n = len(asset_diff["changed"])
saetze.append(
f"{n} Datei(en) (z. B. Bilder, Skripte oder Stylesheets) haben sich verändert. "
"Nach einem geplanten Update ist das normal — sonst bitte prüfen lassen."
)
# --- Kaputte Links (immer melden, bis behoben) ---
neue_kaputt = diff.get("new_broken_urls", [])
bekannte_kaputt = diff.get("known_broken_urls", [])
alle_kaputt = len(neue_kaputt) + len(bekannte_kaputt)
if alle_kaputt:
saetze.append(
f"{alle_kaputt} interne Link(s) führen ins Leere (Seite nicht gefunden). "
"Bitte prüfen und den Verweis auf der betroffenen Seite korrigieren."
)
if ext_links and ext_links.get("broken"):
saetze.append(
f"{ext_links['broken']} Link(s) zu anderen Websites funktionieren nicht mehr. "
"Das ist meist harmlos — die fremde Seite wurde gelöscht oder umbenannt."
)
# --- KI-Inhaltsanalyse ---
if ai_result and ai_result.get("findings"):
text_funde = [f for f in ai_result["findings"] if f.get("kind") == "text"]
bild_funde = [f for f in ai_result["findings"] if f.get("kind") == "image"]
if text_funde:
kategorien = sorted({
_KI_KATEGORIE_TEXT[f["category"]]
for f in text_funde if f.get("category") in _KI_KATEGORIE_TEXT
})
beispiele = (" (z. B. " + ", ".join(kategorien[:3]) + ")") if kategorien else ""
saetze.append(
f"Eine automatische Inhaltsprüfung hat auf {len(text_funde)} Seite(n) Text "
f"gefunden, der auffällig ist{beispiele}. "
"Bitte schauen Sie sich die betroffenen Seiten an."
)
if bild_funde:
saetze.append(
f"Auf {len(bild_funde)} Bild(ern) wurden möglicherweise problematische "
"Inhalte erkannt. Bitte prüfen Sie die betroffenen Bilder."
)
if ai_result and ai_result.get("unchecked"):
n = len(ai_result["unchecked"])
saetze.append(
f"{n} Inhalt(e) konnten nicht automatisch geprüft werden, weil der "
"Prüfdienst gerade nicht erreichbar war. „Nicht geprüft“ bedeutet nicht "
"„unbedenklich“ — bitte beim nächsten Lauf erneut prüfen."
)
if not saetze:
saetze.append("Es hat sich nichts Verdächtiges verändert.")
return saetze
def was_tun(level: str, target: str = "") -> list[str]:
"""Konkrete, laienverständliche Handlungsempfehlung je Level."""
if level == "red":
return [
"Öffnen Sie Ihre Website im Browser und schauen Sie, ob etwas Fremdes zu sehen ist.",
"Informieren Sie die Person oder Firma, die Ihre Website betreut.",
"Geben Sie nichts frei, solange der Verdacht nicht geklärt ist.",
]
if level == "yellow":
return [
"Schauen Sie sich die unten genannten Punkte einmal an.",
"Wenn die Änderung von Ihnen oder Ihrem Dienstleister gewollt war, ist alles in Ordnung.",
"Gewollte Änderungen bestätigen Sie mit: " + scanner_cmd(target, "approve --all"),
]
return ["Sie müssen nichts tun."]