integrity_scanner_fuer_stat.../BEDIENUNGSANLEITUNG.md
Dieter Schlüter 989f5a933f feat: Inhaltsinventar in SQLite — KI-Inhaltsangabe je Objekt (URL + Hash)
Vereinheitlicht den bisherigen JSON-Ledger zu EINEM abfragbaren SQLite-Store
pro Site (data/content_inventory.db): jedes geprüfte Objekt mit Inhalts-Hash,
URL(s), Sicherheits-Verdikt UND neutraler KI-Inhaltsangabe.

- baseline.py: load/save_ai_ledger jetzt SQLite-gestützt (Dict-Schnittstelle
  bleibt → Analyzer unverändert). Tabellen objects + object_urls. Einmalige
  Migration eines vorhandenen ai_ledger.json → SQLite (.migrated). UPSERT mit
  first_seen-Erhalt; object_urls transaktional ersetzt. Neu: query_inventory,
  export_inventory_csv (beide migrieren failsafe).
- ai_analyzer.py: Antwort-Schema + Prompt um neutrales Feld 'description'
  erweitert (im selben Call, keine Extrakosten); _make_entry speichert es.
- __main__.py: neues Kommando 'inventory' (Übersicht, --search, --kind, --csv).
- Doku: README + Bedienungsanleitung.

Audio/Video sind im Schema (kind) vorbereitet (Phase 2: Extractor + Modalitäten).
bredelar.info real migriert: 245 Objekte (200 Bilder, 45 Texte).

Tests: 266 grün (+6: Round-Trip mit description, Migration, first_seen-Erhalt,
invalidate, Suche/CSV-Export, description landet im Ledger).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 05:12:19 +02:00

40 KiB
Raw Permalink Blame History

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 scan mit. Sie müssen dafür nichts extra einrichten — ein einziger täglicher Aufruf genügt für alles.

