Previously, broken links that were part of the baseline ("known broken")
disappeared from the terminal output (score=0, no Klartext mention).
- differ.py: build reverse link index to record which pages link to
each broken URL; "pages" field added to new_broken/known_broken entries
- plain.py: klartext_befunde() now includes known_broken_urls alongside
new_broken_urls — broken links are always reported until fixed
- __main__.py: report shows "gefunden auf: <page>" for every broken entry,
known entries labelled "[bekannt, noch nicht behoben]"
- 3 new tests (169 total)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
182 lines
7.1 KiB
Python
182 lines
7.1 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 (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."
|
|
)
|
|
|
|
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."]
|