feat: initial implementation of website integrity scanner
Python-Package zur unabhängigen Überwachung statischer Websites gegen
SEO-Spam und unbefugte Manipulationen. Läuft außerhalb des Hosters.
Kernfunktionen:
- Reiner Python-Crawler (bs4+lxml), kein wget
- Baseline-Management mit manuellem Freigabe-Workflow (niemals automatisch)
- Text-/Link-/Meta-Diff mit Risiko-Scoring (grün/gelb/rot)
- Hidden-Content-Erkennung (CSS inline, noscript, Kommentar-Links)
- Whitelist für externe Domains und interne Pfade
- E-Mail + Webhook Alarmierung
- CLI: init, crawl, check, scan, approve, report, status
- 79 Unit-Tests (pytest), alle grün
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 00:51:41 +02:00
|
|
|
"""
|
|
|
|
|
Alert dispatch: e-mail (smtplib) and HTTP webhook.
|
|
|
|
|
|
|
|
|
|
Alerts are only sent when the assessment level meets or exceeds min_level.
|
|
|
|
|
"""
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
import smtplib
|
|
|
|
|
from email.message import EmailMessage
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
|
2026-06-13 03:35:05 +02:00
|
|
|
from .plain import ampel
|
|
|
|
|
from .report import render_markdown
|
feat: Laientauglichkeit — Auto-Modus, Klartext-Ausgabe, einfachere Doku
Ziel: ein einziger täglicher Befehl genügt, jede Meldung ist auch ohne
IT-Wissen verständlich.
Auto-Modus (ein Cron-Job genügt für alles):
- data/state.json merkt sich, wann die Wochen-Prüfungen zuletzt liefen
(baseline.py: load_state/save_state/is_check_due/mark_check_run)
- `scan` führt fällige Zusatzprüfungen (Tarnung/externe Links/Dateien)
automatisch mit aus; Gesamt-Ampel = schlechtestes Teilergebnis;
EIN kombinierter Report + EIN Alert
- neuer Config-Abschnitt periodic_checks (Default: wöchentlich, an)
Klartext (scanner/plain.py):
- Ampel 🟢/🟡/🔴 + Sätze ohne Fachbegriffe, aus den strukturierten Befunden
- Report: Klartext oben, "Technische Details (für Ihren Dienstleister)" unten
- Terminal-Ausgabe und E-Mail (alerter.py) ebenso umgestellt
Komfort:
- approve --all/--rebuild legt die Datei-Überwachung automatisch mit an
(Opt-out: --skip-assets); approve-assets bleibt für den Sonderfall
- status zeigt Ampel zuoberst + wann die Wochen-Prüfungen zuletzt liefen
- Onboarding-Text in einfacher Sprache, wenn noch kein Vergleichsstand existiert
Doku: README + BEDIENUNGSANLEITUNG mit "In 3 Schritten"-Einstieg.
Tests: test_state.py, test_plain.py (Fachbegriff-Assertion), Auto-Asset-
Baseline-Test. 165 Tests grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 11:14:05 +02:00
|
|
|
|
feat: initial implementation of website integrity scanner
Python-Package zur unabhängigen Überwachung statischer Websites gegen
SEO-Spam und unbefugte Manipulationen. Läuft außerhalb des Hosters.
Kernfunktionen:
- Reiner Python-Crawler (bs4+lxml), kein wget
- Baseline-Management mit manuellem Freigabe-Workflow (niemals automatisch)
- Text-/Link-/Meta-Diff mit Risiko-Scoring (grün/gelb/rot)
- Hidden-Content-Erkennung (CSS inline, noscript, Kommentar-Links)
- Whitelist für externe Domains und interne Pfade
- E-Mail + Webhook Alarmierung
- CLI: init, crawl, check, scan, approve, report, status
- 79 Unit-Tests (pytest), alle grün
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 00:51:41 +02:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
_LEVEL_ORDER = {"green": 0, "yellow": 1, "red": 2}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def send_alert(report: dict, cfg: dict) -> None:
|
|
|
|
|
"""Send alert if the report's level is at or above min_level."""
|
|
|
|
|
alerting = cfg.get("alerting", {})
|
|
|
|
|
level = report.get("assessment", {}).get("level", "green")
|
|
|
|
|
min_level = alerting.get("min_level", "yellow")
|
|
|
|
|
|
|
|
|
|
if _LEVEL_ORDER.get(level, 0) < _LEVEL_ORDER.get(min_level, 1):
|
|
|
|
|
logger.debug("Alert suppressed: level=%s < min_level=%s", level, min_level)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
subject = _make_subject(report)
|
|
|
|
|
body = _format_body(report)
|
|
|
|
|
|
|
|
|
|
email_cfg = alerting.get("email", {})
|
|
|
|
|
if email_cfg.get("enabled"):
|
|
|
|
|
_send_email(subject, body, email_cfg)
|
|
|
|
|
|
|
|
|
|
webhook_cfg = alerting.get("webhook", {})
|
|
|
|
|
if webhook_cfg.get("enabled") and webhook_cfg.get("url"):
|
|
|
|
|
_send_webhook(webhook_cfg["url"], report, subject)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Formatting
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def _make_subject(report: dict) -> str:
|
feat: Laientauglichkeit — Auto-Modus, Klartext-Ausgabe, einfachere Doku
Ziel: ein einziger täglicher Befehl genügt, jede Meldung ist auch ohne
IT-Wissen verständlich.
Auto-Modus (ein Cron-Job genügt für alles):
- data/state.json merkt sich, wann die Wochen-Prüfungen zuletzt liefen
(baseline.py: load_state/save_state/is_check_due/mark_check_run)
- `scan` führt fällige Zusatzprüfungen (Tarnung/externe Links/Dateien)
automatisch mit aus; Gesamt-Ampel = schlechtestes Teilergebnis;
EIN kombinierter Report + EIN Alert
- neuer Config-Abschnitt periodic_checks (Default: wöchentlich, an)
Klartext (scanner/plain.py):
- Ampel 🟢/🟡/🔴 + Sätze ohne Fachbegriffe, aus den strukturierten Befunden
- Report: Klartext oben, "Technische Details (für Ihren Dienstleister)" unten
- Terminal-Ausgabe und E-Mail (alerter.py) ebenso umgestellt
Komfort:
- approve --all/--rebuild legt die Datei-Überwachung automatisch mit an
(Opt-out: --skip-assets); approve-assets bleibt für den Sonderfall
- status zeigt Ampel zuoberst + wann die Wochen-Prüfungen zuletzt liefen
- Onboarding-Text in einfacher Sprache, wenn noch kein Vergleichsstand existiert
Doku: README + BEDIENUNGSANLEITUNG mit "In 3 Schritten"-Einstieg.
Tests: test_state.py, test_plain.py (Fachbegriff-Assertion), Auto-Asset-
Baseline-Test. 165 Tests grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 11:14:05 +02:00
|
|
|
level = report.get("assessment", {}).get("level", "green")
|
feat: initial implementation of website integrity scanner
Python-Package zur unabhängigen Überwachung statischer Websites gegen
SEO-Spam und unbefugte Manipulationen. Läuft außerhalb des Hosters.
Kernfunktionen:
- Reiner Python-Crawler (bs4+lxml), kein wget
- Baseline-Management mit manuellem Freigabe-Workflow (niemals automatisch)
- Text-/Link-/Meta-Diff mit Risiko-Scoring (grün/gelb/rot)
- Hidden-Content-Erkennung (CSS inline, noscript, Kommentar-Links)
- Whitelist für externe Domains und interne Pfade
- E-Mail + Webhook Alarmierung
- CLI: init, crawl, check, scan, approve, report, status
- 79 Unit-Tests (pytest), alle grün
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 00:51:41 +02:00
|
|
|
target = report.get("target", "?")
|
feat: Laientauglichkeit — Auto-Modus, Klartext-Ausgabe, einfachere Doku
Ziel: ein einziger täglicher Befehl genügt, jede Meldung ist auch ohne
IT-Wissen verständlich.
Auto-Modus (ein Cron-Job genügt für alles):
- data/state.json merkt sich, wann die Wochen-Prüfungen zuletzt liefen
(baseline.py: load_state/save_state/is_check_due/mark_check_run)
- `scan` führt fällige Zusatzprüfungen (Tarnung/externe Links/Dateien)
automatisch mit aus; Gesamt-Ampel = schlechtestes Teilergebnis;
EIN kombinierter Report + EIN Alert
- neuer Config-Abschnitt periodic_checks (Default: wöchentlich, an)
Klartext (scanner/plain.py):
- Ampel 🟢/🟡/🔴 + Sätze ohne Fachbegriffe, aus den strukturierten Befunden
- Report: Klartext oben, "Technische Details (für Ihren Dienstleister)" unten
- Terminal-Ausgabe und E-Mail (alerter.py) ebenso umgestellt
Komfort:
- approve --all/--rebuild legt die Datei-Überwachung automatisch mit an
(Opt-out: --skip-assets); approve-assets bleibt für den Sonderfall
- status zeigt Ampel zuoberst + wann die Wochen-Prüfungen zuletzt liefen
- Onboarding-Text in einfacher Sprache, wenn noch kein Vergleichsstand existiert
Doku: README + BEDIENUNGSANLEITUNG mit "In 3 Schritten"-Einstieg.
Tests: test_state.py, test_plain.py (Fachbegriff-Assertion), Auto-Asset-
Baseline-Test. 165 Tests grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 11:14:05 +02:00
|
|
|
symbol, _ = ampel(level)
|
|
|
|
|
if level == "red":
|
|
|
|
|
return f"{symbol} Ihre Website {target} braucht Aufmerksamkeit"
|
|
|
|
|
if level == "yellow":
|
|
|
|
|
return f"{symbol} Bitte einmal auf Ihrer Website {target} nachschauen"
|
|
|
|
|
return f"{symbol} Ihre Website {target}: alles in Ordnung"
|
feat: initial implementation of website integrity scanner
Python-Package zur unabhängigen Überwachung statischer Websites gegen
SEO-Spam und unbefugte Manipulationen. Läuft außerhalb des Hosters.
Kernfunktionen:
- Reiner Python-Crawler (bs4+lxml), kein wget
- Baseline-Management mit manuellem Freigabe-Workflow (niemals automatisch)
- Text-/Link-/Meta-Diff mit Risiko-Scoring (grün/gelb/rot)
- Hidden-Content-Erkennung (CSS inline, noscript, Kommentar-Links)
- Whitelist für externe Domains und interne Pfade
- E-Mail + Webhook Alarmierung
- CLI: init, crawl, check, scan, approve, report, status
- 79 Unit-Tests (pytest), alle grün
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 00:51:41 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _format_body(report: dict) -> str:
|
2026-06-13 03:35:05 +02:00
|
|
|
"""E-Mail-Body = derselbe Renderer wie der Dateibericht (eine Quelle der
|
|
|
|
|
Wahrheit). So enthalten Report und Mail garantiert denselben Inhalt."""
|
|
|
|
|
return render_markdown(report)
|
feat: initial implementation of website integrity scanner
Python-Package zur unabhängigen Überwachung statischer Websites gegen
SEO-Spam und unbefugte Manipulationen. Läuft außerhalb des Hosters.
Kernfunktionen:
- Reiner Python-Crawler (bs4+lxml), kein wget
- Baseline-Management mit manuellem Freigabe-Workflow (niemals automatisch)
- Text-/Link-/Meta-Diff mit Risiko-Scoring (grün/gelb/rot)
- Hidden-Content-Erkennung (CSS inline, noscript, Kommentar-Links)
- Whitelist für externe Domains und interne Pfade
- E-Mail + Webhook Alarmierung
- CLI: init, crawl, check, scan, approve, report, status
- 79 Unit-Tests (pytest), alle grün
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 00:51:41 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Transport
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def _send_email(subject: str, body: str, cfg: dict) -> None:
|
|
|
|
|
password = os.environ.get(cfg.get("smtp_password_env", "SCANNER_SMTP_PASSWORD"), "")
|
|
|
|
|
recipients = cfg.get("to", [])
|
|
|
|
|
if not recipients:
|
|
|
|
|
logger.warning("E-Mail alert enabled but no recipients configured.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
msg = EmailMessage()
|
|
|
|
|
msg["Subject"] = subject
|
|
|
|
|
msg["From"] = cfg.get("from", "scanner@localhost")
|
|
|
|
|
msg["To"] = ", ".join(recipients)
|
|
|
|
|
msg.set_content(body)
|
|
|
|
|
|
|
|
|
|
host = cfg.get("smtp_host", "localhost")
|
|
|
|
|
port = cfg.get("smtp_port", 587)
|
|
|
|
|
use_tls = cfg.get("smtp_tls", True)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
with smtplib.SMTP(host, port) as server:
|
|
|
|
|
if use_tls:
|
|
|
|
|
server.starttls()
|
|
|
|
|
user = cfg.get("smtp_user", "")
|
|
|
|
|
if user and password:
|
|
|
|
|
server.login(user, password)
|
|
|
|
|
server.send_message(msg)
|
|
|
|
|
logger.info("Alert e-mail sent to %s", recipients)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
logger.error("E-mail alert failed: %s", exc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _send_webhook(url: str, report: dict, subject: str) -> None:
|
|
|
|
|
payload = {
|
|
|
|
|
"text": subject,
|
|
|
|
|
"target": report.get("target"),
|
|
|
|
|
"level": report.get("assessment", {}).get("level"),
|
|
|
|
|
"score": report.get("assessment", {}).get("score"),
|
|
|
|
|
"reasons": report.get("assessment", {}).get("reasons", []),
|
|
|
|
|
"new_external_domains": report.get("diff", {}).get("new_external_domains", []),
|
|
|
|
|
"new_internal_urls": report.get("diff", {}).get("new_internal_urls", []),
|
|
|
|
|
"missing_internal_urls": report.get("diff", {}).get("missing_internal_urls", []),
|
|
|
|
|
"changed_pages": report.get("diff", {}).get("changed_pages", 0),
|
|
|
|
|
}
|
|
|
|
|
try:
|
|
|
|
|
resp = requests.post(url, json=payload, timeout=10)
|
|
|
|
|
logger.info("Webhook delivered: HTTP %s", resp.status_code)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
logger.error("Webhook alert failed: %s", exc)
|