From c65001aa17f00a963bfac60f9ba9003ce33cc7fd Mon Sep 17 00:00:00 2001 From: OfficeCom Codex Date: Thu, 30 Jul 2026 01:16:20 +0200 Subject: [PATCH] Visualize observed access paths --- infra/debug-dashboard/app.py | 51 ++++++++++++++++++++ infra/debug-dashboard/static/app.css | 1 + infra/debug-dashboard/templates/base.html | 2 +- infra/debug-dashboard/templates/network.html | 7 +++ 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 infra/debug-dashboard/templates/network.html diff --git a/infra/debug-dashboard/app.py b/infra/debug-dashboard/app.py index 704caa0..f25779e 100644 --- a/infra/debug-dashboard/app.py +++ b/infra/debug-dashboard/app.py @@ -245,6 +245,57 @@ def organization(organization_id): ) +@app.get("/network") +def network(): + with db_connection() as connection, connection.cursor() as cursor: + cursor.execute( + """ + SELECT machine_name, received_at, payload + FROM ocsentinel.current_device_status + WHERE received_at >= now() - interval '14 days' + """ + ) + reports = cursor.fetchall() + + flows = {} + for machine_name, received_at, payload in reports: + for event in (payload or {}).get("Events", []): + source_ip = event.get("SourceIp") or "" + if not source_ip or source_ip in {"-", "127.0.0.1", "::1"}: + continue + account = event.get("Username") or "[unbekannt]" + target = event.get("Target") or "Anmeldung" + key = (source_ip, machine_name, account, target) + entry = flows.setdefault( + key, + { + "source_ip": source_ip, + "machine_name": machine_name, + "account": account, + "target": target, + "count": 0, + "last_seen": received_at, + }, + ) + entry["count"] += 1 + timestamp = event.get("Timestamp") + if timestamp and (entry["last_seen"] is None or str(timestamp) > str(entry["last_seen"])): + entry["last_seen"] = timestamp + + flow_rows = sorted(flows.values(), key=lambda entry: (entry["count"], str(entry["last_seen"])), reverse=True)[:60] + max_count = max([entry["count"] for entry in flow_rows] or [1]) + source_count = len({entry["source_ip"] for entry in flow_rows}) + target_count = len({entry["machine_name"] for entry in flow_rows}) + return render_template( + "network.html", + flows=flow_rows, + max_count=max_count, + source_count=source_count, + target_count=target_count, + total_events=sum(entry["count"] for entry in flow_rows), + ) + + @app.get("/device/") def device(machine_name): with db_connection() as connection, connection.cursor() as cursor: diff --git a/infra/debug-dashboard/static/app.css b/infra/debug-dashboard/static/app.css index 6266941..99a06db 100644 --- a/infra/debug-dashboard/static/app.css +++ b/infra/debug-dashboard/static/app.css @@ -28,3 +28,4 @@ table { width:100%; border-collapse:collapse; font-family:'Roboto',sans-serif; f .recipient-intro { max-width:720px; margin:6px 0 28px; }.recipient-intro h1 { margin:9px 0 10px; font-size:46px; line-height:1; letter-spacing:-.055em; }.recipient-intro p { margin:0; color:var(--muted); font-size:16px; line-height:1.55; }.recipient-intro strong { color:var(--ink); }.recipient-create-panel { margin-top:0; border-color:#c8dbe8; }.recipient-create-panel .panel-heading h2,.recipient-rules-panel .panel-heading h2 { margin:7px 0 8px; }.recipient-create-panel .panel-heading p { margin:0 0 20px; }.recipient-form button { white-space:nowrap; }.recipient-rules-panel { padding-bottom:12px; }.recipient-rules-panel .panel-heading { display:flex; align-items:end; justify-content:space-between; gap:16px; }.recipient-rules-panel .panel-heading h2 { margin-bottom:20px; }.recipient-rules-panel .panel-heading small { display:inline-block; margin-left:7px; padding:4px 7px; border-radius:12px; background:#edf4f8; color:#4d687b; font-size:10px; font-weight:700; letter-spacing:.04em; vertical-align:middle; }.recipient-table td { height:64px; }.recipient-table tr:last-child td { border-bottom:0; }.recipient-email { color:#245a85; font-weight:500; }.actions-heading { text-align:right; }.recipient-table .rule-actions { justify-content:flex-end; }.empty-state { padding:30px 10px !important; color:var(--muted); text-align:center; } .compact-metrics { grid-template-columns:repeat(4,1fr); }.event-summary-panel { margin-top:8px; }.event-summary-panel .panel-heading h2,.raw-export-panel .panel-heading h2 { margin:7px 0 8px; }.event-summary-panel .panel-heading p,.raw-export-panel .panel-heading p { margin:0 0 20px; }.event-count { display:inline-grid; min-width:28px; min-height:28px; place-items:center; border-radius:14px; background:#fff0d7; color:var(--amber); font:700 12px 'Roboto',sans-serif; }.raw-export-panel { margin-top:8px; }.raw-json { margin-top:18px; border-top:1px solid var(--line); }.raw-json summary { padding:14px 0; color:#245a85; cursor:pointer; font:700 12px 'Roboto',sans-serif; }.raw-json pre { margin-bottom:0; } @media (max-width:850px) { .compact-metrics { grid-template-columns:repeat(2,1fr); }.compact-metrics article:last-child { grid-column:span 2; } } .trend-panel { overflow:hidden; }.trend-chart { display:grid; grid-template-columns:repeat(auto-fit,minmax(48px,1fr)); align-items:end; min-height:210px; gap:10px; padding:18px 4px 0; border-bottom:1px solid var(--line); }.trend-day { display:grid; grid-template-rows:154px auto auto; gap:5px; min-width:0; text-align:center; }.trend-bar { position:relative; align-self:end; height:max(7px,var(--bar)); border-radius:5px 5px 0 0; background:#bfd9eb; transition:height .25s ease; }.trend-critical,.trend-warning { position:absolute; right:0; left:0; bottom:0; display:block; }.trend-critical { height:var(--critical); background:var(--red); }.trend-warning { bottom:var(--critical); height:var(--warning); background:var(--amber); }.trend-day strong { font-size:13px; }.trend-day small { color:var(--muted); font-size:10px; }.chart-note { margin:15px 0 0; color:var(--muted); font-size:11px; }.legend { display:inline-block; width:8px; height:8px; margin:0 4px 0 12px; border-radius:2px; }.legend:first-child { margin-left:0; }.legend.critical { background:var(--red); }.legend.warning { background:var(--amber); }.legend.neutral { background:#bfd9eb; }.organization-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(245px,1fr)); gap:12px; }.organization-card { display:grid; gap:11px; min-height:150px; padding:18px; border:1px solid #d7e3ec; border-radius:8px; background:linear-gradient(145deg,#fff,#f3f8fb); color:var(--ink); text-decoration:none; transition:transform .18s ease,box-shadow .18s ease,border-color .18s ease; }.organization-card:hover { border-color:#8fb7d0; box-shadow:0 14px 26px rgba(24,59,89,.12); transform:translateY(-2px); }.organization-card strong { font-size:19px; letter-spacing:-.035em; }.organization-card div { display:flex; flex-wrap:wrap; align-items:center; gap:5px; color:var(--muted); font-size:12px; }.organization-card small { color:var(--muted); font-size:10px; }.ransomware-panel { border-left:5px solid #8aa3b4; }.ransomware-panel.warning { border-left-color:var(--amber); }.ransomware-panel.critical { border-left-color:var(--red); }.ransomware-panel .panel-heading p { margin:0 0 18px; color:var(--muted); } +.network-panel { overflow:hidden; }.network-flows { display:grid; gap:8px; }.network-flow { display:grid; grid-template-columns:minmax(150px,.9fr) minmax(130px,1.25fr) minmax(210px,1.2fr); align-items:center; gap:16px; padding:13px 14px; border:1px solid #dce6ee; border-radius:7px; background:#fbfdff; color:var(--ink); text-decoration:none; transition:transform .18s ease,box-shadow .18s ease; }.network-flow:hover { transform:translateX(3px); box-shadow:0 8px 18px rgba(31,68,99,.1); }.flow-endpoint { display:grid; gap:3px; }.flow-endpoint span { color:var(--muted); font:700 9px 'Roboto',sans-serif; letter-spacing:.1em; text-transform:uppercase; }.flow-endpoint strong { font-size:14px; }.flow-endpoint small { color:var(--muted); font-size:11px; }.flow-line { position:relative; display:flex; align-items:center; gap:7px; min-height:22px; }.flow-line:before { position:absolute; right:0; left:0; height:3px; background:#d7e4ed; content:''; }.flow-line i { z-index:1; width:max(5%,var(--flow)); height:7px; border-radius:6px; background:linear-gradient(90deg,#245a85,#b8e36a); }.flow-line small { z-index:1; margin-left:auto; padding:2px 5px; border-radius:8px; background:#fff; color:#456174; font:700 10px 'Roboto',sans-serif; } @media (max-width:850px) { .network-flow { grid-template-columns:1fr; gap:9px; }.flow-line { order:3; }.flow-endpoint.target { order:2; } } diff --git a/infra/debug-dashboard/templates/base.html b/infra/debug-dashboard/templates/base.html index 12b40d9..55b34d8 100644 --- a/infra/debug-dashboard/templates/base.html +++ b/infra/debug-dashboard/templates/base.html @@ -12,7 +12,7 @@
OCSentinel - +
{% block content %}{% endblock %}
diff --git a/infra/debug-dashboard/templates/network.html b/infra/debug-dashboard/templates/network.html new file mode 100644 index 0000000..beff938 --- /dev/null +++ b/infra/debug-dashboard/templates/network.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} +{% block title %}Zugriffswege - OfficeCom Sentinel{% endblock %} +{% block content %} +
Letzte 14 Tage

Zugriffswege im Blick

Verdichtete fehlgeschlagene Anmeldungen. Jeder Pfad verbindet Quell-IP, Zielgeraet und betroffenes Konto.

+
Quell-IPs{{ source_count }}
Zielgeraete{{ target_count }}
Beobachtete Versuche{{ total_events }}
AnzeigeTop {{ flows|length }} Pfade
+
Flusskarte

Quell-IP zu Zielgeraet Balkenbreite entspricht der Anzahl der Versuche

{% for flow in flows %}
Quelle{{ flow.source_ip }}
{{ flow.count }}x
Ziel{{ flow.machine_name }}{{ flow.target }} | {{ flow.account }}
{% else %}

Keine externen Zugriffswege aus fehlgeschlagenen Anmeldungen im Zeitraum.

{% endfor %}
+{% endblock %}