Show event freshness and device coverage
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 47s

This commit is contained in:
OfficeCom Codex
2026-07-27 01:50:46 +02:00
parent 0fe8a96057
commit 5be4fb6c33
4 changed files with 107 additions and 11 deletions

View File

@@ -2,6 +2,7 @@ import hashlib
import hmac
import json
import os
from datetime import datetime, timezone
import psycopg
from flask import Flask, abort, redirect, render_template, request, url_for
@@ -9,6 +10,9 @@ from flask import Flask, abort, redirect, render_template, request, url_for
app = Flask(__name__)
CURRENT_EVENT_HOURS = 24
STALE_REPORT_HOURS = 36
def db_connection():
return psycopg.connect(
@@ -32,6 +36,39 @@ def require_csrf():
abort(400)
def event_metadata(payload):
latest_event = None
for event in (payload or {}).get("Events", []):
value = event.get("Timestamp")
if not value:
continue
try:
timestamp = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
continue
if timestamp.tzinfo is None:
timestamp = timestamp.replace(tzinfo=timezone.utc)
if latest_event is None or timestamp > latest_event:
latest_event = timestamp
if latest_event is None:
return {"is_current": False, "label": "keine Ereignisse", "timestamp": None}
age_seconds = max(0, int((datetime.now(timezone.utc) - latest_event.astimezone(timezone.utc)).total_seconds()))
if age_seconds < 3600:
age_label = f"vor {max(1, age_seconds // 60)} Min."
elif age_seconds < 86400:
age_label = f"vor {age_seconds // 3600} Std."
else:
age_label = f"vor {age_seconds // 86400} Tg."
return {
"is_current": age_seconds <= CURRENT_EVENT_HOURS * 3600,
"label": age_label,
"timestamp": latest_event,
}
@app.get("/")
def overview():
with db_connection() as connection, connection.cursor() as cursor:
@@ -41,11 +78,12 @@ def overview():
cursor.execute(
"""
SELECT machine_name, organization_name, received_at, alert_state,
total_events, unique_ip_count, cve_total, cve_critical
total_events, unique_ip_count, cve_total, cve_critical, payload
FROM (
SELECT machine_name, received_at, alert_state, total_events,
unique_ip_count, cve_total, cve_critical,
payload #>> '{NinjaOne,OrganizationName}' AS organization_name
payload #>> '{NinjaOne,OrganizationName}' AS organization_name,
payload
FROM ocsentinel.current_device_status
) AS status
ORDER BY received_at DESC NULLS LAST
@@ -54,6 +92,16 @@ def overview():
)
reports = cursor.fetchall()
cursor.execute(
"""
SELECT count(*) AS known_devices,
count(*) FILTER (WHERE received_at >= now() - interval '36 hours') AS reporting_devices,
count(*) FILTER (WHERE received_at IS NULL OR received_at < now() - interval '36 hours') AS stale_devices
FROM ocsentinel.current_device_status
"""
)
coverage = cursor.fetchone()
cursor.execute(
"""
SELECT machine_name, alert_state, total_events, unique_ip_count,
@@ -65,7 +113,43 @@ def overview():
)
alerts = cursor.fetchall()
return render_template("overview.html", summary=summary, reports=reports, alerts=alerts)
report_rows = []
for row in reports:
event = event_metadata(row[8])
report_rows.append(
{
"machine_name": row[0],
"organization_name": row[1],
"received_at": row[2],
"alert_state": row[3],
"total_events": row[4],
"unique_ip_count": row[5],
"event": event,
}
)
alert_rows = []
for row in alerts:
event = event_metadata(row[5])
alert_rows.append(
{
"machine_name": row[0],
"alert_state": row[1],
"total_events": row[2],
"unique_ip_count": row[3],
"received_at": row[4],
"event": event,
}
)
return render_template(
"overview.html",
summary=summary,
coverage=coverage,
reports=report_rows,
alerts=alert_rows,
current_alert_count=sum(alert["event"]["is_current"] for alert in alert_rows),
)
@app.get("/device/<machine_name>")
@@ -87,7 +171,13 @@ def device(machine_name):
abort(404)
payload = report[12]
return render_template("device.html", report=report, payload=payload, payload_pretty=json.dumps(payload, indent=2, ensure_ascii=False))
return render_template(
"device.html",
report=report,
event=event_metadata(payload),
payload=payload,
payload_pretty=json.dumps(payload, indent=2, ensure_ascii=False),
)
@app.get("/reports")