Neue Abschnitte 17–19 für Cloaking-Erkennung, externe Link-Prüfung und Asset-Hashing. Befehlstabelle, Scoring-Tabelle, Erkennungsübersicht und Verzeichnisstruktur aktualisiert. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
22 KiB
Bedienungsanleitung — Website Integrity Scanner
Überwachungswerkzeug für die statische Website bredelar.info gegen SEO-Spam und unbefugte Manipulationen. Der Scanner läuft lokal im Verantwortungsbereich des Betreibers.
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
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 (dort liegt config.yaml).
3. Erstinbetriebnahme
Schritt 1: Ersten Crawl durchführen
python -m scanner init
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)
Schritt 2: Ausgabe manuell prüfen
Wichtig vor dem nächsten Schritt:
- Alle externen Domains prüfen. Legitime Domains in
config/allowed_external.yamleintragen (siehe Abschnitt 13). - Auffälligkeiten im Browser nachschauen.
Schritt 3: Baseline freigeben
Nur wenn alles sauber ist:
python -m scanner 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 scan
Ergebnis erscheint direkt im Terminal. Der vollständige Report liegt in reports/.
Automatischer Scan (Cron)
# Cron-Eintrag hinzufügen
crontab -e
Eintrag:
# Website-Scan täglich um 03:00 Uhr
0 3 * * * cd /home/dschlueter/Python_Programs/integrity_scanner_fuer_statische_Webseiten && /home/dschlueter/Python_Programs/integrity_scanner_fuer_statische_Webseiten/venv/bin/python -m scanner scan >> logs/cron.log 2>&1
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 --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 |
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
# Direkt die Datei öffnen
ls reports/
cat reports/20260612_030000/report.md
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
- Report lesen:
python -m scanner report - Markierte URLs im Browser aufrufen und Quelltext prüfen
- Ist die Änderung legitim?
- Ja:
python -m scanner 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.
- Report sichern:
cp -r reports/$(ls 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 scan # muss GREEN ergeben python -m scanner approve --rebuild --note "Nach Angriff bereinigt $(date +%Y-%m-%d)"
10. Automatisierung per Cron-Job
Cron-Eintrag
crontab -e
# Website-Scan täglich um 03:00 Uhr
0 3 * * * cd /home/dschlueter/Python_Programs/integrity_scanner_fuer_statische_Webseiten && /home/dschlueter/Python_Programs/integrity_scanner_fuer_statische_Webseiten/venv/bin/python -m scanner scan >> logs/cron.log 2>&1
Log prüfen
tail -50 logs/cron.log
tail -f logs/scanner.log # detaillierteres Log des Scanners selbst
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
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 |
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 |
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+'
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 |
| 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
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 (approve-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
report.md
report.json
YYYYMMDD_HHMMSS_ext-links/ Externer-Links-Report
report.md
report.json
YYYYMMDD_HHMMSS_assets/ Asset-Check-Report
report.md
report.json
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?
Nicht täglich — der Doppel-Crawl belastet den Server doppelt. Empfehlung: monatlich oder bei konkretem Verdacht.
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?
Wöchentlich oder monatlich reicht. Nicht im täglichen Cron — zu viele Requests an externe Server.
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 (einmalig)
python -m scanner crawl # aktuellen Snapshot anlegen (falls noch keiner vorhanden)
python -m scanner approve-assets # SHA-256-Hashes aller Assets als Baseline speichern
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?
Wöchentlich oder nach bekannten Website-Updates. Nicht täglich — lädt alle Assets herunter und ist entsprechend langsam.