Simplify internal Sentinel dashboard
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Failing after 49s

This commit is contained in:
OfficeCom Codex
2026-07-27 01:04:49 +02:00
parent 6722b00ee7
commit 85f0682769
7 changed files with 21 additions and 59 deletions

View File

@@ -3,6 +3,4 @@ DB_PORT=5432
DB_NAME=ocsentinel DB_NAME=ocsentinel
DB_USER=ocsentinel_debug DB_USER=ocsentinel_debug
DB_PASSWORD=replace-with-server-generated-password DB_PASSWORD=replace-with-server-generated-password
DASHBOARD_USER=ocsentinel-debug
DASHBOARD_PASSWORD=replace-with-server-generated-password
DASHBOARD_CSRF_SECRET=replace-with-server-generated-secret DASHBOARD_CSRF_SECRET=replace-with-server-generated-secret

View File

@@ -2,10 +2,9 @@ import hashlib
import hmac import hmac
import json import json
import os import os
from functools import wraps
import psycopg import psycopg
from flask import Flask, Response, abort, redirect, render_template, request, url_for from flask import Flask, abort, redirect, render_template, request, url_for
app = Flask(__name__) app = Flask(__name__)
@@ -22,20 +21,6 @@ def db_connection():
) )
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(): def csrf_token():
secret = os.environ["DASHBOARD_CSRF_SECRET"].encode("utf-8") secret = os.environ["DASHBOARD_CSRF_SECRET"].encode("utf-8")
return hmac.new(secret, b"recipient-rules", hashlib.sha256).hexdigest() return hmac.new(secret, b"recipient-rules", hashlib.sha256).hexdigest()
@@ -48,7 +33,6 @@ def require_csrf():
@app.get("/") @app.get("/")
@requires_auth
def overview(): def overview():
with db_connection() as connection, connection.cursor() as cursor: with db_connection() as connection, connection.cursor() as cursor:
cursor.execute("SELECT * FROM ocsentinel.organization_summary") cursor.execute("SELECT * FROM ocsentinel.organization_summary")
@@ -85,7 +69,6 @@ def overview():
@app.get("/device/<machine_name>") @app.get("/device/<machine_name>")
@requires_auth
def device(machine_name): def device(machine_name):
with db_connection() as connection, connection.cursor() as cursor: with db_connection() as connection, connection.cursor() as cursor:
cursor.execute( cursor.execute(
@@ -108,7 +91,6 @@ def device(machine_name):
@app.get("/reports") @app.get("/reports")
@requires_auth
def reports(): def reports():
with db_connection() as connection, connection.cursor() as cursor: with db_connection() as connection, connection.cursor() as cursor:
cursor.execute( cursor.execute(
@@ -126,7 +108,6 @@ def reports():
@app.get("/reports/<int:report_id>") @app.get("/reports/<int:report_id>")
@requires_auth
def weekly_report(report_id): def weekly_report(report_id):
with db_connection() as connection, connection.cursor() as cursor: with db_connection() as connection, connection.cursor() as cursor:
cursor.execute( cursor.execute(
@@ -147,7 +128,6 @@ def weekly_report(report_id):
@app.get("/recipients") @app.get("/recipients")
@requires_auth
def recipients(): def recipients():
with db_connection() as connection, connection.cursor() as cursor: with db_connection() as connection, connection.cursor() as cursor:
cursor.execute( cursor.execute(
@@ -173,7 +153,6 @@ def recipients():
@app.post("/recipients") @app.post("/recipients")
@requires_auth
def add_recipient(): def add_recipient():
require_csrf() require_csrf()
organization_id = request.form.get("organization_id", "").strip() organization_id = request.form.get("organization_id", "").strip()
@@ -198,7 +177,6 @@ def add_recipient():
@app.post("/recipients/<int:rule_id>/toggle") @app.post("/recipients/<int:rule_id>/toggle")
@requires_auth
def toggle_recipient(rule_id): def toggle_recipient(rule_id):
require_csrf() require_csrf()
with db_connection() as connection, connection.cursor() as cursor: with db_connection() as connection, connection.cursor() as cursor:
@@ -211,7 +189,6 @@ def toggle_recipient(rule_id):
@app.post("/recipients/<int:rule_id>/delete") @app.post("/recipients/<int:rule_id>/delete")
@requires_auth
def delete_recipient(rule_id): def delete_recipient(rule_id):
require_csrf() require_csrf()
with db_connection() as connection, connection.cursor() as cursor: with db_connection() as connection, connection.cursor() as cursor:

View File

@@ -3,13 +3,12 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}OCSentinel Debug{% endblock %}</title> <title>{% block title %}OC Sentinel{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
</head> </head>
<body class="app-shell"> <body class="app-shell">
<header class="masthead"> <header class="masthead">
<a href="/" class="brand"><span>OC</span> Sentinel</a> <nav class="header-links"><a class="{{ 'active' if request.endpoint == 'overview' else '' }}" href="/">Uebersicht</a><a class="{{ 'active' if request.endpoint in ('reports', 'weekly_report') else '' }}" href="{{ url_for('reports') }}">Berichte</a><a class="{{ 'active' if request.endpoint in ('recipients', 'add_recipient', 'toggle_recipient', 'delete_recipient') else '' }}" href="{{ url_for('recipients') }}">Empfaenger</a></nav>
<div class="header-links"><a class="{{ 'active' if request.endpoint == 'overview' else '' }}" href="/">Sicherheitslage</a><a class="{{ 'active' if request.endpoint in ('reports', 'weekly_report') else '' }}" href="{{ url_for('reports') }}">Wochenberichte</a><a class="{{ 'active' if request.endpoint in ('recipients', 'add_recipient', 'toggle_recipient', 'delete_recipient') else '' }}" href="{{ url_for('recipients') }}">Empfaenger</a><div class="badge">interner Sicherheitsbereich</div></div>
</header> </header>
<main>{% block content %}{% endblock %}</main> <main>{% block content %}{% endblock %}</main>
</body> </body>

View File

@@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ report[0] }} - OC Sentinel{% endblock %} {% block title %}{{ report[0] }} - OC Sentinel{% endblock %}
{% block content %} {% block content %}
<section class="hero compact"><p class="eyebrow">Geraetedetails</p><h1>{{ report[0] }}</h1><p><span class="state {{ report[6] }}">{{ report[6] }}</span> | Letzte Datenmeldung: {{ report[5] }}</p></section> <section class="panel"><div class="panel-heading"><h2>{{ report[0] }}</h2><span class="state {{ report[6] }}">{{ report[6] }}</span></div></section>
<section class="metrics compact-metrics"><article><span>Ereignisse</span><strong>{{ report[8] }}</strong></article><article><span>Quell-IPs</span><strong>{{ report[9] }}</strong></article><article><span>CVEs</span><strong>{{ report[10] }}</strong></article><article><span>Kritische CVEs</span><strong class="critical">{{ report[11] }}</strong></article></section> <section class="metrics compact-metrics"><article><span>Ereignisse</span><strong>{{ report[8] }}</strong></article><article><span>Quell-IPs</span><strong>{{ report[9] }}</strong></article><article><span>CVEs</span><strong>{{ report[10] }}</strong></article><article><span>Kritische CVEs</span><strong class="critical">{{ report[11] }}</strong></article></section>
<section class="panel"><div class="panel-heading"><p class="eyebrow">Technische Details</p><h2>Signierter Sicherheitsbericht</h2><p>Unveraenderte Rohdaten zur Nachvollziehbarkeit und Fehleranalyse.</p></div><pre>{{ payload_pretty }}</pre></section> <section class="panel"><pre>{{ payload_pretty }}</pre></section>
{% endblock %} {% endblock %}

View File

@@ -1,28 +1,21 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<section class="hero">
<p class="eyebrow">OCSentinel Sicherheitszentrale</p>
<h1>OfficeCom Sentinal Übersicht</h1>
<p>Aktuelle Auswertung aller signierten Sicherheitsberichte aus den betreuten Organisationen.</p>
<div class="hero-note"><span></span> Zentraler Ueberblick ueber Geraete, Ereignisse und Handlungsbedarf</div>
</section>
<section class="situation {% if summary[2] %}critical{% elif summary[1] %}warning{% else %}ok{% endif %}"> <section class="situation {% if summary[2] %}critical{% elif summary[1] %}warning{% else %}ok{% endif %}">
<div><p class="eyebrow">Aktuelle Lage</p><strong>{% if summary[2] %}Kritische Ereignisse erfordern Aufmerksamkeit.{% elif summary[1] %}Hinweise vorhanden - bitte auffaellige Geraete pruefen.{% else %}Keine kritischen Auffaelligkeiten gemeldet.{% endif %}</strong></div> <div><strong>{% if summary[2] %}Kritische Ereignisse{% elif summary[1] %}Hinweise vorhanden{% else %}Keine kritischen Auffaelligkeiten{% endif %}</strong></div>
<span>{% if summary[2] %}KRITISCH{% elif summary[1] %}PRUEFEN{% else %}STABIL{% endif %}</span> <span>{% if summary[2] %}KRITISCH{% elif summary[1] %}PRUEFEN{% else %}STABIL{% endif %}</span>
</section> </section>
<section class="metrics"> <section class="metrics">
<article><span>Geraete mit Bericht</span><strong>{{ summary[0] }}</strong></article> <article><span>Geraete</span><strong>{{ summary[0] }}</strong></article>
<article><span>Warnungen</span><strong class="warning">{{ summary[1] }}</strong></article> <article><span>Warnungen</span><strong class="warning">{{ summary[1] }}</strong></article>
<article><span>Kritische Geraete</span><strong class="critical">{{ summary[2] }}</strong></article> <article><span>Kritisch</span><strong class="critical">{{ summary[2] }}</strong></article>
<article><span>Erkannte Ereignisse</span><strong>{{ summary[3] }}</strong></article> <article><span>Ereignisse</span><strong>{{ summary[3] }}</strong></article>
<article><span>Letzte Datenmeldung</span><strong class="timestamp">{{ summary[7] or 'noch keine Daten' }}</strong></article> <article><span>Letzte Meldung</span><strong class="timestamp">{{ summary[7] or '-' }}</strong></article>
</section> </section>
{% if alerts %} {% if alerts %}
<section class="panel alert-panel"> <section class="panel alert-panel">
<div class="panel-heading"><p class="eyebrow">Handlungsbedarf</p><h2>Auffaellige Geraete</h2><p>Diese Geraete haben zuletzt Warnungen oder kritische Sicherheitsereignisse gemeldet.</p></div> <div class="panel-heading"><h2>Auffaellige Geraete</h2></div>
<div class="alert-grid"> <div class="alert-grid">
{% for alert in alerts %} {% for alert in alerts %}
<a class="alert-card {{ alert[1] }}" href="{{ url_for('device', machine_name=alert[0]) }}"> <a class="alert-card {{ alert[1] }}" href="{{ url_for('device', machine_name=alert[0]) }}">
@@ -31,13 +24,11 @@
{% endfor %} {% endfor %}
</div> </div>
</section> </section>
{% else %}
<section class="panel calm-panel"><div class="panel-heading"><p class="eyebrow">Handlungsbedarf</p><h2>Keine auffaelligen Geraete</h2><p>Die zuletzt eingegangenen Berichte enthalten keine Warnungen oder kritischen Ereignisse.</p></div></section>
{% endif %} {% endif %}
<section class="panel"> <section class="panel">
<div class="panel-heading"><p class="eyebrow">Berichtsbestand</p><h2>Aktuelle Geraetestatus</h2><p>Jede Zeile zeigt den letzten erfolgreich uebermittelten OCSentinel-Bericht eines Geraets.</p></div> <div class="panel-heading"><h2>Geraetestatus</h2></div>
<div class="table-wrap"><table><thead><tr><th>Geraet</th><th>Organisation</th><th>Status</th><th>Ereignisse</th><th>Quell-IPs</th><th>Empfangen</th></tr></thead> <div class="table-wrap"><table><thead><tr><th>Geraet</th><th>Organisation</th><th>Status</th><th>Ereignisse</th><th>Quell-IPs</th><th>Empfangen</th></tr></thead>
<tbody>{% for row in reports %}<tr><td><a href="{{ url_for('device', machine_name=row[0]) }}">{{ row[0] }}</a></td><td>{{ row[1] or '-' }}</td><td><span class="state {{ row[3] }}">{{ row[3] }}</span></td><td>{{ row[4] }}</td><td>{{ row[5] }}</td><td>{{ row[2] or '-' }}</td></tr>{% else %}<tr><td colspan="6">Noch keine Geraeteberichte vorhanden.</td></tr>{% endfor %}</tbody></table></div> <tbody>{% for row in reports %}<tr><td><a href="{{ url_for('device', machine_name=row[0]) }}">{{ row[0] }}</a></td><td>{{ row[1] or '-' }}</td><td><span class="state {{ row[3] }}">{{ row[3] }}</span></td><td>{{ row[4] }}</td><td>{{ row[5] }}</td><td>{{ row[2] or '-' }}</td></tr>{% else %}<tr><td colspan="6">Keine Geraeteberichte.</td></tr>{% endfor %}</tbody></table></div>
</section> </section>
{% endblock %} {% endblock %}

View File

@@ -1,19 +1,17 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Berichtsempfaenger - OCSentinel{% endblock %} {% block title %}Empfaenger - OC Sentinel{% endblock %}
{% block content %} {% block content %}
<section class="hero compact"><p class="eyebrow">Wochenberichte</p><h1>Berichtsempfaenger</h1><p>Diese Regeln bestimmen, wer den Wochenbericht einer Organisation per E-Mail erhaelt.</p></section> <section class="panel"><div class="panel-heading"><h2>Empfaenger hinzufuegen</h2></div>
<section class="panel"><div class="panel-heading"><p class="eyebrow">Neue Regel</p><h2>Empfaenger hinzufuegen</h2></div>
<form class="recipient-form" method="post" action="{{ url_for('add_recipient') }}"> <form class="recipient-form" method="post" action="{{ url_for('add_recipient') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>Organisation<select name="organization_id" id="organization_id" required onchange="document.getElementById('organization_name').value=this.options[this.selectedIndex].dataset.name"><option value="*" data-name="Alle Organisationen">Alle Organisationen</option>{% for organization in organizations %}<option value="{{ organization[0] }}" data-name="{{ organization[1] }}">{{ organization[1] }}</option>{% endfor %}</select></label> <label>Organisation<select name="organization_id" id="organization_id" required onchange="document.getElementById('organization_name').value=this.options[this.selectedIndex].dataset.name"><option value="*" data-name="Alle Organisationen">Alle Organisationen</option>{% for organization in organizations %}<option value="{{ organization[0] }}" data-name="{{ organization[1] }}">{{ organization[1] }}</option>{% endfor %}</select></label>
<input type="hidden" name="organization_name" id="organization_name" value="Alle Organisationen"> <input type="hidden" name="organization_name" id="organization_name" value="Alle Organisationen">
<label>E-Mail-Adresse<input type="email" name="recipient_email" placeholder="name@officecom.it" required></label> <label>E-Mail-Adresse<input type="email" name="recipient_email" placeholder="name@officecom.it" required></label>
<button type="submit">Empfaenger speichern</button> <button type="submit">Speichern</button>
</form></section> </form></section>
<section class="panel"><div class="panel-heading"><p class="eyebrow">Aktive Regeln</p><h2>E-Mail-Verteiler</h2><p>"Alle Organisationen" wird zu jedem organisationsspezifischen Verteiler hinzugefuegt.</p></div> <section class="panel"><div class="panel-heading"><h2>E-Mail-Verteiler</h2></div>
<div class="table-wrap"><table><thead><tr><th>Organisation</th><th>E-Mail-Adresse</th><th>Status</th><th>Aktion</th></tr></thead><tbody> <div class="table-wrap"><table><thead><tr><th>Organisation</th><th>E-Mail-Adresse</th><th>Status</th><th>Aktion</th></tr></thead><tbody>
{% for rule in rules %}<tr><td>{{ rule[2] }}</td><td>{{ rule[3] }}</td><td><span class="state {{ 'ok' if rule[4] else 'warning' }}">{{ 'aktiv' if rule[4] else 'pausiert' }}</span></td><td class="rule-actions"><form method="post" action="{{ url_for('toggle_recipient', rule_id=rule[0]) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="button-secondary" type="submit">{{ 'Pausieren' if rule[4] else 'Aktivieren' }}</button></form><form method="post" action="{{ url_for('delete_recipient', rule_id=rule[0]) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="button-danger" type="submit">Loeschen</button></form></td></tr>{% else %}<tr><td colspan="4">Noch keine Empfaengerregeln angelegt.</td></tr>{% endfor %} {% for rule in rules %}<tr><td>{{ rule[2] }}</td><td>{{ rule[3] }}</td><td><span class="state {{ 'ok' if rule[4] else 'warning' }}">{{ 'aktiv' if rule[4] else 'pausiert' }}</span></td><td class="rule-actions"><form method="post" action="{{ url_for('toggle_recipient', rule_id=rule[0]) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="button-secondary" type="submit">{{ 'Pausieren' if rule[4] else 'Aktivieren' }}</button></form><form method="post" action="{{ url_for('delete_recipient', rule_id=rule[0]) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="button-danger" type="submit">Loeschen</button></form></td></tr>{% else %}<tr><td colspan="4">Keine Empfaengerregeln.</td></tr>{% endfor %}
</tbody></table></div></section> </tbody></table></div></section>
{% endblock %} {% endblock %}

View File

@@ -1,8 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Wochenberichte - OCSentinel Debug{% endblock %} {% block title %}Berichte - OC Sentinel{% endblock %}
{% block content %} {% block content %}
<section class="hero compact"><p class="eyebrow">Archiv</p><h1>Wochenberichte</h1><p>Je Organisation automatisch durch n8n erzeugt.</p></section>
<section class="panel"><div class="table-wrap"><table><thead><tr><th>Organisation</th><th>Zeitraum</th><th>Geraete</th><th>Warnung</th><th>Kritisch</th><th>Events</th><th>Erstellt</th></tr></thead><tbody> <section class="panel"><div class="table-wrap"><table><thead><tr><th>Organisation</th><th>Zeitraum</th><th>Geraete</th><th>Warnung</th><th>Kritisch</th><th>Events</th><th>Erstellt</th></tr></thead><tbody>
{% for row in reports %}<tr><td><a href="{{ url_for('weekly_report', report_id=row[0]) }}">{{ row[1] }}</a></td><td>{{ row[2] }} bis {{ row[3] }}</td><td>{{ row[5] }}</td><td>{{ row[6] }}</td><td>{{ row[7] }}</td><td>{{ row[8] }}</td><td>{{ row[4] }}</td></tr>{% else %}<tr><td colspan="7">Noch keine Wochenberichte erzeugt.</td></tr>{% endfor %} {% for row in reports %}<tr><td><a href="{{ url_for('weekly_report', report_id=row[0]) }}">{{ row[1] }}</a></td><td>{{ row[2] }} bis {{ row[3] }}</td><td>{{ row[5] }}</td><td>{{ row[6] }}</td><td>{{ row[7] }}</td><td>{{ row[8] }}</td><td>{{ row[4] }}</td></tr>{% else %}<tr><td colspan="7">Keine Wochenberichte.</td></tr>{% endfor %}
</tbody></table></div></section> </tbody></table></div></section>
{% endblock %} {% endblock %}