Beim ersten approve --all wird 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

  1. Grundprinzip
  2. Voraussetzungen und Installation
  3. Erstinbetriebnahme
  4. Täglicher Betrieb
  5. Befehle im Überblick
  6. Ergebnis-Level und Exit-Codes
  7. Berichte lesen
  8. Änderungen freigeben (approve)
  9. Vorgehen bei einem Alarm
  10. Automatisierung per Cron-Job
  11. E-Mail-Benachrichtigung einrichten
  12. Konfigurationsreferenz
  13. Externe Domains verwalten
  14. Rausch-Unterdrückung konfigurieren
  15. Was der Scanner erkennt
  16. Verzeichnisstruktur
  17. Cloaking-Erkennung
  18. Externe Links auf Erreichbarkeit prüfen
  19. Binärdateien und Assets prüfen
  20. 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 approve freigegeben 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.yaml eintragen (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
inventory KI-Inhaltsinventar (jedes Objekt: URL, Hash, Inhaltsangabe) anzeigen
inventory --search <text> Inventar nach Inhaltsangabe/URL/Kategorie durchsuchen
inventory --csv <pfad> Inventar als CSV exportieren
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 019 0 Keine Auffälligkeiten
YELLOW 2059 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

  1. Diffs sichten: python -m scanner https://ihre-website.de report --diff-only
  2. Vollständigen Report lesen: python -m scanner https://ihre-website.de report
  3. Markierte URLs im Browser aufrufen und Quelltext prüfen
  4. Ist die Änderung legitim?
    • Ja: python -m scanner https://ihre-website.de approve --url <URL> --note "Grund"
    • Nein: Abschnitt Angriff lesen

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

  1. Nicht sofort approve aufrufen — das würde den kompromittierten Zustand als neue Baseline festschreiben.
  2. Diffs sichten: python -m scanner https://ihre-website.de report --diff-only
  3. Report sichern: cp -r ihre-website.de/reports/$(ls ihre-website.de/reports/ | tail -1) ~/alarm-$(date +%Y%m%d)/
  4. Website im Browser aufrufen und nach sichtbaren Fremdinhalten suchen.
  5. Quelltext der betroffenen Seiten prüfen (insbesondere display:none-Bereiche).
  6. Hoster kontaktieren, SSH-/FTP-Zugriffslogs prüfen.
  7. Dateien mit Backup vergleichen (diff oder rsync --dry-run).
  8. 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:

  1. Ersten Scan starten — Verzeichnis und config.yaml werden automatisch angelegt:
    python -m scanner https://neue-seite.de init
    
  2. Externe Domains prüfen und in neue-seite.de/config/allowed_external.yaml eintragen
  3. Baseline freigeben: python -m scanner https://neue-seite.de approve --all --note "Erststand geprüft"
  4. Weiteren Cron-Eintrag ergänzen — mit 15 Minuten Abstand zum vorherigen (z.B. 03:45 Uhr)
  5. neue-seite.de/ in .gitignore eintragen

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:

  1. Domain im Browser prüfen — gehört sie zur Website?
  2. Wenn legitim: in config/allowed_external.yaml eintragen
  3. 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 4060
Veränderungen in JS/CSS/Bildern (Inhalt) Ja, mit check-assets Score 2040
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-Agent gesetzt 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.


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ür breaker_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.

Vollständige Abdeckung (kein blinder Fleck)

Eine KI-Bildanalyse ist nur sinnvoll, wenn alle Bilder geprüft werden — sonst bliebe ein Bild jenseits eines Limits dauerhaft ungeprüft (z. B. ein eingeschleustes strafbares Logo auf einer ansonsten stabilen Seite). Deshalb gilt:

  • Geänderte/neue Inhalte werden IMMER sofort geprüft — ungeachtet jeder Drossel. Fügt jemand ein Bild auf einer Seite ein, ändert sich diese Seite → das Bild wird im selben Scan analysiert.
  • max_pages_per_scan / max_images_per_scan drosseln nur den historischen Altbestand, und ihr Default ist 0 = unbegrenzt: Der erste Scan deckt den kompletten Bestand ab. Dank des Hash-Caches ist das eine einmalige Ausgabe (wenige Cent); Folge-Scans prüfen nur noch Neues.
  • Ein positiver Wert ist nur für sehr große Sites (z. B. 10.000 Bilder) gedacht, um die erste Volldurchsicht über mehrere Scans zu strecken. Selbst dann werden geänderte/neue Inhalte weiter sofort geprüft.

Initiale Einrichtung: Beim ersten vollen Durchlauf markiert die KI auch legitime Inhalte (z. B. Marken-/Partnerlogos auf Firmenseiten) — diese werden einmalig mit ai-dismiss --hash <fp> akzeptiert. Da die Quittung am Byte-Hash hängt, wird ein später ausgetauschtes Bild (andere Bytes → neuer Hash) automatisch neu geprüft und nicht stillschweigend mitakzeptiert.

Byte-Tausch bekannter Bilder (z. B. ein bestehendes Logo wird heimlich gegen ein strafbares ausgetauscht): Bekannte Bilder werden aus Effizienzgründen nicht bei jedem Scan neu geladen (URL→Byte-Hash gemerkt). Damit ein Austausch trotzdem nicht durchrutscht, ist die Datei-Prüfung (check-assets) an die KI gekoppelt: Erkennt sie eine geänderte Bilddatei, wird deren URL aus der Hash-Erinnerung entfernt → die KI lädt das Bild neu und bewertet seinen Inhalt neu (neue Bytes → neuer Hash → Analyse). So greifen Integritäts- und Inhaltsprüfung ineinander.

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.

Inhaltsinventar (SQLite)

Jedes geprüfte Objekt (Seitentext, Bild; später Audio/Video) wird mit Inhalts-Hash, URL(s), Sicherheits-Verdikt und einer neutralen KI-Inhaltsangabe in data/content_inventory.db (SQLite) gespeichert — eine durchsuchbare Quelle der Wahrheit pro Site.

python -m scanner https://ihre-website.de inventory                 # Übersicht
python -m scanner https://ihre-website.de inventory --search kloster # Volltextsuche
python -m scanner https://ihre-website.de inventory --kind image     # nur Bilder
python -m scanner https://ihre-website.de inventory --csv inventar.csv

Die Datenbank lässt sich auch direkt mit jedem SQLite-Werkzeug abfragen (Tabellen objects und object_urls). Ein bereits vorhandenes ai_ledger.json (Vorgängerformat) wird beim ersten Zugriff automatisch nach SQLite migriert und als ai_ledger.json.migrated abgelegt.

Hinweis: Das description-Feld füllt sich erst, wenn ein Objekt (neu/geändert) analysiert wird. Objekte, die vor Einführung der Inhaltsangabe geprüft wurden, haben zunächst eine leere Beschreibung; sie wird beim nächsten Re-Check des jeweiligen Inhalts ergänzt.