# -*- 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)