Files
aza/AzA march 2026/aza_monitoring.py

316 lines
11 KiB
Python
Raw Normal View History

2026-03-25 22:03:39 +01:00
# -*- coding: utf-8 -*-
"""
AZA MedWork Monitoring & Integritaetspruefung.
Sammelt Health-Status, Metriken aus Audit-/Consent-Logs
und fuehrt Integritaetschecks durch. Keine Patientendaten.
Nutzung:
python aza_monitoring.py health -> Health-Checks
python aza_monitoring.py metrics -> Metriken aus Logs
python aza_monitoring.py integrity -> Integritaetspruefung
python aza_monitoring.py all -> Alles zusammen
python aza_monitoring.py nightly -> Nightly-Check (Integrity + Alert-Metriken)
"""
import json
import os
import sys
import time
import urllib.request
import ssl
from datetime import datetime, timezone
from pathlib import Path
_BASE_DIR = Path(__file__).resolve().parent
# =====================================================================
# 1) HEALTH CHECKS
# =====================================================================
_SERVICES = [
{"name": "backend_main", "url": os.getenv("AZA_BACKEND_URL", "https://127.0.0.1:8000/health")},
{"name": "transcribe_server", "url": os.getenv("AZA_TRANSCRIBE_URL", "https://127.0.0.1:8090/health")},
{"name": "todo_server", "url": os.getenv("AZA_TODO_URL", "https://127.0.0.1:5111/health")},
]
def check_health(services=None) -> list:
"""Prueft /health fuer alle konfigurierten Services."""
if services is None:
services = _SERVICES
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
results = []
for svc in services:
entry = {"name": svc["name"], "url": svc["url"], "status": "FAIL", "detail": ""}
try:
req = urllib.request.Request(svc["url"], method="GET")
resp = urllib.request.urlopen(req, timeout=5, context=ctx)
data = json.loads(resp.read().decode("utf-8"))
entry["status"] = "OK" if data.get("status") == "ok" else "WARN"
entry["version"] = data.get("version", "?")
entry["uptime_s"] = data.get("uptime_s", 0)
entry["tls"] = data.get("tls", False)
except Exception as e:
entry["detail"] = str(e)[:120]
results.append(entry)
return results
# =====================================================================
# 2) MONITORING-METRIKEN
# =====================================================================
def collect_metrics() -> dict:
"""Sammelt Metriken aus Audit-Log und Backup-Status."""
metrics = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"audit_log": {},
"consent_log": {},
"backup": {},
}
try:
from aza_audit_log import get_log_stats
metrics["audit_log"] = get_log_stats()
except Exception as e:
metrics["audit_log"] = {"error": str(e)[:100]}
try:
from aza_consent import verify_chain_integrity
ok, errs = verify_chain_integrity()
consent_file = _BASE_DIR / "aza_consent_log.json"
count = 0
if consent_file.exists():
try:
with open(consent_file, "r", encoding="utf-8") as f:
count = len(json.load(f))
except Exception:
pass
metrics["consent_log"] = {
"entries": count,
"integrity": "PASS" if ok else "FAIL",
}
except Exception as e:
metrics["consent_log"] = {"error": str(e)[:100]}
try:
backup_dir = _BASE_DIR / "backups"
if backup_dir.exists():
zips = sorted(backup_dir.glob("aza_backup_*.zip"), key=lambda p: p.stat().st_mtime, reverse=True)
metrics["backup"] = {
"count": len(zips),
"latest": zips[0].name if zips else None,
"latest_time": datetime.fromtimestamp(zips[0].stat().st_mtime, tz=timezone.utc).isoformat() if zips else None,
}
else:
metrics["backup"] = {"count": 0, "latest": None}
except Exception as e:
metrics["backup"] = {"error": str(e)[:100]}
return metrics
def get_alert_metrics() -> list:
"""Extrahiert sicherheitsrelevante Zaehler fuer Alerting."""
alerts = []
try:
from aza_audit_log import get_log_stats
stats = get_log_stats()
events = stats.get("events", {})
login_fail = events.get("LOGIN_FAIL", 0)
if login_fail > 0:
alerts.append({"metric": "login_fail_count", "value": login_fail, "severity": "WARN" if login_fail < 10 else "HIGH"})
ai_blocked = events.get("AI_BLOCKED", 0)
if ai_blocked > 0:
alerts.append({"metric": "ai_blocked_count", "value": ai_blocked, "severity": "INFO"})
ai_calls = events.get("AI_CHAT", 0) + events.get("AI_TRANSCRIBE", 0)
alerts.append({"metric": "ai_calls_total", "value": ai_calls, "severity": "INFO"})
twofa_fail = events.get("2FA_FAIL", 0)
if twofa_fail > 0:
alerts.append({"metric": "2fa_fail_count", "value": twofa_fail, "severity": "WARN" if twofa_fail < 5 else "HIGH"})
if stats.get("integrity") == "FAIL":
alerts.append({"metric": "audit_log_integrity", "value": "FAIL", "severity": "CRITICAL"})
except Exception as e:
alerts.append({"metric": "audit_log_read_error", "value": str(e)[:80], "severity": "HIGH"})
return alerts
# =====================================================================
# 3) INTEGRITAETS-CHECKS
# =====================================================================
def check_integrity() -> dict:
"""Prueft Audit-Log und Consent-Log Integritaet."""
results = {"timestamp": datetime.now(timezone.utc).isoformat(), "audit_log": {}, "consent_log": {}}
try:
from aza_audit_log import verify_integrity, verify_all_rotations, _LOG_FILE
if _LOG_FILE.exists():
ok_all, res_all = verify_all_rotations()
results["audit_log"] = {
"status": "PASS" if ok_all else "FAIL",
"files": {k: {"ok": v["ok"], "errors": v["errors"]} for k, v in res_all.items()},
}
else:
results["audit_log"] = {"status": "PASS", "note": "Keine Logdatei vorhanden"}
except Exception as e:
results["audit_log"] = {"status": "ERROR", "error": str(e)[:120]}
try:
from aza_consent import verify_chain_integrity
consent_file = _BASE_DIR / "aza_consent_log.json"
if consent_file.exists():
ok, errs = verify_chain_integrity()
results["consent_log"] = {
"status": "PASS" if ok else "FAIL",
"errors": errs,
}
else:
results["consent_log"] = {"status": "PASS", "note": "Keine Logdatei vorhanden"}
except Exception as e:
results["consent_log"] = {"status": "ERROR", "error": str(e)[:120]}
if results["audit_log"].get("status") == "FAIL" or results["consent_log"].get("status") == "FAIL":
try:
from aza_audit_log import log_event
log_event("INTEGRITY_FAIL", source="monitoring",
detail=f"audit={results['audit_log'].get('status')} consent={results['consent_log'].get('status')}")
except Exception:
pass
return results
# =====================================================================
# 4) NIGHTLY CHECK (alle Pruefungen + Ausgabe)
# =====================================================================
def run_nightly() -> dict:
"""Fuehrt alle naechtlichen Pruefungen durch."""
report = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"integrity": check_integrity(),
"alerts": get_alert_metrics(),
"metrics": collect_metrics(),
}
all_ok = (
report["integrity"]["audit_log"].get("status") in ("PASS", None)
and report["integrity"]["consent_log"].get("status") in ("PASS", None)
and not any(a.get("severity") in ("HIGH", "CRITICAL") for a in report["alerts"])
)
report["overall"] = "PASS" if all_ok else "ATTENTION"
return report
# =====================================================================
# CLI
# =====================================================================
def _print_health(results):
print(f"\n{'='*60}")
print("HEALTH CHECKS")
print(f"{'='*60}")
for r in results:
status = r["status"]
line = f" {r['name']:25s} {status:4s}"
if status == "OK":
line += f" v{r.get('version','?')} uptime={r.get('uptime_s',0)}s tls={r.get('tls','?')}"
else:
line += f" {r.get('detail','')}"
print(line)
def _print_metrics(m):
print(f"\n{'='*60}")
print("METRIKEN")
print(f"{'='*60}")
al = m.get("audit_log", {})
print(f" Audit-Log: {al.get('total_lines', '?')} Eintraege, "
f"Integritaet={al.get('integrity','?')}, "
f"Groesse={al.get('size_mb','?')} MB")
for ev, cnt in sorted(al.get("events", {}).items()):
print(f" {ev}: {cnt}")
cl = m.get("consent_log", {})
print(f" Consent-Log: {cl.get('entries','?')} Eintraege, Integritaet={cl.get('integrity','?')}")
bk = m.get("backup", {})
print(f" Backups: {bk.get('count','?')} vorhanden, letztes={bk.get('latest','keins')}")
if bk.get("latest_time"):
print(f" Zeitpunkt: {bk['latest_time']}")
def _print_integrity(r):
print(f"\n{'='*60}")
print("INTEGRITAETS-CHECKS")
print(f"{'='*60}")
for name in ("audit_log", "consent_log"):
info = r.get(name, {})
status = info.get("status", "?")
print(f" {name:15s} {status}")
for e in info.get("errors", []):
print(f" {e}")
if info.get("note"):
print(f" ({info['note']})")
def _print_alerts(alerts):
print(f"\n{'='*60}")
print("SICHERHEITS-ALERTS")
print(f"{'='*60}")
if not alerts:
print(" Keine Alerts.")
for a in alerts:
print(f" [{a['severity']:8s}] {a['metric']}: {a['value']}")
if __name__ == "__main__":
cmd = sys.argv[1] if len(sys.argv) > 1 else "all"
if cmd == "health":
_print_health(check_health())
elif cmd == "metrics":
_print_metrics(collect_metrics())
elif cmd == "integrity":
r = check_integrity()
_print_integrity(r)
ok = all(r[k].get("status") in ("PASS", None) for k in ("audit_log", "consent_log"))
sys.exit(0 if ok else 1)
elif cmd == "alerts":
_print_alerts(get_alert_metrics())
elif cmd == "nightly":
report = run_nightly()
_print_integrity(report["integrity"])
_print_alerts(report["alerts"])
print(f"\n GESAMT: {report['overall']}")
out = _BASE_DIR / f"monitoring_nightly_{datetime.now().strftime('%Y-%m-%d')}.json"
with open(out, "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(f" Report: {out}")
sys.exit(0 if report["overall"] == "PASS" else 1)
elif cmd == "all":
_print_health(check_health())
m = collect_metrics()
_print_metrics(m)
r = check_integrity()
_print_integrity(r)
_print_alerts(get_alert_metrics())
else:
print("Nutzung: python aza_monitoring.py [health|metrics|integrity|alerts|nightly|all]")
sys.exit(1)