235 lines
7.6 KiB
Python
235 lines
7.6 KiB
Python
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/<machine_name>")
|
|
@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/<int:report_id>")
|
|
@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/<int:rule_id>/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/<int:rule_id>/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)
|