Ziel: ein einziger täglicher Befehl genügt, jede Meldung ist auch ohne IT-Wissen verständlich. Auto-Modus (ein Cron-Job genügt für alles): - data/state.json merkt sich, wann die Wochen-Prüfungen zuletzt liefen (baseline.py: load_state/save_state/is_check_due/mark_check_run) - `scan` führt fällige Zusatzprüfungen (Tarnung/externe Links/Dateien) automatisch mit aus; Gesamt-Ampel = schlechtestes Teilergebnis; EIN kombinierter Report + EIN Alert - neuer Config-Abschnitt periodic_checks (Default: wöchentlich, an) Klartext (scanner/plain.py): - Ampel 🟢/🟡/🔴 + Sätze ohne Fachbegriffe, aus den strukturierten Befunden - Report: Klartext oben, "Technische Details (für Ihren Dienstleister)" unten - Terminal-Ausgabe und E-Mail (alerter.py) ebenso umgestellt Komfort: - approve --all/--rebuild legt die Datei-Überwachung automatisch mit an (Opt-out: --skip-assets); approve-assets bleibt für den Sonderfall - status zeigt Ampel zuoberst + wann die Wochen-Prüfungen zuletzt liefen - Onboarding-Text in einfacher Sprache, wenn noch kein Vergleichsstand existiert Doku: README + BEDIENUNGSANLEITUNG mit "In 3 Schritten"-Einstieg. Tests: test_state.py, test_plain.py (Fachbegriff-Assertion), Auto-Asset- Baseline-Test. 165 Tests grün. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
179 lines
6.9 KiB
Python
179 lines
6.9 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 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
|
|
|
|
|
|
def klartext_befunde(
|
|
diff: dict | None = None,
|
|
cloak_diff: dict | None = None,
|
|
asset_diff: dict | None = None,
|
|
ext_links: 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 (rein informativ) ---
|
|
neue_kaputt = diff.get("new_broken_urls", [])
|
|
if neue_kaputt:
|
|
saetze.append(
|
|
f"{len(neue_kaputt)} interne Link(s) führen ins Leere (Seite nicht gefunden)."
|
|
)
|
|
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."
|
|
)
|
|
|
|
if not saetze:
|
|
saetze.append("Es hat sich nichts Verdächtiges verändert.")
|
|
|
|
return saetze
|
|
|
|
|
|
def was_tun(level: 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: python -m scanner approve --all",
|
|
]
|
|
return ["Sie müssen nichts tun."]
|