Der reale Erst-Scan dauerte ~10 min, weil pro Kandidat sequenziell erst die rate-limited Free-Modelle (429) probiert wurden. Zwei Hebel beheben das: 1. ModelRouter: programmatische Telemetrie statt LLM-Orchestrator. Pro Modell Erfolg/Latenz; ausgefallene/rate-limited Modelle bekommen per Circuit-Breaker einen Cooldown und werden übersprungen, das schnellste gesunde Modell zuerst. Failsafe-Pruning toter Slugs über OpenRouter /models (24h-Cache). State persistent in data/ai_router_state.json. 2. Parallelisierung: ThreadPoolExecutor mit konfigurierbarer max_concurrency (Default 8, I/O-gebunden → an API-Rate-Limits gebunden, nicht an CPU-Kerne). Klassifikationen laufen nebenläufig, Merge im Hauptthread (keine Locks), findings deterministisch sortiert. Fingerprint-Gruppierung: identischer Inhalt wird nur einmal klassifiziert, Funde aber für alle URLs emittiert. Realtest bredelar.info: Frisch-Scan von ~10 min auf 1:01 min; Breaker öffnete 14× (Free-Modelle übersprungen), alle Verdikte von gemini-2.5-flash-lite. Config: max_concurrency, breaker_failure_threshold, breaker_cooldown_seconds, refresh_models. Tests: 251 grün (+12: Router-Reihung/Breaker/Pruning-failsafe, parallel==sequenziell, Dedup, deterministische Reihenfolge). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
37 KiB
Bedienungsanleitung — Website Integrity Scanner
Überwachungswerkzeug für statische Websites gegen SEO-Spam und unbefugte Manipulationen. Der Scanner läuft lokal im Verantwortungsbereich des Betreibers.
Für Einsteiger – in 3 Schritten
Sie brauchen kein IT-Wissen. Das Programm zeigt eine Ampel und erklärt alles in einfachen Worten.
Schritt 1 — Ersten Scan durchführen (Scanner legt das Zielverzeichnis automatisch an):
python -m scanner https://ihre-website.de init
Schritt 2 — Erststand speichern (einmalig, wenn die Website in Ordnung ist):
python -m scanner https://ihre-website.de approve --all --note "Erststand geprüft"
Schritt 3 — täglich prüfen (am besten automatisch per Cron, siehe Abschnitt 10):
python -m scanner https://ihre-website.de scan
Bei 🟡 oder 🔴 nachschauen:
python -m scanner https://ihre-website.de report
Was die Ampel bedeutet:
| Ampel | Bedeutung | Was tun? |
|---|---|---|
| 🟢 | Alles in Ordnung | Nichts. |
| 🟡 | Bitte nachschauen | Bericht lesen; wenn die Änderung gewollt war, mit approve --all bestätigen. |
| 🔴 | Achtung | Website ansehen und die Person/Firma informieren, die Ihre Website betreut. |
Wichtig: Die selteneren Prüfungen (Tarnung, externe Links, Dateien) laufen automatisch einmal pro Woche beim täglichen
scanmit. Sie müssen dafür nichts extra einrichten — ein einziger täglicher Aufruf genügt für alles.Beim ersten
approve --allwird zusätzlich die Datei-Überwachung automatisch eingerichtet (Bilder, Skripte, Stylesheets). Auch dafür ist kein weiterer Schritt nötig.
Alles Weitere in dieser Anleitung richtet sich an Fortgeschrittene und an Ihren Dienstleister.
Inhaltsverzeichnis
- Grundprinzip
- Voraussetzungen und Installation
- Erstinbetriebnahme
- Täglicher Betrieb
- Befehle im Überblick
- Ergebnis-Level und Exit-Codes
- Berichte lesen
- Änderungen freigeben (approve)
- Vorgehen bei einem Alarm
- Automatisierung per Cron-Job
- E-Mail-Benachrichtigung einrichten
- Konfigurationsreferenz
- Externe Domains verwalten
- Rausch-Unterdrückung konfigurieren
- Was der Scanner erkennt
- Verzeichnisstruktur
- Cloaking-Erkennung
- Externe Links auf Erreichbarkeit prüfen
- Binärdateien und Assets prüfen
- KI-gestützte Inhaltsanalyse
1. Grundprinzip
Der Scanner arbeitet nach dem Baseline-Diff-Prinzip:
Initiale Sichtprüfung → Baseline festlegen
↓
Täglicher Scan
↓
Aktueller Zustand ──vergleiche── genehmigte Baseline
↓
Abweichungen → Score → GREEN / YELLOW / RED
- Die Baseline ist ein einmal manuell geprüfter und freigegebener Referenzzustand. Sie wird niemals automatisch aktualisiert.
- Jede Abweichung erhält Punkte. Der Gesamt-Score entscheidet über das Alarm-Level.
- Legitime Änderungen (z.B. neuer Artikel) müssen explizit per
approvefreigegeben werden.
2. Voraussetzungen und Installation
# Python 3.10 oder neuer erforderlich
python3 --version
# Virtuelle Umgebung anlegen und aktivieren
cd /home/dschlueter/Python_Programs/integrity_scanner_fuer_statische_Webseiten
python3 -m venv venv
source venv/bin/activate
# Abhängigkeiten installieren
pip install -r requirements.txt
Alle Befehle müssen aus dem Projektverzeichnis ausgeführt werden. Alternativ steht das
Wrapper-Script scanner.sh zur Verfügung, das automatisch ins Projektverzeichnis wechselt:
./scanner.sh https://ihre-website.de scan
# oder nach Ablage in ~/bin/:
scanner https://ihre-website.de scan
3. Erstinbetriebnahme
Die URL-Syntax legt das Zielverzeichnis beim ersten Aufruf automatisch an — kein manuelles Anlegen von Verzeichnissen oder Konfigurieren von Pfaden nötig.
Schritt 1: Ersten Crawl durchführen
python -m scanner https://ihre-website.de init
Der Scanner legt beim ersten Aufruf automatisch ihre-website.de/ mit einer
angepassten config.yaml an und beginnt sofort mit dem Crawl.
Die Ausgabe zeigt:
- alle gefundenen internen URLs
- alle externen Domains (mit Hinweis, ob in der Whitelist)
- potenzielle Auffälligkeiten (Hidden Content, Meta-Refresh, verdächtige Scripts)
- Crawl-Fehler (Seiten, die nicht erreichbar waren)
Außerdem erscheint ein Hinweis, welche Zeilen in .gitignore noch einzutragen sind.
Schritt 2: Ausgabe manuell prüfen
Wichtig vor dem nächsten Schritt:
- Alle externen Domains prüfen. Legitime Domains in
ihre-website.de/config/allowed_external.yamleintragen (siehe Abschnitt 13). - Auffälligkeiten im Browser nachschauen.
Schritt 3: Baseline freigeben
Nur wenn alles sauber ist:
python -m scanner https://ihre-website.de approve --all --note "Initiale Baseline, geprüft am $(date +%Y-%m-%d)"
Ab jetzt ist der tägliche Betrieb möglich.
4. Täglicher Betrieb
Manueller Scan
python -m scanner https://ihre-website.de scan
Ergebnis erscheint direkt im Terminal. Der vollständige Report liegt in ihre-website.de/reports/.
Automatischer Scan (Cron)
crontab -e
Je eine Zeile pro überwachter Website einfügen (mit leichtem Zeitversatz):
# bredelar.info — täglich 03:00 Uhr
0 3 * * * cd /home/dschlueter/Python_Programs/integrity_scanner_fuer_statische_Webseiten && python -m scanner --config bredelar.info/config.yaml scan >> bredelar.info/logs/cron.log 2>&1
# jamulix.de — täglich 03:15 Uhr
15 3 * * * cd /home/dschlueter/Python_Programs/integrity_scanner_fuer_statische_Webseiten && python -m scanner --config jamulix.de/config.yaml scan >> jamulix.de/logs/cron.log 2>&1
# www.bergbauspuren-bredelar.de — täglich 03:30 Uhr
30 3 * * * cd /home/dschlueter/Python_Programs/integrity_scanner_fuer_statische_Webseiten && python -m scanner --config www.bergbauspuren-bredelar.de/config.yaml scan >> www.bergbauspuren-bredelar.de/logs/cron.log 2>&1
Danach prüfen: crontab -l
Der Exit-Code (0/1/2) ist für Cron-basierte Alarmierung nutzbar (z.B. mit cronitor oder
systemd-Timern mit OnFailure=).
5. Befehle im Überblick
Alle Befehle werden mit python -m scanner <befehl> aufgerufen.
Globale Optionen: --config path/to/config.yaml, --verbose (-v).
| Befehl | Zweck |
|---|---|
init |
Erstcrawl, Ausgabe aller Funde, noch kein Baseline-Update |
crawl |
Crawlt die Website, speichert Snapshot (kein Vergleich) |
check |
Vergleicht letzten Snapshot mit Baseline, erstellt Report |
scan |
crawl + check in einem Schritt (für den Cron-Betrieb) |
approve --all |
Alle neuen Snapshot-URLs in die Baseline übernehmen |
approve --url URL |
Einzelne URL freigeben |
approve --rebuild |
Gesamte Baseline ersetzen (nach großem Redesign) |
report |
Letzten Report im Terminal anzeigen |
report --diff-only |
Nur Textdiffs geänderter Seiten — farbig, kompakt |
report --format json |
Letzten Report als JSON anzeigen |
status |
Baseline-Datum, letzter Scan, aktueller Risk-Level |
cloak-check |
Doppel-Crawl: Browser-UA vs. Googlebot (Cloaking-Erkennung) |
check-ext-links |
Externe Links auf Erreichbarkeit prüfen (4xx/5xx) |
check-assets |
Binärdateien (JS/CSS/Bilder) auf Inhaltsänderung prüfen |
approve-assets |
Aktuelle Asset-Hashes als neue Baseline setzen |
test-alert |
Test-E-Mail senden ohne echten Scan — prüft SMTP-Konfiguration |
ai-dismiss --all |
KI-Funde als geprüft/akzeptiert quittieren (Fehlalarm) |
Beispiele
# Einzelne Seite freigeben (z.B. nach Impressums-Update)
python -m scanner approve --url https://bredelar.info/impressum/ --note "Impressum aktualisiert Juni 2026"
# Alle Änderungen auf einmal freigeben (nur nach vollständiger Prüfung!)
python -m scanner approve --all --note "Redesign vom 2026-06-12 freigegeben"
# Baseline nach komplettem Umbau neu aufbauen
python -m scanner approve --rebuild --note "Komplettes Redesign v2, alle Seiten neu geprüft"
# Bestimmten Snapshot freigeben (statt den neuesten)
python -m scanner approve --all --snapshot data/snapshots/20260612_030000
# Aktuellen Stand prüfen
python -m scanner status
# Letzten Report anzeigen
python -m scanner report
6. Ergebnis-Level und Exit-Codes
| Level | Score | Exit-Code | Bedeutung |
|---|---|---|---|
| GREEN | 0–19 | 0 |
Keine Auffälligkeiten |
| YELLOW | 20–59 | 1 |
Warnung — manuelle Prüfung empfohlen |
| RED | ≥ 60 | 2 |
Alarm — sofortige Prüfung notwendig |
Die Schwellenwerte sind in config.yaml unter thresholds anpassbar.
7. Berichte lesen
Reports liegen in reports/YYYYMMDD_HHMMSS/ als report.md und report.json.
# Neuesten Report im Terminal lesen
python -m scanner report
# Nur Diffs geänderter Seiten anzeigen (farbig, kompakt)
python -m scanner report --diff-only
# Direkt die Datei öffnen
ls reports/
cat reports/20260612_030000/report.md
--diff-only zeigt ausschließlich was sich verändert hat: Textdiffs farbig (+ grün,
- rot, @@ cyan), neue/fehlende URLs, Link-Änderungen, Hidden Content, Meta-Refresh.
Farben werden automatisch deaktiviert wenn die Ausgabe in eine Pipe oder Datei umgeleitet wird.
Aufbau des Reports
| Abschnitt | Inhalt |
|---|---|
| Zusammenfassung | Kennzahlen auf einen Blick |
| Risikobewertung | Alle Gründe für den Score (was hat wie viele Punkte ausgelöst) |
| Neue externe Domains | Domains, die bisher nicht in der Baseline waren |
| Neue interne URLs | Seiten, die neu dazugekommen sind |
| Fehlende URLs | Seiten, die in der Baseline stehen, jetzt aber nicht mehr gefunden wurden |
| Geänderte Seiten | Detailansicht: Textänderungen, Link-Diffs, Meta-Änderungen |
| Kaputte Links | [NEU] = noch nicht in Baseline, [bekannt] = schon bekannt, wird nicht bewertet |
| Unerwartetes JSON-LD | Strukturierte Daten mit unbekanntem @type |
| Verdächtige Dateinamen | Links auf Dateien mit Webshell-typischen Namen |
| Whitelist-Verstösse | Externe Domains, die nicht in allowed_external.yaml stehen |
| Fehlende Security-Header | Informativ (kein Score) — nur auf HTTP-200-Seiten |
| Crawl-Fehler | URLs, die der Crawler nicht laden konnte |
| Nächste Schritte | Abhängig vom Level: konkrete Handlungsempfehlungen |
8. Änderungen freigeben (approve)
Wann approve nötig ist:
- Nach einer legitimen Inhaltsänderung (neuer Artikel, aktualisiertes Impressum, ...)
- Nach einem Redesign
- Nach Bereinigung eines echten Angriffs
Einzelne URL freigeben
Wenn nur eine bestimmte Seite geändert wurde:
python -m scanner approve --url https://bredelar.info/veranstaltungen/ \
--note "Veranstaltungskalender aktualisiert"
Alle Änderungen freigeben
Nach Prüfung aller markierten URLs:
python -m scanner approve --all --note "Monatliches Update geprüft und freigegeben"
Achtung: --all übernimmt alles aus dem letzten Snapshot. Nur anwenden, wenn jede
gemeldete Änderung manuell gesichtet wurde.
Komplette Baseline neu aufbauen
Nach einem großen Redesign oder nach Bereinigung eines Angriffs:
python -m scanner approve --rebuild --note "Nach Angriff bereinigt, neues Redesign"
--rebuild ersetzt die gesamte bestehende Baseline durch den aktuellen Snapshot.
9. Vorgehen bei einem Alarm
YELLOW — Warnung
- Diffs sichten:
python -m scanner https://ihre-website.de report --diff-only - Vollständigen Report lesen:
python -m scanner https://ihre-website.de report - Markierte URLs im Browser aufrufen und Quelltext prüfen
- Ist die Änderung legitim?
- Ja:
python -m scanner https://ihre-website.de approve --url <URL> --note "Grund" - Nein: Abschnitt Angriff lesen
- Ja:
RED — Alarm
Der Score ist ≥ 60. Typische Auslöser: neue externe Domain (50 Pkt.) + Hidden Content (40 Pkt.), Webshell-Dateiname (60 Pkt.), Meta-Refresh (40 Pkt.).
Angriff
- Nicht sofort approve aufrufen — das würde den kompromittierten Zustand als neue Baseline festschreiben.
- Diffs sichten:
python -m scanner https://ihre-website.de report --diff-only - Report sichern:
cp -r ihre-website.de/reports/$(ls ihre-website.de/reports/ | tail -1) ~/alarm-$(date +%Y%m%d)/ - Website im Browser aufrufen und nach sichtbaren Fremdinhalten suchen.
- Quelltext der betroffenen Seiten prüfen (insbesondere
display:none-Bereiche). - Hoster kontaktieren, SSH-/FTP-Zugriffslogs prüfen.
- Dateien mit Backup vergleichen (diff oder rsync --dry-run).
- Nach vollständiger Bereinigung:
python -m scanner https://ihre-website.de scan # muss GREEN ergeben python -m scanner https://ihre-website.de approve --rebuild --note "Nach Angriff bereinigt $(date +%Y-%m-%d)"
10. Automatisierung per Cron-Job
Cron-Einträge einrichten
crontab -e
Je eine Zeile pro überwachter Website einfügen — mit Zeitversatz von mindestens 15 Minuten zwischen den einzelnen Scans. Ein Scan lädt alle Seiten einer Website herunter; laufen mehrere Scans gleichzeitig, konkurrieren sie um Netzwerk und CPU und können sich gegenseitig verlangsamen oder die überwachten Server unnötig belasten.
# bredelar.info — täglich 03:00 Uhr
0 3 * * * cd /home/dschlueter/Python_Programs/integrity_scanner_fuer_statische_Webseiten && python -m scanner --config bredelar.info/config.yaml scan >> bredelar.info/logs/cron.log 2>&1
# jamulix.de — täglich 03:15 Uhr
15 3 * * * cd /home/dschlueter/Python_Programs/integrity_scanner_fuer_statische_Webseiten && python -m scanner --config jamulix.de/config.yaml scan >> jamulix.de/logs/cron.log 2>&1
# www.bergbauspuren-bredelar.de — täglich 03:30 Uhr
30 3 * * * cd /home/dschlueter/Python_Programs/integrity_scanner_fuer_statische_Webseiten && python -m scanner --config www.bergbauspuren-bredelar.de/config.yaml scan >> www.bergbauspuren-bredelar.de/logs/cron.log 2>&1
Danach prüfen ob die Einträge gespeichert wurden:
crontab -l
Neue Website hinzufügen:
- Ersten Scan starten — Verzeichnis und config.yaml werden automatisch angelegt:
python -m scanner https://neue-seite.de init - Externe Domains prüfen und in
neue-seite.de/config/allowed_external.yamleintragen - Baseline freigeben:
python -m scanner https://neue-seite.de approve --all --note "Erststand geprüft" - Weiteren Cron-Eintrag ergänzen — mit 15 Minuten Abstand zum vorherigen (z.B. 03:45 Uhr)
neue-seite.de/in.gitignoreeintragen
Log prüfen
tail -50 bredelar.info/logs/cron.log
tail -50 jamulix.de/logs/cron.log
Log-Rotation (optional)
Datei /etc/logrotate.d/integrity-scanner:
/home/dschlueter/Python_Programs/integrity_scanner_fuer_statische_Webseiten/*/logs/*.log {
daily
rotate 30
compress
missingok
notifempty
}
11. E-Mail-Benachrichtigung einrichten
Konfiguration testen ohne echten Scan
python -m scanner https://ihre-website.de test-alert
Sendet eine Test-Nachricht und prüft dabei SMTP-Host, Zugangsdaten und Empfängeradresse. Sinnvoll nach jeder Änderung an der SMTP-Konfiguration.
Optional: test-alert --level red sendet eine Nachricht mit rotem Alarm-Level.
config.yaml anpassen
alerting:
min_level: "yellow" # oder "red" — nur bei rotem Alarm mailen
email:
enabled: true
smtp_host: "mail.linix.de" # SMTP-Server des Providers
smtp_port: 587
smtp_tls: true
smtp_user: "scanner@linix.de"
smtp_password_env: "SCANNER_SMTP_PASSWORD"
from: "scanner@bredelar.info"
to:
- "dieter.schlueter@linix.de"
Passwort als Umgebungsvariable setzen
Niemals das Passwort direkt in config.yaml eintragen. Stattdessen:
# Temporär (bis zum nächsten Login)
export SCANNER_SMTP_PASSWORD="geheimesPasswort"
# Dauerhaft in ~/.bashrc oder ~/.profile
echo 'export SCANNER_SMTP_PASSWORD="geheimesPasswort"' >> ~/.bashrc
Für den Cron-Job die Variable in der Crontab oder in /etc/environment setzen:
SCANNER_SMTP_PASSWORD=geheimesPasswort
0 3 * * * cd /home/dschlueter/...
12. Konfigurationsreferenz
Alle Einstellungen in config.yaml. Fehlende Werte werden aus den Defaults übernommen.
Kern-Einstellungen
| Schlüssel | Standard | Bedeutung |
|---|---|---|
target |
— | Zu überwachende URL (Pflicht) |
user_agent |
integrity-scanner/1.0 |
HTTP User-Agent |
request_timeout |
20 |
Timeout pro Seite in Sekunden |
Crawl-Einstellungen (crawl:)
| Schlüssel | Standard | Bedeutung |
|---|---|---|
max_pages |
200 |
Maximale Anzahl gecrawlter Seiten |
delay_seconds |
1.0 |
Pause zwischen Requests (Server-Schonung) |
skip_extensions |
.jpg, .pdf etc. |
Dateitypen, die nicht gecrawlt werden |
sitemap |
true |
Sitemap.xml auslesen um Seiten ohne eingehende Links zu finden |
Score-Gewichte (scoring:)
| Ereignis | Standard-Punkte |
|---|---|
| Verdächtiger Dateiname (Webshell) | 60 |
| Cloaking: Link nur im Bot-Crawl sichtbar | 60 |
| Neue externe Domain | 50 |
| Hidden Content (CSS-verstecktes Element mit Links/Text) | 40 |
| Meta-Refresh | 40 |
| Cloaking: signifikant mehr Text im Bot-Crawl (>100 Zeichen) | 40 |
Geänderte JavaScript-Datei (check-assets) |
40 |
| Unerwartete Canonical-URL | 35 |
Unerwartetes JSON-LD @type |
35 |
Geänderte CSS-Datei (check-assets) |
30 |
| Neue interne URL | 30 |
| Neues verdächtiges Inline-Script | 30 |
| Link in HTML-Kommentar | 25 |
Cloaking: Vary: User-Agent Header (ohne Inhaltsdiff) |
25 |
| Großer Textblock (>200 Zeichen) | 20 |
| Fehlende interne URL | 20 |
| Neuer kaputte Link (4xx/5xx, nicht in Baseline) | 20 |
Geänderte Bild-/Binärdatei (check-assets) |
20 |
KI-Inhaltsanalyse (nur wenn aktiviert; Gesamt-Level aus KI ist auf Gelb gedeckelt):
| Ereignis (KI) | Standard-Punkte |
|---|---|
Pornografie/sexuell expliziter Inhalt (ai_pornography) |
50 |
Diffamierende/strafbare Aussage (ai_defamation_illegal) |
50 |
Politische Propaganda (ai_propaganda) |
40 |
Versteckter Spam (ai_hidden_spam) |
40 |
Problematischer Bildinhalt — Untergrenze (ai_suspicious_image) |
40 |
Themenfremde Werbung (ai_off_topic_commercial) |
30 |
Widersprüchliche Aussage (ai_contradiction) |
20 |
Alle Werte sind in config.yaml unter scoring: anpassbar.
Alarm-Schwellen (thresholds:)
thresholds:
yellow: 20 # ab hier: YELLOW
red: 60 # ab hier: RED
large_text_block_chars: 200 # ab dieser Zeichenanzahl gilt ein Textblock als "groß"
Erlaubte JSON-LD-Typen (allowed_jsonld_types:)
Alle @type-Werte, die in den strukturierten Daten der Website vorkommen dürfen.
Unbekannte Typen lösen einen Fund aus (35 Punkte). Aktuell konfiguriert:
Organization, WebSite, WebPage, Article, BreadcrumbList, LocalBusiness,
Event, FAQPage u.a.
Security-Header (security_headers:)
Liste der Header, die auf jeder Seite erwartet werden. Fehlende Header werden im Report gemeldet, aber nicht bewertet (kein Score-Einfluss).
13. Externe Domains verwalten
Datei: config/allowed_external.yaml
# Legitime externe Domains — eine pro Zeile
- fonts.googleapis.com
- www.kloster-bredelar.de
Neue externe Domain erscheint im Scanner:
- Domain im Browser prüfen — gehört sie zur Website?
- Wenn legitim: in
config/allowed_external.yamleintragen python -m scanner approve --url <betroffene-Seite>oder--all
Wenn eine Domain nicht in der Liste steht, löst jedes Vorkommen 50 Punkte aus (YELLOW-Alarm, bei weiteren Funden RED).
14. Rausch-Unterdrückung konfigurieren
Manche Inhalte ändern sich automatisch (Datumsangaben, Zeitstempel) und würden sonst bei jedem Scan einen Fund auslösen. Zwei Mechanismen:
CSS-Selektoren ignorieren (ignore_selectors:)
Elemente werden vor dem Diff vollständig aus dem DOM entfernt:
normalization:
ignore_selectors:
- "#last-modified"
- ".timestamp"
- ".cookie-banner"
Datei: direkt in config.yaml oder in config/ignore_rules.yaml (kommentierte Beispiele
dort vorhanden).
Text-Muster ignorieren (ignore_patterns:)
Reguläre Ausdrücke, die im normalisierten Text durch [IGNORED] ersetzt werden:
normalization:
ignore_patterns:
- '\b\d{1,2}\.\s*(?:Januar|Februar|März|April|Mai|Juni|Juli|August|September|Oktober|November|Dezember)\s+\d{4}\b'
- '\b\d{1,2}\.\d{1,2}\.\d{4}\b'
- 'Stand:\s+\S+'
# WordPress Contact Form 7: Platzhalter-Text rotiert bei jedem Seitenaufruf
- 'Kommentar oder Nachricht \* \w+'
Das Muster wird auf beiden Seiten des Vergleichs (Baseline und aktueller Snapshot) angewendet — falsch-positive Alarme durch dynamische CMS-Inhalte werden so unterdrückt.
15. Was der Scanner erkennt
Erkennt der Scanner ...
| Prüfung | Erkannt? | Anmerkung |
|---|---|---|
| Neuen Text auf einer Seite | Ja | Ab 200 Zeichen bewertet |
| Versteckten Text (CSS display:none etc.) | Ja | 40 Punkte |
| Neue externe Domain | Ja | 50 Punkte |
| Meta-Refresh-Weiterleitung | Ja | 40 Punkte |
| Webshell-Dateien (shell.php, c99 etc.) | Ja | 60 Punkte |
| Links in HTML-Kommentaren | Ja | 25 Punkte |
| Geänderte Canonical-URL | Ja | 35 Punkte |
Unbekanntes JSON-LD @type |
Ja | 35 Punkte |
eval(), atob(), XMLHttpRequest in Scripts |
Ja | 30 Punkte |
| Kaputte interne Links (neu) | Ja | 20 Punkte |
| Kaputte interne Links (bekannt) | Ja, informativ | Kein Score |
| Fehlende Security-Header | Ja, informativ | Kein Score |
| Kaputte externe Links (4xx/5xx) | Ja, mit check-ext-links |
Kein Score, nur Report |
| UA-basiertes Cloaking (Googlebot sieht anderen Inhalt) | Ja, mit cloak-check |
Score 40–60 |
| Veränderungen in JS/CSS/Bildern (Inhalt) | Ja, mit check-assets |
Score 20–40 |
| Problematischer Text (Pornografie, Propaganda, Spam, Off-Topic) | Ja, mit KI-Analyse | Gelb; Pornografie/strafbar bei hoher Schwere+Konfidenz → Rot |
| Problematische Bildinhalte + Text im Bild (OCR) | Ja, mit KI-Analyse | Gelb; Pornografie/strafbar bei hoher Schwere+Konfidenz → Rot |
| IP-basiertes Cloaking | Nein | Nur Google Search Console kann das erkennen |
| Serverseitige Code-Änderungen | Nein | Nur sichtbare HTML-Ausgabe |
Grenzen des Scanners
- IP-basiertes Cloaking wird nicht erkannt: Wenn der Server je nach Herkunfts-IP unterschiedliche Inhalte liefert (nur für echte Googlebot-IPs), sieht der Scanner immer den normalen Inhalt. Ergänzung: Google Search Console → URL-Prüfung.
- Serverseitig injizierter Code, der nur unter bestimmten Bedingungen aktiv wird, ist
nur per
cloak-check(UA-Variante) teilweise erkennbar.
16. Verzeichnisstruktur
scanner.sh Wrapper-Script (ausführbar, startet python -m scanner)
meine-seite.de/ Template für neue Targets
ihre-website.de/ Automatisch angelegtes Zielverzeichnis (ein Ordner pro Site)
config.yaml Hauptkonfiguration
config/
allowed_external.yaml Whitelist erlaubter externer Domains
ignore_rules.yaml Kommentierte Beispiele für Rausch-Unterdrückung
data/
baseline/ Freigegebene Referenz (NIEMALS automatisch geändert)
manifest.json Wer/was/wann freigegeben hat
pages/ Pro URL eine JSON-Datei (16-stelliger Hex-Name)
asset_hashes.json SHA-256-Hashes aller freigegebenen Assets
snapshots/ Ein Verzeichnis pro Crawl-Durchlauf
YYYYMMDD_HHMMSS/
manifest.json Zeitstempel, Seitenzahl, Fehler
pages/ Extrahierte Seiten-Daten
logs/
scanner.log Detailliertes Laufzeit-Log
cron.log Ausgabe der Cron-Ausführungen
reports/
YYYYMMDD_HHMMSS/ Normaler Scan-Report
report.md
report.json
YYYYMMDD_HHMMSS_cloak/ Cloaking-Check-Report
YYYYMMDD_HHMMSS_ext-links/ Externer-Links-Report
YYYYMMDD_HHMMSS_assets/ Asset-Check-Report
scanner/ Python-Paket (Quellcode)
tests/ Automatisierte Tests
Tests ausführen
pytest tests/ -v # alle Tests
pytest tests/test_differ.py -v # einzelnes Testmodul
pytest tests/test_differ.py::TestScoreDiff::test_green_for_no_changes -v # einzelner Test
17. Cloaking-Erkennung
Cloaking bedeutet: der Server liefert Suchmaschinen-Bots absichtlich anderen Inhalt als normalen Besuchern — ein klassisches Parasite-SEO-Angriffsmuster.
Befehl
python -m scanner cloak-check
Der Scanner crawlt die Website zweimal nacheinander: einmal mit dem normalen Browser-User-Agent,
einmal als Googlebot/2.1. Unterschiede im Text, in den Links oder im Vary-Header werden
gemeldet.
Scoring
| Fund | Punkte |
|---|---|
| Link nur im Bot-Crawl sichtbar | 60 (RED) |
| Bot sieht >100 Zeichen mehr Text | 40 |
Vary: User-Agent Header ohne Inhaltsdiff |
25 |
Report
Der Report liegt unter reports/<ts>_cloak/report.md und zeigt pro auffälliger Seite:
- ob
Vary: User-Agentgesetzt ist - welche Links nur der Bot sieht
- wie viele Zeichen mehr Text der Bot bekommt
- einen Text-Diff (bis zu 20 Zeilen)
Grenzen
Erkennt nur UA-basiertes Cloaking. IP-basiertes Cloaking (Server prüft die Herkunfts-IP des Crawlers) ist damit nicht erkennbar — dafür ist die Google Search Console das richtige Werkzeug (URL-Prüfung → Live testen).
Wann ausführen?
Läuft automatisch wöchentlich beim täglichen scan mit (steuerbar über
periodic_checks in der config.yaml). Der manuelle Aufruf ist nur bei konkretem
Verdacht nötig.
18. Externe Links auf Erreichbarkeit prüfen
Der tägliche scan erkennt neue externe Domains, prüft aber nicht ob bestehende externe
Links noch funktionieren. Dafür gibt es:
python -m scanner check-ext-links
Was geprüft wird
Alle externen <a>-Links aus dem letzten Snapshot werden per HEAD-Request abgefragt
(Fallback auf GET bei HTTP 405). Gemeldet werden:
- Kaputte Links (HTTP 4xx/5xx) — mit Angabe, auf welchen Seiten sie verlinkt sind
- Verbindungsfehler (Timeout, DNS-Fehler) — ebenfalls mit Quelladressen
Scoring
Kaputte externe Links werden nicht bewertet (kein Score-Einfluss). Das Ergebnis ist rein informativ — externe Links brechen regelmäßig ohne Angriffsbezug.
Exit-Code: 0 wenn alle Links erreichbar, 1 wenn mindestens ein kaputtes oder
fehlendes Link gefunden wurde.
Report
Der Report liegt unter reports/<ts>_ext-links/report.md.
Wann ausführen?
Läuft automatisch wöchentlich beim täglichen scan mit (steuerbar über
periodic_checks in der config.yaml). Ein manueller Aufruf ist nur bei Bedarf nötig.
19. Binärdateien und Assets prüfen
Der tägliche scan erkennt Änderungen im HTML-Text und an Link-URLs, aber nicht ob der
Inhalt einer Bilddatei oder eines JavaScript-Files ausgetauscht wurde. Dafür:
Ersteinrichtung
Die Asset-Baseline wird automatisch beim ersten approve --all angelegt — kein
separater Schritt nötig. Nur wenn die Asset-Baseline manuell neu gesetzt werden soll:
python -m scanner https://ihre-website.de approve-assets --note "Manuelle Baseline nach CDN-Wechsel"
Regelmäßige Prüfung
python -m scanner check-assets
Alle verlinkten JS-, CSS- und Bilddateien aus dem letzten Snapshot werden heruntergeladen und per SHA-256 gehasht. Abweichungen von der gespeicherten Baseline werden gemeldet.
Scoring
| Dateityp | Punkte |
|---|---|
JavaScript (.js) |
40 — YELLOW |
CSS (.css, Stylesheets) |
30 — YELLOW |
| Bilder, PDFs, sonstige | 20 — YELLOW |
| 2× JS geändert | 80 — RED |
Änderungen freigeben
Nach einem legitimen Update (neues CDN-Bundle, neues Logo):
python -m scanner approve-assets --note "CDN-Update nach Redesign Juli 2026"
approve-assets fetcht alle Assets frisch und setzt die neuen Hashes als Baseline.
Report
Der Report liegt unter reports/<ts>_assets/report.md und zeigt pro geänderter Datei:
- den Dateityp
- die ersten 16 Zeichen des alten und neuen SHA-256-Hash
- die Größenänderung in Bytes
Wann ausführen?
Läuft automatisch wöchentlich beim täglichen scan mit (steuerbar über
periodic_checks in der config.yaml). Die Datei-Vergleichsgrundlage wird zudem beim
ersten approve --all automatisch angelegt — approve-assets ist nur für den manuellen
Sonderfall gedacht.
20. KI-gestützte Inhaltsanalyse
Eine optionale Ergänzung zum Integritäts-Kern. Sie prüft Text und Bilder semantisch auf problematische Inhalte, die kein strukturelles Signal hinterlassen:
- sexuell anstößige Inhalte / Pornografie
- politische Propaganda
- diffamierende oder strafbare Aussagen
- versteckter Werbe-/Spam-Text
- Werbung, die thematisch nicht zur Website passt
- in sich widersprüchliche Aussagen
- problematische Bildinhalte und Text in Bildern (OCR)
Die KI ist Ergänzung, nicht Kern: Ist sie deaktiviert (Standard), verhält sich der Scanner exakt wie bisher.
Das Kostengate (warum das günstig ist)
Jeder Text und jedes Bild bekommt einen Fingerabdruck (Hash). Das Ergebnis der KI-Prüfung
wird zu diesem Fingerabdruck in data/ai_ledger.json gespeichert. Bei jedem weiteren Scan
gilt:
- Inhalt unverändert → gleicher Hash → Ergebnis kommt aus dem Cache → keine Abfrage, keine Kosten.
- Inhalt neu oder geändert → eine günstige OpenRouter-Abfrage, Ergebnis wird gespeichert.
Dadurch kann die Analyse bei jedem Scan mitlaufen und kostet an den meisten Tagen 0 €.
Wirkung auf die Ampel
KI-Funde sind grundsätzlich auf Gelb gedeckelt — mit einer bewussten Ausnahme: die
schwerwiegendsten Kategorien (Pornografie, strafbar/diffamierend) lösen bei hoher
Schwere + hoher Konfidenz ROT aus. Alle anderen Kategorien (Spam, themenfremde Werbung,
Propaganda, Widerspruch) bleiben höchstens Gelb. Nur Funde mit ausreichender Sicherheit
(Konfidenz ≥ ai_confidence_min, Standard 0,7) und mindestens mittlerer Schwere werden
überhaupt gewertet — das hält Fehlalarme niedrig.
Die ROT-Eskalation ist konfigurierbar:
ai_analysis:
red_categories: ["pornography", "defamation_illegal"] # nur diese dürfen Rot auslösen
red_min_severity: "high" # mindestens diese Schwere
red_min_confidence: 0.9 # mindestens diese Konfidenz
Mit red_categories: [] bleibt die KI wie früher immer höchstens Gelb. Hintergrund: KI-
Konfidenzen sind nicht perfekt kalibriert, daher dürfen nur die gravierendsten Fälle die
volle ROT-Dringlichkeit („Dienstleister informieren, nichts freigeben") auslösen.
Fehlt der API-Key oder schlägt eine Abfrage fehl, wird die Analyse übersprungen und der Scan läuft unverändert weiter. Die KI kann den normalen Betrieb nie blockieren.
„Nicht geprüft" ≠ „sauber"
Kann ein Inhalt trotz Modell-Eskalation und Wiederholung (max_retries) nicht geprüft
werden, weil der Dienst gerade nicht erreichbar ist, wird er nicht still als unbedenklich
gewertet. Stattdessen erscheint er als ungeprüft im Terminal, im Report und in der E-Mail —
mit URL und dem Hinweis, beim nächsten Lauf erneut zu prüfen. Über unchecked_level steuern
Sie die Schärfe:
ai_analysis:
max_retries: 1 # Wiederholungen der Kette bei Komplettausfall
unchecked_level: "warn" # "warn" = nur Hinweis | "yellow" = Level auf mindestens Gelb anheben
Für hohe Sicherheitsansprüche empfiehlt sich "yellow": Dann fällt jeder nicht prüfbare
Inhalt sofort als gelbe Warnung auf, statt nur als Notiz zu erscheinen.
Aktivieren
ai_analysis:
enabled: true
api_key_env: "OPENROUTER_API_KEY"
site_context: "Heimat- und Vereinswebsite über die Bergbaugeschichte in Bredelar."
ai_confidence_min: 0.7
attempt_timeout: 30 # Zeitlimit je Modell-Versuch → eskaliert bei Langsamkeit
text:
enabled: true
models: # Kette: Stufe 1+2 free, Stufe 3 günstig bezahlt
- "qwen/qwen3-next-80b-a3b-instruct:free"
- "meta-llama/llama-3.3-70b-instruct:free"
- "google/gemini-2.5-flash-lite"
min_chars: 200 # nur Seitentexte ab dieser Größe
max_pages_per_scan: 20 # Kostendeckel pro Lauf
image:
enabled: true
models: # multimodal: Inhalt + OCR
- "google/gemma-4-31b-it:free"
- "nvidia/nemotron-nano-12b-v2-vl:free"
- "google/gemini-2.5-flash-lite"
max_images_per_scan: 15
Wichtig — site_context: Die KI bewertet die thematische Passung zur hier
beschriebenen Website, nicht einzelne Schlüsselwörter. Ein Angreifer kann Heimat- oder
Ortsbegriffe einstreuen, um simple Wortlisten zu täuschen — gegen die semantische
Themenprüfung hilft das nicht. Je präziser der site_context, desto besser die Trefferquote.
API-Key setzen
Das Passwort/Token niemals in config.yaml eintragen, nur als Umgebungsvariable:
# Dauerhaft in ~/.bashrc
echo 'export OPENROUTER_API_KEY="sk-or-..."' >> ~/.bashrc
Für den Cron-Betrieb die Variable wie bei SCANNER_SMTP_PASSWORD in der Crontab oder in
/etc/environment hinterlegen.
Fehlalarm quittieren
Wurde legitimer Inhalt fälschlich als auffällig markiert:
python -m scanner https://ihre-website.de ai-dismiss --all # alle offenen Funde
python -m scanner https://ihre-website.de ai-dismiss --hash <fp> # gezielt einen Fund
Der Fingerabdruck <fp> steht im Report bei jedem KI-Fund. Ein approve --all oder
approve --url quittiert die zugehörigen KI-Funde automatisch mit (freigegebener Inhalt
gilt als geprüft).
Modelle (OpenRouter) und Eskalation
Pro Modalität wird eine Modell-Kette mit zweifacher Eskalation abgearbeitet. Schlägt ein
Modell fehl (Fehler, Rate-Limit) oder antwortet es langsamer als attempt_timeout Sekunden,
wird automatisch zur nächsten Stufe gewechselt. Stufe 1 und 2 sind kostenlose Modelle, Stufe 3
ist ein günstiges Bezahlmodell — so bleibt die Prüfung in der Regel kostenlos und ist trotzdem
zuverlässig. Welche Stufe das Verdikt geliefert hat, wird im Ledger und Report festgehalten.
| Zweck | Stufe 1 (free) | Stufe 2 (free) | Stufe 3 (bezahlt) |
|---|---|---|---|
| Text/Semantik | qwen3-next-80b-a3b-instruct:free |
llama-3.3-70b-instruct:free |
gemini-2.5-flash-lite |
| Bild + OCR | gemma-4-31b-it:free |
nemotron-nano-12b-v2-vl:free |
gemini-2.5-flash-lite |
Hinweis: Kostenlose Modelle liefern nicht immer ein gültiges Ergebnis (sie unterstützen das strikte JSON-Format nicht durchgehend). In dem Fall greift automatisch die nächste Stufe — das zuverlässige Bezahlmodell auf Stufe 3 fängt solche Fälle ab.
Adaptiver Modell-Router und Parallelität
Statt die Kette stur von vorn abzuarbeiten, lernt der Scanner während des Laufs, welche Modelle gerade funktionieren:
- Circuit-Breaker: Antwortet ein Modell wiederholt mit Fehler/Rate-Limit (
429) oder zu langsam, wird es fürbreaker_cooldown_secondsübersprungen — keine verschwendeten Versuche mehr an gerade tote Free-Modelle. Das schnellste gesunde Modell kommt zuerst dran. - Live-Abgleich (
refresh_models): Einmal täglich gleicht der Router die Modell-Liste mit OpenRouter ab; nicht mehr existierende Slugs fallen automatisch raus (failsafe — bei Netzfehler bleibt alles wie gehabt). - Parallelität (
max_concurrency): Mehrere Inhalte werden gleichzeitig geprüft. Die KI-Calls warten fast nur auf die API-Antwort (Netzwerk), nicht auf die CPU — deshalb ist die Stellgröße an die Rate-Limits der API gebunden, nicht an die CPU-Kerne. Default 8 passt auf einen 4-Kern-vhost genauso wie auf einen 24-Kern-Rechner.
ai_analysis:
max_concurrency: 8 # gleichzeitige API-Anfragen
breaker_failure_threshold: 2 # Fehler, bis ein Modell pausiert wird
breaker_cooldown_seconds: 120 # Pausendauer eines ausgefallenen Modells
refresh_models: true # tote Modell-Slugs automatisch aussortieren
Wirkung: Der Erst-Scan einer Site (der den ganzen Bestand prüft) wird deutlich schneller, weil rate-limited Free-Modelle sofort übersprungen werden und die verbleibenden Anfragen nebenläufig laufen.
Audio- und Video-Prüfung sind als abschaltbare Erweiterungspunkte vorbereitet
(ai_analysis.audio / ai_analysis.video, default deaktiviert) und können aktiviert werden,
sobald eine Website solche Mediendateien direkt einbindet.
Status prüfen
python -m scanner https://ihre-website.de status
zeigt bei aktivierter KI, wie viele Inhalte im Cache liegen und wie viele offene Funde es gibt.