import hashlib import hmac import json import os from functools import wraps import psycopg from flask import Flask, Response, abort, redirect, render_template, request, url_for app = Flask(__name__) def db_connection(): return psycopg.connect( host=os.environ["DB_HOST"], port=os.getenv("DB_PORT", "5432"), dbname=os.environ["DB_NAME"], user=os.environ["DB_USER"], password=os.environ["DB_PASSWORD"], connect_timeout=5, ) def requires_auth(view): @wraps(view) def wrapped(*args, **kwargs): auth = __import__("flask").request.authorization expected_user = os.environ["DASHBOARD_USER"] expected_password = os.environ["DASHBOARD_PASSWORD"] valid = auth and hmac.compare_digest(auth.username or "", expected_user) and hmac.compare_digest(auth.password or "", expected_password) if not valid: return Response("Authentication required", 401, {"WWW-Authenticate": 'Basic realm="OCSentinel Debug"'}) return view(*args, **kwargs) return wrapped def csrf_token(): secret = os.environ["DASHBOARD_CSRF_SECRET"].encode("utf-8") return hmac.new(secret, b"recipient-rules", hashlib.sha256).hexdigest() def require_csrf(): supplied = request.form.get("csrf_token", "") if not hmac.compare_digest(supplied, csrf_token()): abort(400) @app.get("/") @requires_auth def overview(): with db_connection() as connection, connection.cursor() as cursor: cursor.execute("SELECT * FROM ocsentinel.organization_summary") summary = cursor.fetchone() cursor.execute( """ SELECT machine_name, organization_name, received_at, alert_state, total_events, unique_ip_count, cve_total, cve_critical FROM ( SELECT machine_name, received_at, alert_state, total_events, unique_ip_count, cve_total, cve_critical, payload #>> '{NinjaOne,OrganizationName}' AS organization_name FROM ocsentinel.current_device_status ) AS status ORDER BY received_at DESC NULLS LAST LIMIT 100 """ ) reports = cursor.fetchall() cursor.execute( """ SELECT machine_name, alert_state, total_events, unique_ip_count, received_at, payload FROM ocsentinel.current_device_status WHERE alert_state IN ('warning', 'critical') ORDER BY CASE alert_state WHEN 'critical' THEN 0 ELSE 1 END, received_at DESC """ ) alerts = cursor.fetchall() return render_template("overview.html", summary=summary, reports=reports, alerts=alerts) @app.get("/device/") @requires_auth def device(machine_name): with db_connection() as connection, connection.cursor() as cursor: cursor.execute( """ SELECT machine_name, first_seen_at, last_seen_at, last_client_version, generated_at_utc, received_at, alert_state, base_alert_state, total_events, unique_ip_count, cve_total, cve_critical, payload FROM ocsentinel.current_device_status WHERE machine_name = %s """, (machine_name,), ) report = cursor.fetchone() if report is None: abort(404) payload = report[12] return render_template("device.html", report=report, payload=payload, payload_pretty=json.dumps(payload, indent=2, ensure_ascii=False)) @app.get("/reports") @requires_auth def reports(): with db_connection() as connection, connection.cursor() as cursor: cursor.execute( """ SELECT id, organization_name, period_start_utc, period_end_utc, generated_at, device_count, warning_count, critical_count, total_events FROM ocsentinel.weekly_organization_report ORDER BY period_end_utc DESC, organization_name """ ) weekly_reports = cursor.fetchall() return render_template("reports.html", reports=weekly_reports) @app.get("/reports/") @requires_auth def weekly_report(report_id): with db_connection() as connection, connection.cursor() as cursor: cursor.execute( """ SELECT organization_name, period_start_utc, period_end_utc, generated_at, report_html FROM ocsentinel.weekly_organization_report WHERE id = %s """, (report_id,), ) report = cursor.fetchone() if report is None: abort(404) return render_template("weekly_report.html", report=report) @app.get("/recipients") @requires_auth def recipients(): with db_connection() as connection, connection.cursor() as cursor: cursor.execute( """ SELECT id, organization_id, organization_name, recipient_email, enabled FROM ocsentinel.organization_report_recipient ORDER BY organization_id = '*', organization_name, recipient_email """ ) rules = cursor.fetchall() cursor.execute( """ SELECT DISTINCT payload #>> '{NinjaOne,OrganizationId}', payload #>> '{NinjaOne,OrganizationName}' FROM ocsentinel.current_device_status WHERE coalesce(payload #>> '{NinjaOne,OrganizationId}', '') <> '' ORDER BY 2 """ ) organizations = cursor.fetchall() return render_template("recipients.html", rules=rules, organizations=organizations, csrf_token=csrf_token()) @app.post("/recipients") @requires_auth def add_recipient(): require_csrf() organization_id = request.form.get("organization_id", "").strip() organization_name = request.form.get("organization_name", "").strip() recipient_email = request.form.get("recipient_email", "").strip().lower() if not organization_id or not organization_name or "@" not in recipient_email: abort(400) with db_connection() as connection, connection.cursor() as cursor: cursor.execute( """ INSERT INTO ocsentinel.organization_report_recipient (organization_id, organization_name, recipient_email) VALUES (%s, %s, %s) ON CONFLICT (organization_id, recipient_email) DO NOTHING """, (organization_id, organization_name, recipient_email), ) connection.commit() return redirect(url_for("recipients")) @app.post("/recipients//toggle") @requires_auth def toggle_recipient(rule_id): require_csrf() with db_connection() as connection, connection.cursor() as cursor: cursor.execute( "UPDATE ocsentinel.organization_report_recipient SET enabled = NOT enabled WHERE id = %s", (rule_id,), ) connection.commit() return redirect(url_for("recipients")) @app.post("/recipients//delete") @requires_auth def delete_recipient(rule_id): require_csrf() with db_connection() as connection, connection.cursor() as cursor: cursor.execute("DELETE FROM ocsentinel.organization_report_recipient WHERE id = %s", (rule_id,)) connection.commit() return redirect(url_for("recipients")) @app.get("/healthz") def healthz(): try: with db_connection() as connection, connection.cursor() as cursor: cursor.execute("SELECT 1") return {"status": "ok"} except Exception: return {"status": "unavailable"}, 503 if __name__ == "__main__": app.run(host="0.0.0.0", port=8080)