Show event freshness and device coverage
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user