initialer upload des projektes
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
# FritzBox Setup
|
||||||
|
FRITZ_IP=172.178.1.1
|
||||||
|
FRITZ_USER=SYSLOG
|
||||||
|
FRITZ_PWD=DEIN_SYSLOG_PASSWORD_SECRET
|
||||||
|
|
||||||
|
# Syslog Ziel
|
||||||
|
|
||||||
|
SYSLOG_SENDER_NAME=fritzbox-1
|
||||||
|
SYSLOG_SERVER=192.168.1.200
|
||||||
|
SYSLOG_PORT=514
|
||||||
|
SYSLOG_PROTO=UDP
|
||||||
|
|
||||||
|
SEND_INITIAL_LOG=True
|
||||||
|
SYSLOG_EXTRACT_TIME=True
|
||||||
|
# Standard-Fallback falls Extraktion fehlschlägt
|
||||||
|
TZ=Europe/Berlin
|
||||||
|
HASH_PERSISTENT=True
|
||||||
|
HASH_LIMIT=2000
|
||||||
|
HASH_RETAIN=1000
|
||||||
|
# /data/ im Container muss als Volume gemountet sein
|
||||||
|
HASH_FILE=/data/hashes.db.txt
|
||||||
|
HASH_BACKUP_EVERY=20
|
||||||
|
|
||||||
|
|
||||||
|
# aussehen des syslogs bestimmen,
|
||||||
|
SYSLOG_EXTRACT_TIME=True
|
||||||
|
|
||||||
|
# achtung , entweder
|
||||||
|
# syslog mixed
|
||||||
|
# bei true wird die syslogmessage und der json gesendet
|
||||||
|
SYSLOG_MIXED_JSON=false
|
||||||
|
# oder
|
||||||
|
SYSLOG_JSON_ONLY=false
|
||||||
|
|
||||||
|
# Trenner zwischen Klartext und JSON bei syslog_mixed_json
|
||||||
|
JSON_SEPARATOR=" -_- "
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Timing & Retries
|
||||||
|
CHECK_INTERVAL=60
|
||||||
|
MAX_RETRIES=3
|
||||||
|
|
||||||
|
# Neue Schalter
|
||||||
|
DEBUG_MODE=True
|
||||||
|
DEBUG2_IGNORE_CACHE=True
|
||||||
|
#REVERSE_LOG=false
|
||||||
|
# ersetzt durch neue logik ;)
|
||||||
|
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
# fbsyslogsender
|
|
||||||
|
|
||||||
Dieses Projekt ermöglicht ein lückenloses Monitoring von FRITZ!Box-Systemen (DSL, VPN, WLAN, Login) via Syslog, optimiert für Graylog und Elastic.
|
|
||||||
|
|
||||||
# FRITZ!Box Enterprise Monitor (Lua-Based)
|
# FRITZ!Box Enterprise Monitor (Lua-Based)
|
||||||
|
|
||||||
Dieses Projekt ermöglicht ein lückenloses Monitoring von FRITZ!Box-Systemen (DSL, VPN, WLAN, Login) via Syslog, optimiert für Graylog und Elastic.
|
Dieses Projekt ermöglicht ein lückenloses Monitoring von FRITZ!Box-Systemen (DSL, VPN, WLAN, Login) via Syslog, optimiert für Graylog und Elastic.
|
||||||
@@ -38,9 +34,12 @@ Sicherung des aktuellen Standorts inkl. aller Hashes:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Übersicht
|
## Übersicht
|
||||||
|
|
||||||
## Dieses System überwacht eine FRITZ!Box (getestet mit FRITZ!OS 7.52+) auf System-Ereignisse und VPN-Statusänderungen. Da die Standard-Schnittstelle (TR-064) von AVM oft auf 5 Log-Einträge limitiert ist, nutzt dieses Skript die Lua-Sch>
|
## Dieses System überwacht eine FRITZ!Box (getestet mit FRITZ!OS 7.52+) auf System-Ereignisse und VPN-Statusänderungen. Da die Standard-Schnittstelle (TR-064) von AVM oft auf 5 Log-Einträge limitiert ist, nutzt dieses Skript die Lua-Schnittstelle (Web-UI API), um das vollständige System-Log und detaillierte VPN-Statusinformationen abzugreifen.
|
||||||
|
|
||||||
## Funktionsweise
|
## Funktionsweise
|
||||||
|
|
||||||
Authentifizierung: Das Skript meldet sich via Challenge-Response-Verfahren an der FRITZ!Box an und erhält eine Session-ID (SID).
|
Authentifizierung: Das Skript meldet sich via Challenge-Response-Verfahren an der FRITZ!Box an und erhält eine Session-ID (SID).
|
||||||
@@ -57,7 +56,7 @@ Sicherung des aktuellen Standorts inkl. aller Hashes:
|
|||||||
|
|
||||||
## Installation & Deployment
|
## Installation & Deployment
|
||||||
|
|
||||||
### 1. Umgebungsvariablen (.env)
|
# 1. Umgebungsvariablen (.env)
|
||||||
Erstelle eine .env Datei mit folgendem Inhalt:
|
Erstelle eine .env Datei mit folgendem Inhalt:
|
||||||
env
|
env
|
||||||
|
|
||||||
@@ -74,23 +73,21 @@ SEND_INITIAL_LOG=True
|
|||||||
DEBUG_MODE=False
|
DEBUG_MODE=False
|
||||||
|
|
||||||
|
|
||||||
|
## 2. Docker Compose
|
||||||
|
|
||||||
### 2. Docker Compose
|
|
||||||
Starten des Containers mit:
|
Starten des Containers mit:
|
||||||
bash
|
bash
|
||||||
|
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
|
|
||||||
#### Troubleshooting ("Notfall-Guide")
|
## Troubleshooting ("Notfall-Guide")
|
||||||
##### Falls keine Logs mehr im Syslog-Server ankommen:
|
# Falls keine Logs mehr im Syslog-Server ankommen:
|
||||||
|
|
||||||
Container-Logs prüfen: docker logs -f fritz-syslog-forwarder.
|
Container-Logs prüfen: docker logs -f fritz-syslog-forwarder.
|
||||||
SID-Fehler: Erscheint ein "Login-Fehler", prüfen ob das Passwort abgelaufen ist oder ob die FRITZ!Box eine 2FA-Bestätigung (Taste drücken) für den API-Zugriff verlangt.
|
SID-Fehler: Erscheint ein "Login-Fehler", prüfen ob das Passwort abgelaufen ist oder ob die FRITZ!Box eine 2FA-Bestätigung (Taste drücken) für den API-Zugriff verlangt.
|
||||||
Netzwerk: Testen, ob der Container den Syslog-Server pingen kann.
|
Netzwerk: Testen, ob der Container den Syslog-Server pingen kann.
|
||||||
FRITZ!OS Update: Nach einem Firmware-Update könnte AVM die data.lua Pfade ändern. In diesem Fall muss die fetch_data Funktion im Skript geprüft werden.
|
FRITZ!OS Update: Nach einem Firmware-Update könnte AVM die data.lua Pfade ändern. In diesem Fall muss die fetch_data Funktion im Skript geprüft werden.
|
||||||
|
|
||||||
#### Technische Details
|
## Technische Details
|
||||||
|
|
||||||
Sprache: Python 3.11-slim
|
Sprache: Python 3.11-slim
|
||||||
Bibliotheken: requests, python-dotenv
|
Bibliotheken: requests, python-dotenv
|
||||||
@@ -98,7 +95,7 @@ bash
|
|||||||
Datenquelle: data.lua?page=log und data.lua?page=vpn
|
Datenquelle: data.lua?page=log und data.lua?page=vpn
|
||||||
|
|
||||||
|
|
||||||
### 3. Hinweis zur Häufungserkennung
|
## 3. Hinweis zur Häufungserkennung
|
||||||
Die FRITZ!Box fasst identische Meldungen zusammen (z.B. Login-Versuche), um das Log nicht zu fluten. Durch das elif oben wird eine Zeile wie:
|
Die FRITZ!Box fasst identische Meldungen zusammen (z.B. Login-Versuche), um das Log nicht zu fluten. Durch das elif oben wird eine Zeile wie:
|
||||||
Anmeldung des Benutzers syslog... [145 Meldungen seit 04.02.26 02:00:39]
|
Anmeldung des Benutzers syslog... [145 Meldungen seit 04.02.26 02:00:39]
|
||||||
automatisch als Warning markiert. Das ist ideal für Graylog-Alerts, da du so sofort siehst, wenn ein Dienst (oder ein Angreifer) "Amok läuft".
|
automatisch als Warning markiert. Das ist ideal für Graylog-Alerts, da du so sofort siehst, wenn ein Dienst (oder ein Angreifer) "Amok läuft".
|
||||||
@@ -110,28 +107,28 @@ Zusammenfassung der Änderungen für deine Meta-Doku:
|
|||||||
|
|
||||||
Sobald du die 7.62 installierst, schau im DEBUG_MODE besonders auf die VPN-Meldungen – AVM führt dort oft neue Verschlüsselungs-Bezeichnungen ein.
|
Sobald du die 7.62 installierst, schau im DEBUG_MODE besonders auf die VPN-Meldungen – AVM führt dort oft neue Verschlüsselungs-Bezeichnungen ein.
|
||||||
|
|
||||||
### 4. Erklärung der erweiterten Umgebungsvariablen (.env)
|
|
||||||
Parameter Wert (Beispiel) Beschreibung
|
|
||||||
SEND_INITIAL_LOG True/False Wenn True, wird beim Containerstart das gesamte vorhandene Log der FRITZ!Box gesendet. Bei False werden nur Ereignisse ab Startmoment übertragen.
|
|
||||||
SYSLOG_EXTRACT_TIME True/False Extrahiert den Zeitstempel direkt aus der FRITZ!Box-Meldung (04.02.26 03:02). Garantiert sekundengenaue Logs, unabhängig von der Container-Zeit.
|
|
||||||
SYSLOG_MIXED_JSON True/False Sendet eine hybride Nachricht: Erst der Klartext, dann der Trenner, dann strukturiertes JSON. Ideal für parallele Nutzung von Syslog-Viewern und Graylog.
|
|
||||||
SYSLOG_JSON_ONLY True/False Sendet nach dem Syslog-Header ausschließlich das JSON-Objekt. Optimiert für Elasticsearch und Graylog (kein Parsing des Klartextes nötig).
|
|
||||||
JSON_SEPARATOR " -_- " Definiert die Zeichenfolge, die bei MIXED_JSON zwischen Text und Daten steht. Dient Graylog als Anker für das Splitting.
|
|
||||||
TZ Europe/Berlin Zeitzone für den Fall, dass die Zeitextraktion fehlschlägt oder VPN-Events (Echtzeit) gesendet werden.
|
|
||||||
DEBUG_MODE True/False Schaltet ausführliche Ausgaben im Container-Log (docker logs) frei (Login-Status, gesendete Payloads, Hash-Checks).
|
|
||||||
DEBUG2_IGNORE_CACHE True/False Vorsicht: Deaktiviert den Dubletten-Schutz. Sendet bei jedem Intervall das komplette Log erneut. Nur für kurzzeitige Fehlersuche nutzen!
|
|
||||||
|
|
||||||
### 5. Syslog-Prioritäten & Level-Mapping
|
## 4. Erklärung der erweiterten Umgebungsvariablen (.env)
|
||||||
|
Parameter Wert (Beispiel) Beschreibung
|
||||||
|
SEND_INITIAL_LOG True/False Wenn True, wird beim Containerstart das gesamte vorhandene Log der FRITZ!Box gesendet. Bei False werden nur Ereignisse ab Startmoment übertragen.
|
||||||
|
SYSLOG_EXTRACT_TIME True/False Extrahiert den Zeitstempel direkt aus der FRITZ!Box-Meldung (04.02.26 03:02). Garantiert sekundengenaue Logs, unabhängig von der Container-Zeit.
|
||||||
|
SYSLOG_MIXED_JSON True/False Sendet eine hybride Nachricht: Erst der Klartext, dann der Trenner, dann strukturiertes JSON. Ideal für parallele Nutzung von Syslog-Viewern und Graylog.
|
||||||
|
SYSLOG_JSON_ONLY True/False Sendet nach dem Syslog-Header ausschließlich das JSON-Objekt. Optimiert für Elasticsearch und Graylog (kein Parsing des Klartextes nötig).
|
||||||
|
JSON_SEPARATOR " -_- " Definiert die Zeichenfolge, die bei MIXED_JSON zwischen Text und Daten steht. Dient Graylog als Anker für das Splitting.
|
||||||
|
TZ Europe/Berlin Zeitzone für den Fall, dass die Zeitextraktion fehlschlägt oder VPN-Events (Echtzeit) gesendet werden.
|
||||||
|
DEBUG_MODE True/False Schaltet ausführliche Ausgaben im Container-Log (docker logs) frei (Login-Status, gesendete Payloads, Hash-Checks).
|
||||||
|
DEBUG2_IGNORE_CACHE True/False Vorsicht: Deaktiviert den Dubletten-Schutz. Sendet bei jedem Intervall das komplette Log erneut. Nur für kurzzeitige Fehlersuche nutzen!
|
||||||
|
|
||||||
|
## 5. Syslog-Prioritäten & Level-Mapping
|
||||||
Das Skript übersetzt FRITZ!Box-Ereignisse automatisch in standardisierte Syslog-Severities (RFC 5424). Dies ermöglicht in Graylog/Zabbix eine sofortige Alarmierung basierend auf dem Feld level oder der Priority-ID.
|
Das Skript übersetzt FRITZ!Box-Ereignisse automatisch in standardisierte Syslog-Severities (RFC 5424). Dies ermöglicht in Graylog/Zabbix eine sofortige Alarmierung basierend auf dem Feld level oder der Priority-ID.
|
||||||
Severity ID Level Name Trigger im Skript (Keywords) Bedeutung
|
Severity ID Level Name Trigger im Skript (Keywords) Bedeutung
|
||||||
2 CRITICAL 0x203e, 0x2027, ike-error Kritische VPN-Handshake-Fehler. Handlungsbedarf bei Site-to-Site.
|
2 CRITICAL 0x203e, 0x2027, ike-error Kritische VPN-Handshake-Fehler. Handlungsbedarf bei Site-to-Site.
|
||||||
3 ERROR Fehler, Error, Failed, Abbruch, gescheitert Fehlgeschlagene Logins, DSL-Abbrüche, Internet-Trennung.
|
3 ERROR Fehler, Error, Failed, Abbruch, gescheitert Fehlgeschlagene Logins, DSL-Abbrüche, Internet-Trennung.
|
||||||
4 WARNING [... Meldungen seit ...], Warnung, Warning, ursache Häufung von Ereignissen oder instabile Leitungen (Störsicherheit).
|
4 WARNING [... Meldungen seit ...], Warnung, Warning, ursache Häufung von Ereignissen oder instabile Leitungen (Störsicherheit).
|
||||||
6 INFO Alle anderen Meldungen WLAN-Anmeldungen, reguläre Reconnects, Systemstarts.
|
6 INFO Alle anderen Meldungen WLAN-Anmeldungen, reguläre Reconnects, Systemstarts.
|
||||||
|
|
||||||
|
|
||||||
## Pro-Tipp für die Wartung:
|
## Pro-Tipp für die Wartung:
|
||||||
Sollte die FRITZ!Box nach einem Update auf 7.62 oder 8.0 keine Logs mehr senden, prüfen Sie im DEBUG_MODE, ob die fetch_data Funktion noch ein gültiges JSON-Objekt von der data.lua zurückerhält.
|
Sollte die FRITZ!Box nach einem Update auf 7.62 oder 8.0 keine Logs mehr senden, prüfen Sie im DEBUG_MODE, ob die fetch_data Funktion noch ein gültiges JSON-Objekt von der data.lua zurückerhält.
|
||||||
AVM neigt dazu, die Feldnamen (z.B. von vpnList zu vpn_list) bei Major-Updates leicht zu ändern.
|
AVM neigt dazu, die Feldnamen (z.B. von vpnList zu vpn_list) bei Major-Updates leicht zu ändern.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
fritz-syslog:
|
||||||
|
build: .
|
||||||
|
container_name: fritz-syslog-forwarder
|
||||||
|
env_file: .env
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Berlin
|
||||||
|
volumes:
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- ./data:/data
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# #############################################################################
|
||||||
|
# SCRIPT : deploy-fb-monitor.sh
|
||||||
|
# VERSION : 1.2.0
|
||||||
|
# DESCRIPTION : Deployment- & Backup-Tool für FB-Enterprise-Monitor
|
||||||
|
# AUTHOR : Andreas O. Schmidt
|
||||||
|
# CONTACT : cert.a.o.schmidt@outlook.de
|
||||||
|
#
|
||||||
|
# ISMS/WIKI : https://wiki.deinefirma.internal
|
||||||
|
# GIT : https://github.com[DeinAccount]/[Projekt]/blob/main/deploy.sh
|
||||||
|
#
|
||||||
|
# REVISION :
|
||||||
|
# - Last : 2026-02-04
|
||||||
|
# - Next : 2026-08-04
|
||||||
|
# #############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# FritzBox-Monitor Deployment & Backup Tool
|
||||||
|
|
||||||
|
PROJECT_NAME="fbsyslog-monitor"
|
||||||
|
VERSION=$2
|
||||||
|
TAG=$3
|
||||||
|
BACKUP_DIR="./backups"
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
DATADIR=.data
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
pack)
|
||||||
|
echo "📦 Packe Monitor-Setup für neuen Standort..."
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
tar -czvf "${BACKUP_DIR}/${PROJECT_NAME}_${VERSION}_${TIMESTAMP}.tar.gz" \
|
||||||
|
syslog-fb-monitor.py dockerfile requirements.txt compose.yaml.dist .env.dist README.md deploy-fb-monitor.sh
|
||||||
|
echo "✅ Archiv erstellt: ${BACKUP_DIR}/${PROJECT_NAME}_${VERSION}_${TAG}_${TIMESTAMP}.tar.gz"
|
||||||
|
;;
|
||||||
|
backup)
|
||||||
|
echo "📦 Packe Monitor-Backup für neuen Standort..."
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
tar -czvf "${BACKUP_DIR}/${PROJECT_NAME}_${VERSION}_${TIMESTAMP}.tar.gz" \
|
||||||
|
syslog-fb-monitor.py dockerfile requirements.txt compose.yaml .env.dist .env README.md deploy-fb-monitor.sh
|
||||||
|
echo "✅ Archiv erstellt: ${BACKUP_DIR}/${PROJECT_NAME}_${VERSION}_${TAG}_${TIMESTAMP}.tar.gz"
|
||||||
|
;;
|
||||||
|
|
||||||
|
deploy)
|
||||||
|
echo "🚀 Initialisiere neues Standort-Deployment..."
|
||||||
|
mkdir -p ${DATADIR}
|
||||||
|
|
||||||
|
# 1. Interaktive Abfrage
|
||||||
|
read -p "📍 Standort-Name (z.B. hamburg1): " SITE_NAME
|
||||||
|
read -p "🌐 IP der FRITZ!Box: " FB_IP
|
||||||
|
read -p "🖥️ IP des Syslog-Servers: " SYSLOG_IP
|
||||||
|
|
||||||
|
# 2. Dateien vorbereiten
|
||||||
|
cp .env.dist .env
|
||||||
|
sed -i "s/^FRITZ_IP=.*/FRITZ_IP=${FB_IP}/" .env
|
||||||
|
sed -i "s/^SYSLOG_SERVER=.*/SYSLOG_SERVER=${SYSLOG_IP}/" .env
|
||||||
|
sed -i "s/^SYSLOG_SENDER_NAME=.*/SYSLOG_SENDER_NAME=fritzbox_${SITE_NAME}/" .env
|
||||||
|
|
||||||
|
C_NAME="fritz-monitor-${SITE_NAME}"
|
||||||
|
echo -e "\n# containername=${C_NAME}" >> .env
|
||||||
|
|
||||||
|
if [ -f compose.yaml.dist ]; then
|
||||||
|
cp compose.yaml.dist compose.yaml
|
||||||
|
sed -i "s/container_name:.*/container_name: ${C_NAME}/" compose.yaml
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Manuelle Kontrolle
|
||||||
|
echo "Öffne .env zur Endkontrolle..."
|
||||||
|
sleep 1
|
||||||
|
nano .env
|
||||||
|
|
||||||
|
# 4. Start und Erster Log-Check via Compose
|
||||||
|
# --build erzwingt frisches Image, -d läuft im Hintergrund
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
echo "⏳ Kurzer Wait für den ersten Connect (50s)..."
|
||||||
|
sleep 50
|
||||||
|
|
||||||
|
echo "📊 Zeige erste Logs (Service-Level)..."
|
||||||
|
# Nutzt den Service-Namen aus der compose.yaml, ignoriert Synology-Präfixe
|
||||||
|
docker compose logs --tail 50
|
||||||
|
|
||||||
|
echo "⏳ Langer Wait für vollständigen Initial-Sync (200s)..."
|
||||||
|
sleep 200
|
||||||
|
|
||||||
|
echo "📊 Zeige finale Logs vor Abschluss..."
|
||||||
|
docker compose logs --tail 50
|
||||||
|
read -p "wenn alles Okay ist, weiter mit enter, bei fehlern, mit Strg+C abbrechen:"
|
||||||
|
|
||||||
|
# 5. Variable auf false setzen und finaler Restart
|
||||||
|
echo "✅ Initialer Sync verarbeitet, setze SEND_INITIAL_LOG auf false"
|
||||||
|
sed -i 's/^SEND_INITIAL_LOG=.*/SEND_INITIAL_LOG=false/' .env
|
||||||
|
|
||||||
|
echo "Restart mit neuen Einstellungen..."
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
echo "🏁 Setup für Standort ${SITE_NAME} abgeschlossen!"
|
||||||
|
# Zur Sicherheit zeigen wir an, wie der Container nun wirklich heißt
|
||||||
|
docker compose ps
|
||||||
|
;; start)
|
||||||
|
echo "🚀 Starte Monitor-Container..."
|
||||||
|
mkdir ${DATADIR}
|
||||||
|
docker compose up -d --build
|
||||||
|
echo "📊 Zeige Logs (Abbrechen mit Strg+C)..."
|
||||||
|
sleep 20
|
||||||
|
docker logs -f fritz-syslog-forwarder
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
echo "🚀 Stoppe Monitor-Container..."
|
||||||
|
docker compose logs
|
||||||
|
docker compose down
|
||||||
|
echo "📊 Zeige Logs (Abbrechen mit Strg+C)..."
|
||||||
|
sleep 2
|
||||||
|
docker logs -f fritz-syslog-forwarder
|
||||||
|
;;
|
||||||
|
|
||||||
|
update)
|
||||||
|
echo "🚀 Update Monitor-Container..."
|
||||||
|
docker compose down && docker compose up -d --build
|
||||||
|
echo "📊 Zeige Logs (Abbrechen mit Strg+C)..."
|
||||||
|
sleep 2
|
||||||
|
docker logs -f fritz-syslog-forwarder
|
||||||
|
;;
|
||||||
|
help)
|
||||||
|
echo -e "--- 📖 Lange Hilfe: FRITZ!Box Enterprise Monitor Deployment ---"
|
||||||
|
echo -e "Dieses Skript automatisiert das Rollout von FB-Monitoren."
|
||||||
|
echo -e "\nBefehle:"
|
||||||
|
echo -e " pack : Erstellt ein sauberes Archiv ohne .env für neue Standorte."
|
||||||
|
echo -e " backup : Sichert alles inkl. .env und Hashes (Zustandssicherung)."
|
||||||
|
echo -e " deploy : Interaktives Setup (IPs, Name, Nano, Initial-Sync)."
|
||||||
|
echo -e " update : Zieht neues Image und startet Container neu."
|
||||||
|
echo -e " stop : Fährt den Stack am Standort sauber runter."
|
||||||
|
echo -e "\nParameter:"
|
||||||
|
echo -e " <version> : Versionsnummer für das Archiv (z.B. 1.0.2)"
|
||||||
|
echo -e " <tag> : Standort oder Status (z.B. 'hamburg' oder 'stable')"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "❌ Ungültiger Aufruf!"
|
||||||
|
echo -e "Nutzung: $0 {pack|backup|deploy|update|stop|help} [[<version>] [<tag>]]"
|
||||||
|
echo -e "\nSynopsys:"
|
||||||
|
echo -e " $0 pack 0.1.2 'release' -> Sauberes Paket packen"
|
||||||
|
echo -e " $0 backup 0.1.2 'hamburg1' -> Lokales Backup inkl. Hashes"
|
||||||
|
echo -e " $0 deploy -> Interaktives Standort-Setup"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
#*)
|
||||||
|
# echo "Nutzung: $0 {pack|backup|setup|deploy|update|stop} [[<version>] [<tag>]] \n synopsys: \n deploy-fb-monitor.sh pack 0.1.2 'latest' \n deploy-fb-monitor.sh backup 0.1.2 'site1' \n deploy-fb-monitor.sh pack 0.1.2 \n deploy-fb-monitor.sh backup site1 "
|
||||||
|
# exit 1
|
||||||
|
# ;;
|
||||||
|
esac
|
||||||
|
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
WORKDIR /app
|
||||||
|
# Verhindert, dass Python stdout/stderr puffert
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY . .
|
||||||
|
# Nutzt -u für unbuffered binary stdout
|
||||||
|
CMD ["python", "-u", "syslog-fb-monitor.py"]
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
fritzconnection
|
||||||
|
requests
|
||||||
|
python-dotenv
|
||||||
|
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# #############################################################################
|
||||||
|
# TOOL : FRITZ!Box Enterprise Monitor (Lua-Based)
|
||||||
|
# VERSION : 1.2.0
|
||||||
|
# AUTHOR : [Dein Name/Kürzel]
|
||||||
|
# CONTACT : [Deine E-Mail / Abteilung]
|
||||||
|
# REPOSITORY : https://github.com[DeinAccount]/[Projekt]
|
||||||
|
#
|
||||||
|
# DOCUMENTATION:
|
||||||
|
# - WIKI : https://wiki.deinefirma.internal
|
||||||
|
# - ISMS : Asset-ID [z.B. FB-MON-2026] / Risiko-Klasse: Medium
|
||||||
|
# - MONITORING: Graylog-Stream "FritzBox-Satelliten"
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# ISMS CONTEXT:
|
||||||
|
# - ASSET-ID : HOME-NET-MON-01
|
||||||
|
# - CLASS : Internal / Private Setup
|
||||||
|
# - WIKI : https://dein-privates-wiki.internal
|
||||||
|
#
|
||||||
|
# HISTORY:
|
||||||
|
# - 2026-02-04: v1.2.0 - Refactoring auf Lua-API, JSON-Support & Persistenz
|
||||||
|
# - REVISION : Quarterly (Next: 2026-05-04)
|
||||||
|
# - Created : 2026-02-04
|
||||||
|
# - Last Rev. : 2026-02-04 (Initial Release nach Provider-Wartung Hamburg)
|
||||||
|
# - Next Rev. : 2026-08-04 (Halbjährlicher Security-Audit & API-Check)
|
||||||
|
#
|
||||||
|
# DESCRIPTION:
|
||||||
|
# Extrahiert via Lua-Session das System-Log und VPN-Statusdaten aus
|
||||||
|
# FRITZ!Boxen und sendet diese strukturiert (JSON) an einen Syslog-Server.
|
||||||
|
# #############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
import logging.handlers
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from datetime import datetime
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
VER="8.55_rc3 mit json, mixed, s2s, caching, errorlevels-spezial"
|
||||||
|
|
||||||
|
# Initialisierung
|
||||||
|
print(f"SKRIPT-START: Initialisiere Fritz-Enterprise-Monitor v {VER} :...", flush=True)
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Konfiguration aus .env
|
||||||
|
FB_IP = os.getenv("FRITZ_IP")
|
||||||
|
FB_USER = os.getenv("FRITZ_USER")
|
||||||
|
FB_PWD = os.getenv("FRITZ_PWD")
|
||||||
|
SENDER_NAME = os.getenv("SYSLOG_SENDER_NAME", "fritzbox")
|
||||||
|
SYSLOG_SERVER = os.getenv("SYSLOG_SERVER")
|
||||||
|
SYSLOG_PORT = int(os.getenv("SYSLOG_PORT", 514))
|
||||||
|
INTERVAL = int(os.getenv("CHECK_INTERVAL", 60))
|
||||||
|
|
||||||
|
# Schalter
|
||||||
|
DEBUG_MODE = os.getenv("DEBUG_MODE", "False").lower() == "true"
|
||||||
|
SEND_INITIAL = os.getenv("SEND_INITIAL_LOG", "True").lower() == "true"
|
||||||
|
EXTRACT_TIME = os.getenv("SYSLOG_EXTRACT_TIME", "True").lower() == "true"
|
||||||
|
MIXED_JSON = os.getenv("SYSLOG_MIXED_JSON", "False").lower() == "true"
|
||||||
|
JSON_ONLY = os.getenv("SYSLOG_JSON_ONLY", "False").lower() == "true"
|
||||||
|
SEPARATOR = os.getenv("JSON_SEPARATOR", " -_- ")
|
||||||
|
|
||||||
|
# Persistenz-Konfiguration
|
||||||
|
HASH_PERSISTENT = os.getenv("HASH_PERSISTENT", "False").lower() == "true"
|
||||||
|
HASH_FILE = os.getenv("HASH_FILE", "/data/hashes.db.txt")
|
||||||
|
HASH_LIMIT = int(os.getenv("HASH_LIMIT", 2000))
|
||||||
|
HASH_RETAIN = int(os.getenv("HASH_RETAIN", 1000))
|
||||||
|
|
||||||
|
last_hashes = set()
|
||||||
|
|
||||||
|
|
||||||
|
def save_hashes():
|
||||||
|
""" Speichert das Hash-Set in eine Datei """
|
||||||
|
if not HASH_PERSISTENT: return
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(HASH_FILE), exist_ok=True)
|
||||||
|
with open(HASH_FILE, "w") as f:
|
||||||
|
for h in last_hashes:
|
||||||
|
f.write(f"{h}\n")
|
||||||
|
if DEBUG_MODE: print(f"[DEBUG] {len(last_hashes)} Hashes persistent gespeichert.", flush=True)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"!!! Fehler beim Speichern der Hashes: {e}", flush=True)
|
||||||
|
|
||||||
|
def load_hashes():
|
||||||
|
""" Lädt Hashes beim Start """
|
||||||
|
global last_hashes
|
||||||
|
if not HASH_PERSISTENT or not os.path.exists(HASH_FILE): return
|
||||||
|
try:
|
||||||
|
with open(HASH_FILE, "r") as f:
|
||||||
|
lines = f.read().splitlines()
|
||||||
|
last_hashes = set(lines)
|
||||||
|
print(f"[*] {len(last_hashes)} Hashes aus Persistenz geladen.", flush=True)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"!!! Fehler beim Laden der Hashes: {e}", flush=True)
|
||||||
|
|
||||||
|
def signal_handler(sig, frame):
|
||||||
|
""" Fängt Docker Stop Signale ab """
|
||||||
|
print(f"[*] Signal {sig} empfangen. Beende sicher...", flush=True)
|
||||||
|
save_hashes()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Signale registrieren (für sauberes Docker Down)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Syslog Handler
|
||||||
|
handler = logging.handlers.SysLogHandler(address=(SYSLOG_SERVER, SYSLOG_PORT))
|
||||||
|
|
||||||
|
class VPNErrorLookup:
|
||||||
|
""" Übersetzt kryptische Fritzbox VPN Hex-Codes in Klartext """
|
||||||
|
# ERRORS = {
|
||||||
|
# "0x203e": "IKE-Error: Timeout. Gegenstelle antwortet nicht (DNS prüfen oder Host down).",
|
||||||
|
# "0x2027": "IKE-Error: Connection refused. Gegenstelle lehnt Verbindung aktiv ab (IP-Mismatch?).",
|
||||||
|
# "0x201c": "IKE-Error: Authentisierungsfehler. Shared Secret (PSK) prüfen!",
|
||||||
|
# "0x2026": "IKE-Error: Remote-ID mismatch. Identität passt nicht zur Konfiguration.",
|
||||||
|
# "0x2020": "IKE-Error: Proposal mismatch. Verschlüsselungs-SAs passen nicht zusammen.",
|
||||||
|
# "0x2028": "IKE-Error: Initial-Contact fehlgeschlagen (Gegenstelle instabil).",
|
||||||
|
# "0x2033": "IKE-Error: Lifetime-Timeout (Re-Keying fehlgeschlagen).",
|
||||||
|
# "0x1": "VPN-Fehler: Interner Fehler oder DNS-Auflösung fehlgeschlagen.",
|
||||||
|
# "3 IKE server": "VPN-Trennung durch lokalen Server (z.B. Reconnect/Zwangstrennung)."
|
||||||
|
# }
|
||||||
|
|
||||||
|
ERRORS = {
|
||||||
|
# Verbindungsprobleme
|
||||||
|
"0x203e": "IKE-Error: Timeout. Gegenstelle antwortet nicht (DNS prüfen oder Host down).",
|
||||||
|
"0x2027": "IKE-Error: Connection refused. Gegenstelle lehnt Verbindung aktiv ab.",
|
||||||
|
"0x1": "VPN-Fehler: DNS-Fehler oder interner Fehler beim Verbindungsaufbau.",
|
||||||
|
"3 IKE server": "VPN-Trennung durch lokalen Server (z.B. Zwangstrennung/Reconnect).",
|
||||||
|
|
||||||
|
# Authentifizierung & Schlüssel
|
||||||
|
"0x201c": "IKE-Error: Authentisierungsfehler. Shared Secret (PSK) prüfen!",
|
||||||
|
"0x2026": "IKE-Error: Remote-ID mismatch. Identität der Gegenstelle passt nicht zur Konfiguration.",
|
||||||
|
|
||||||
|
# Protokoll- & Konfigurationskonflikte
|
||||||
|
"0x2020": "IKE-Error: Proposal mismatch. Verschlüsselungs-Algorithmen (IKE/IPsec SAs) passen nicht zusammen.",
|
||||||
|
"0x2005": "IKE-Error: Keine virtuelle IP-Adresse für VPN-Nutzer festlegbar.",
|
||||||
|
"0x203d": "IKE-Error: Peer-Authentifizierungs-Problem (Zertifikat oder ID fehlerhaft).",
|
||||||
|
"0x200A": "IKE-Error: Invalid exchange type (Protokollfehler).",
|
||||||
|
"0x200D": "IKE-Error: Invalid minor version.",
|
||||||
|
"0x200E": "IKE-Error: Invalid protocol version.",
|
||||||
|
"0x000F": "IKE-Error: Payloads not encrypted.",
|
||||||
|
"0x0010": "IKE-Error: Payloads are encrypted (obwohl unverschlüsselt erwartet).",
|
||||||
|
"0x2028": "IKE-Error: Initial-Contact fehlgeschlagen (Gegenstelle evtl. instabil).",
|
||||||
|
"0x2033": "IKE-Error: Lifetime-Timeout (Re-Keying fehlgeschlagen)."
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def translate(cls, msg):
|
||||||
|
for code, description in cls.ERRORS.items():
|
||||||
|
if code.lower() in msg.lower():
|
||||||
|
return f"{msg} | INFO: {description}"
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def get_meta(msg):
|
||||||
|
""" Kategorisierung und Severity-Mapping """
|
||||||
|
m = msg.lower()
|
||||||
|
cat = "SYSTEM"
|
||||||
|
if any(x in m for x in ["anmeldung", "zugang", "benutzeroberfläche"]): cat = "LOGIN"
|
||||||
|
#elif any(x in m for x in ["dyndns", "wlan", "funknetz"]): cat = "DynDNS"
|
||||||
|
elif any(x in m for x in ["ghz", "wlan", "funknetz"]): cat = "WLAN"
|
||||||
|
elif any(x in m for x in ["vpn", "ike", "ipsec", "wireguard"]): cat = "VPN"
|
||||||
|
elif any(x in m for x in ["dsl", "internetverbindung", "synchronisierung", "breitband"]): cat = "UPLINK"
|
||||||
|
elif any(x in m for x in ["einstellung", "konfiguration", "geändert"]): cat = "CONFIG"
|
||||||
|
elif "dyndns" in m: cat = "DynDNS"
|
||||||
|
|
||||||
|
# Severity Mapping (RFC 5424)
|
||||||
|
sev, lvl_name = 6, "INFO"
|
||||||
|
|
||||||
|
## Kritisch: VPN-Fehler während Site-to-Site Phase
|
||||||
|
## noch etwas zu hart gefasst
|
||||||
|
#if "vpn" in m and any(x in m for x in ["0x", "fehler", "error", "ike"]):
|
||||||
|
# sev, lvl_name = 2, "CRITICAL"
|
||||||
|
#elif any(x in m for x in ["fehler", "error", "failed", "abbruch", "gescheitert"]):
|
||||||
|
# sev, lvl_name = 3, "ERROR"
|
||||||
|
#elif (any(x in m for x in ["warnung", "warning", "ursache", "seit"]) and "[" in m):
|
||||||
|
# sev, lvl_name = 4, "WARNING"
|
||||||
|
#
|
||||||
|
|
||||||
|
# hier eine entschärftere version gegen false positive bei VPN
|
||||||
|
# KRITISCH: VPN-Fehler, aber NUR wenn NICHT "erfolgreich" im Text steht
|
||||||
|
# Das verhindert, dass der Tunnel-Aufbau als Fehler markiert wird.
|
||||||
|
if "vpn" in m and any(x in m for x in ["0x", "fehler", "error", "ike"]):
|
||||||
|
if "erfolgreich" not in m:
|
||||||
|
sev, lvl_name = 2, "CRITICAL"
|
||||||
|
else:
|
||||||
|
sev, lvl_name = 6, "INFO" # Erfolgreicher Aufbau ist nur eine Info
|
||||||
|
|
||||||
|
elif any(x in m for x in ["fehler", "error", "failed", "abbruch", "gescheitert"]):
|
||||||
|
sev, lvl_name = 3, "ERROR"
|
||||||
|
|
||||||
|
elif any(x in m for x in ["warnung", "warning", "ursache", "seit"]) and "[" in m:
|
||||||
|
sev, lvl_name = 4, "WARNING"
|
||||||
|
|
||||||
|
# DynDNS-Logik (Dein neues Beispiel)
|
||||||
|
elif "dyndns" in m:
|
||||||
|
# Fehlerszenarien: "nicht aufgelöst werden" oder klassische Fehler-Keywords
|
||||||
|
if any(x in m for x in ["fehler", "error", "nicht aufgelöst", "deaktiviert", "gescheitert"]):
|
||||||
|
sev, lvl_name = 3, "ERROR"
|
||||||
|
elif "erfolgreich" in m:
|
||||||
|
sev, lvl_name = 6, "INFO"
|
||||||
|
else:
|
||||||
|
sev, lvl_name = 4, "WARNING" # Fallback für unklare Meldungen
|
||||||
|
|
||||||
|
# Allgemeine Fehlerprüfung für andere Kategorien
|
||||||
|
elif any(x in m for x in ["fehler", "error", "failed", "abbruch", "gescheitert"]):
|
||||||
|
sev, lvl_name = 3, "ERROR"
|
||||||
|
|
||||||
|
elif any(x in m for x in ["warnung", "warning", "ursache", "seit"]) and "[" in m:
|
||||||
|
sev, lvl_name = 4, "WARNING"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ausgabe der ergebnisse
|
||||||
|
return cat, sev, lvl_name
|
||||||
|
|
||||||
|
def send_payload(msg, date_str=None, time_str=None, force_cat=None):
|
||||||
|
""" Baut das finale Paket und sendet an Syslog """
|
||||||
|
enriched_msg = VPNErrorLookup.translate(msg)
|
||||||
|
category, severity, lvl_name = get_meta(enriched_msg)
|
||||||
|
if force_cat: category = force_cat
|
||||||
|
|
||||||
|
# Zeitstempel
|
||||||
|
if EXTRACT_TIME and date_str and time_str:
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(f"{date_str} {time_str}", "%d.%m.%y %H:%M:%S")
|
||||||
|
ts = dt.strftime("%b %d %H:%M:%S")
|
||||||
|
except: ts = datetime.now().strftime("%b %d %H:%M:%S")
|
||||||
|
else: ts = datetime.now().strftime("%b %d %H:%M:%S")
|
||||||
|
|
||||||
|
# Basis JSON Objekt
|
||||||
|
log_obj = {
|
||||||
|
"timestamp_raw": f"{date_str} {time_str}" if date_str else ts,
|
||||||
|
"hostname": SENDER_NAME,
|
||||||
|
"category": category,
|
||||||
|
"level": lvl_name,
|
||||||
|
"message": enriched_msg.strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Erweiterte VPN-Metadaten
|
||||||
|
if category == "VPN":
|
||||||
|
p_ip, p_name = "0.0.0.0", "unknown"
|
||||||
|
ip_match = re.search(r"\[(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]", msg)
|
||||||
|
if ip_match: p_ip = ip_match.group(1)
|
||||||
|
name_match = re.search(r"zu\s+([^\s\[]+)", msg)
|
||||||
|
if name_match: p_name = name_match.group(1).strip(',').strip()
|
||||||
|
|
||||||
|
log_obj.update({
|
||||||
|
"vpn_peer_ip": p_ip,
|
||||||
|
"vpn_peer_name": p_name,
|
||||||
|
"vpn_error_code": re.search(r"0x[0-9a-fA-F]+", msg).group(0) if "0x" in msg else "None",
|
||||||
|
"vpn_phase": "Phase 1 (IKE)" if "IKE SA" in msg else "Phase 2 (IPsec)"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Paketbau
|
||||||
|
pri = (1 * 8) + severity
|
||||||
|
header = f"<{pri}>{ts} {SENDER_NAME} FRITZ_{category}: "
|
||||||
|
|
||||||
|
if JSON_ONLY:
|
||||||
|
packet = f"{header}{json.dumps(log_obj)}"
|
||||||
|
elif MIXED_JSON:
|
||||||
|
packet = f"{header}{enriched_msg.strip()}{SEPARATOR}{json.dumps(log_obj)}"
|
||||||
|
else:
|
||||||
|
packet = f"{header}{enriched_msg.strip()}"
|
||||||
|
|
||||||
|
if DEBUG_MODE: print(f"[DEBUG] OUT: {packet}", flush=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
handler.socket.sendto(packet.encode('utf-8'), (SYSLOG_SERVER, SYSLOG_PORT))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"!!! Sendefehler: {e}", flush=True)
|
||||||
|
|
||||||
|
class FritzLuaSession:
|
||||||
|
def __init__(self, host, user, pwd):
|
||||||
|
self.host, self.user, self.pwd, self.sid = host, user, pwd, "0000000000000000"
|
||||||
|
|
||||||
|
def get_sid(self):
|
||||||
|
url = f"http://{self.host}/login_sid.lua"
|
||||||
|
try:
|
||||||
|
r = requests.get(url, timeout=5)
|
||||||
|
challenge = ET.fromstring(r.text).find('Challenge').text
|
||||||
|
response = f"{challenge}-{hashlib.md5(f'{challenge}-{self.pwd}'.encode('utf-16-le')).hexdigest()}"
|
||||||
|
r = requests.get(f"{url}?username={self.user}&response={response}", timeout=5)
|
||||||
|
self.sid = ET.fromstring(r.text).find('SID').text
|
||||||
|
if DEBUG_MODE: print(f"[DEBUG] Login OK, SID: {self.sid}", flush=True)
|
||||||
|
return self.sid
|
||||||
|
except Exception as e:
|
||||||
|
print(f"!!! Login-Fehler: {e}", flush=True); return "0000000000000000"
|
||||||
|
|
||||||
|
def fetch_data(self, page):
|
||||||
|
if self.sid == "0000000000000000": self.get_sid()
|
||||||
|
try:
|
||||||
|
r = requests.post(f"http://{self.host}/data.lua", data={"sid": self.sid, "page": page}, timeout=10)
|
||||||
|
d = r.json()
|
||||||
|
if 'data' not in d and 'sid' in d: self.get_sid(); return None
|
||||||
|
return d
|
||||||
|
except: return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global last_hashes
|
||||||
|
print(f"=== Enterprise Monitor aktiv: {SENDER_NAME} ===", flush=True)
|
||||||
|
|
||||||
|
# 1. Hashes aus Persistenz laden (Zustand vor dem Stopp/Absturz)
|
||||||
|
if HASH_PERSISTENT:
|
||||||
|
load_hashes()
|
||||||
|
|
||||||
|
# 2. Verhalten bei SEND_INITIAL_LOG steuern
|
||||||
|
# Wenn True, wird der lokale Cache ignoriert/gelöscht, um alles in der Box neu zu pushen
|
||||||
|
if SEND_INITIAL:
|
||||||
|
print("[!] SEND_INITIAL_LOG aktiv: Lokaler Cache wird ignoriert -> Kompletter Neu-Push aller Box-Einträge.", flush=True)
|
||||||
|
last_hashes = set()
|
||||||
|
|
||||||
|
session = FritzLuaSession(FB_IP, FB_USER, FB_PWD)
|
||||||
|
vpn_states = {}
|
||||||
|
is_initial = True
|
||||||
|
new_hashes_since_backup = 0
|
||||||
|
|
||||||
|
# Backup-Intervall aus ENV (z.B. alle 20 neuen Hashes sichern)
|
||||||
|
backup_every = int(os.getenv("HASH_BACKUP_EVERY", 20))
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# --- TEIL 1: SYSTEM LOGS (Chronologischer Rückwärts-Sync) ---
|
||||||
|
log_data = session.fetch_data("log")
|
||||||
|
if log_data and 'data' in log_data and 'log' in log_data['data']:
|
||||||
|
entries = log_data['data']['log']
|
||||||
|
entries.reverse() # Wichtig: Alt nach Neu verarbeiten
|
||||||
|
|
||||||
|
for e in entries:
|
||||||
|
m, d, t = e.get('msg', ''), e.get('date', ''), e.get('time', '')
|
||||||
|
h = hashlib.sha1(f"{d}{t}{m}".encode()).hexdigest()
|
||||||
|
|
||||||
|
if h not in last_hashes:
|
||||||
|
# Senden wenn:
|
||||||
|
# a) Es kein Initial-Lauf ist (Normalbetrieb)
|
||||||
|
# b) Es der Initial-Lauf ist UND SEND_INITIAL_LOG auf True steht
|
||||||
|
if not is_initial or SEND_INITIAL:
|
||||||
|
send_payload(m, d, t)
|
||||||
|
|
||||||
|
# Neuen Hash in Cache aufnehmen und Zähler erhöhen
|
||||||
|
last_hashes.add(h)
|
||||||
|
new_hashes_since_backup += 1
|
||||||
|
|
||||||
|
# Inkrementelles Backup während des Laufs (beugt Datenverlust bei Absturz vor)
|
||||||
|
if HASH_PERSISTENT and new_hashes_since_backup >= backup_every:
|
||||||
|
save_hashes()
|
||||||
|
new_hashes_since_backup = 0
|
||||||
|
|
||||||
|
# --- TEIL 2: VPN MONITORING (Site-to-Site Fokus) ---
|
||||||
|
vpn_data = session.fetch_data("vpn")
|
||||||
|
if vpn_data and 'data' in vpn_data and 'vpnList' in vpn_data['data']:
|
||||||
|
for t in vpn_data['data']['vpnList']:
|
||||||
|
n, c = t.get('name'), t.get('connected', False)
|
||||||
|
|
||||||
|
# Statusänderung erkennen
|
||||||
|
if vpn_states.get(n) != c:
|
||||||
|
# VPN-Alarme nur nach dem ersten Scan senden (Spamschutz beim Boot)
|
||||||
|
if not is_initial:
|
||||||
|
v_msg = f"VPN Tunnel {n} ist {'UP' if c else 'DOWN'}"
|
||||||
|
send_payload(v_msg, force_cat="VPN")
|
||||||
|
|
||||||
|
vpn_states[n] = c
|
||||||
|
|
||||||
|
# --- TEIL 3: INITIAL-ABSCHLUSS & CACHE-PFLEGE ---
|
||||||
|
|
||||||
|
# Nach dem allerersten Durchlauf: Inkrementelle Hashes sofort sichern
|
||||||
|
if is_initial:
|
||||||
|
if HASH_PERSISTENT and new_hashes_since_backup > 0:
|
||||||
|
save_hashes()
|
||||||
|
new_hashes_since_backup = 0
|
||||||
|
is_initial = False
|
||||||
|
|
||||||
|
# Speichermanagement: Cache-Limit prüfen und ggf. rotieren
|
||||||
|
if len(last_hashes) > HASH_LIMIT:
|
||||||
|
# Behalte nur die neuesten X Hashes laut HASH_RETAIN
|
||||||
|
sorted_hashes = list(last_hashes)
|
||||||
|
last_hashes = set(sorted_hashes[-HASH_RETAIN:])
|
||||||
|
|
||||||
|
if DEBUG_MODE:
|
||||||
|
print(f"[DEBUG] Cache-Limit ({HASH_LIMIT}) erreicht. Bereinigt auf {HASH_RETAIN}.", flush=True)
|
||||||
|
|
||||||
|
# Nach der Bereinigung den neuen kompakten Stand sichern
|
||||||
|
if HASH_PERSISTENT:
|
||||||
|
save_hashes()
|
||||||
|
|
||||||
|
# Pause bis zum nächsten Polling-Intervall
|
||||||
|
time.sleep(INTERVAL)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
Reference in New Issue
Block a user