Bisher: Konnte die KI kein Verdikt liefern (alle Modellstufen scheitern),
ging der Inhalt still als grün durch — für ein Sicherheitswerkzeug ein
Falsch-Negativ-Risiko ("konnte nicht prüfen" ≠ "sauber").
Zwei Maßnahmen:
1. Retry: _classify wiederholt die ganze Modell-Kette bei Komplettausfall
bis max_retries (Default 1) mit Backoff — fängt transiente Aussetzer ab.
2. Tracking: nicht prüfbare Inhalte landen in ai_result["unchecked"] (statt
stillem skip) und werden sichtbar in Terminal, Report (Markdown), diff-only
und E-Mail — jeweils mit URL. score_ai_findings nimmt sie als Grund auf;
mit unchecked_level="yellow" heben sie das Level auf mindestens Gelb an
(Default "warn" = nur Hinweis, Ampel unberührt).
Config: max_retries, retry_backoff_seconds, unchecked_level (ai_analysis).
Tests: 230 grün (+ Retry-Recovery, unchecked-Tracking, unchecked_level).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
222 lines
8.9 KiB
Python
222 lines
8.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
|
|
|
|
|
|
# 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) -> 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."]
|