From b97f8819f67677192b81a5938748be519e7b6acb Mon Sep 17 00:00:00 2001 From: OfficeCom Codex Date: Fri, 31 Jul 2026 00:39:36 +0200 Subject: [PATCH] Add fileserver context and map controls --- config/ocsentinel-settings.example.json | 1 + infra/debug-dashboard/app.py | 26 +++++++-- infra/debug-dashboard/static/app.css | 1 + infra/debug-dashboard/templates/device.html | 1 + infra/debug-dashboard/templates/network.html | 58 +++++++++++++------- src/OCSentinelCli/Configuration.cs | 2 + src/OCSentinelCli/Models.cs | 13 +++++ src/OCSentinelCli/RansomwareBetaDetector.cs | 53 +++++++++++++++++- 8 files changed, 129 insertions(+), 26 deletions(-) diff --git a/config/ocsentinel-settings.example.json b/config/ocsentinel-settings.example.json index 4e5d786..97366f5 100644 --- a/config/ocsentinel-settings.example.json +++ b/config/ocsentinel-settings.example.json @@ -14,6 +14,7 @@ "ransomwareLookbackMinutes": 15, "ransomwareWarningSignalCount": 2, "ransomwareCriticalSignalCount": 3, + "ransomwareCaptureSmbSessions": true, "ransomwareExcludedProcesses": [], "ftpRoots": [ "C:\\inetpub\\logs\\LogFiles", diff --git a/infra/debug-dashboard/app.py b/infra/debug-dashboard/app.py index a9ff1e0..0e7d51b 100644 --- a/infra/debug-dashboard/app.py +++ b/infra/debug-dashboard/app.py @@ -245,26 +245,31 @@ def organization(organization_id): ) -def load_network_flows(): +def load_network_flows(days=14): + days = max(1, min(days, 90)) 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' - """ + WHERE received_at >= now() - (%s * interval '1 day') + """, + (days,), ) reports = cursor.fetchall() flows = {} for machine_name, received_at, payload in reports: + ninja_context = (payload or {}).get("NinjaOne") or {} + organization_id = str(ninja_context.get("OrganizationId") or "unknown") + organization_name = ninja_context.get("OrganizationName") or "Organisation unbekannt" 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) + key = (source_ip, machine_name, account, target, organization_id) entry = flows.setdefault( key, { @@ -272,6 +277,8 @@ def load_network_flows(): "machine_name": machine_name, "account": account, "target": target, + "organization_id": organization_id, + "organization_name": organization_name, "count": 0, "last_seen": received_at, }, @@ -290,19 +297,24 @@ def load_network_flows(): @app.get("/network") def network(): - flows, _, source_count, target_count = load_network_flows() + days = request.args.get("days", 14, type=int) + flows, _, source_count, target_count = load_network_flows(days) return render_template( "network.html", source_count=source_count, target_count=target_count, total_events=sum(entry["count"] for entry in flows), path_count=len(flows), + days=max(1, min(days, 90)), + organizations=sorted({(entry["organization_id"], entry["organization_name"]) for entry in flows}, key=lambda item: item[1]), + event_types=sorted({entry["target"] for entry in flows}), ) @app.get("/api/network") def network_api(): - flows, max_count, source_count, target_count = load_network_flows() + days = request.args.get("days", 14, type=int) + flows, max_count, source_count, target_count = load_network_flows(days) nodes = {} edges = [] for index, flow in enumerate(flows): @@ -321,6 +333,8 @@ def network_api(): "event_type": flow["target"], "last_seen": str(flow["last_seen"] or "-"), "machine_name": flow["machine_name"], + "organization_id": flow["organization_id"], + "organization_name": flow["organization_name"], } } ) diff --git a/infra/debug-dashboard/static/app.css b/infra/debug-dashboard/static/app.css index 8f6b75a..ac3100a 100644 --- a/infra/debug-dashboard/static/app.css +++ b/infra/debug-dashboard/static/app.css @@ -30,3 +30,4 @@ table { width:100%; border-collapse:collapse; font-family:'Roboto',sans-serif; f .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; } } .network-map-panel { overflow:hidden; }.map-toolbar { display:flex; flex-wrap:wrap; gap:8px; margin:0 0 16px; }.map-toolbar label { flex:1 1 240px; display:grid; gap:5px; color:var(--muted); font:700 9px 'Roboto',sans-serif; letter-spacing:.1em; text-transform:uppercase; }.map-toolbar input,.map-toolbar button { min-height:38px; padding:8px 10px; border:1px solid var(--line); border-radius:5px; background:#fff; color:var(--ink); font:700 12px 'Roboto',sans-serif; }.map-toolbar button { cursor:pointer; background:#f5f9fc; }.network-map-layout { display:grid; grid-template-columns:minmax(0,1fr) 260px; min-height:560px; overflow:hidden; border:1px solid #d7e4ed; border-radius:9px; background:radial-gradient(circle at 18% 12%,#f5fbff,transparent 28rem),#edf4f8; }.network-map-layout #network-map { min-height:560px; background-image:linear-gradient(rgba(36,90,133,.05) 1px,transparent 1px),linear-gradient(90deg,rgba(36,90,133,.05) 1px,transparent 1px); background-size:32px 32px; }.network-map-layout aside { padding:22px; border-left:1px solid #d7e4ed; background:#fff; }.network-map-layout aside strong { display:block; margin:8px 0 12px; font-size:18px; line-height:1.15; letter-spacing:-.035em; }.network-map-layout aside p { color:var(--muted); font-size:13px; line-height:1.5; }.network-map-layout dl { display:grid; grid-template-columns:1fr; gap:4px; margin:18px 0 0; }.network-map-layout dt { color:var(--muted); font-size:10px; font-weight:700; text-transform:uppercase; }.network-map-layout dd { margin:0 0 10px; font-size:13px; overflow-wrap:anywhere; }.inspector-arrow { color:var(--green); font-size:13px; }.legend.source { background:#245a85; }.legend.target { background:#14735b; } @media (max-width:850px) { .network-map-layout { grid-template-columns:1fr; }.network-map-layout aside { border-top:1px solid #d7e4ed; border-left:0; }.network-map-layout #network-map { min-height:460px; } } +.map-hero { display:flex; align-items:end; justify-content:space-between; gap:28px; margin:0 -2vw 26px; padding:38px 3vw 30px; border-radius:14px; color:#eaf2f8; background:radial-gradient(circle at 82% 10%,rgba(79,163,223,.25),transparent 20rem),linear-gradient(132deg,#102a43,#0c1c2a 70%); box-shadow:0 18px 50px rgba(13,30,44,.22); }.map-hero h1 { max-width:650px; margin:8px 0 14px; font-size:clamp(42px,6vw,76px); line-height:.87; letter-spacing:-.07em; }.map-hero h1 em { color:#b8e36a; font-style:normal; }.map-hero p { max-width:620px; margin:0; color:#b7cad9; font-size:15px; line-height:1.55; }.map-hero .eyebrow { color:#9cc8e8; }.map-hero-status { display:grid; min-width:145px; gap:4px; padding:16px 18px; border:1px solid rgba(184,227,106,.35); border-radius:10px; background:rgba(11,31,45,.55); }.map-hero-status span { color:#b8e36a; font:700 9px 'Roboto',sans-serif; letter-spacing:.13em; }.map-hero-status strong { font-size:37px; line-height:1; letter-spacing:-.06em; }.map-hero-status small { color:#b7cad9; }.map-stat-strip { display:grid; grid-template-columns:repeat(4,1fr); gap:1px; margin:-10px 2vw 28px; border:1px solid #d8e5ee; border-radius:9px; overflow:hidden; background:#d8e5ee; box-shadow:0 10px 22px rgba(31,68,99,.08); }.map-stat-strip article { padding:15px 18px; background:#fff; }.map-stat-strip span,.map-stat-strip small { display:block; color:var(--muted); font:700 9px 'Roboto',sans-serif; letter-spacing:.1em; text-transform:uppercase; }.map-stat-strip strong { display:block; margin:8px 0 4px; font-size:29px; letter-spacing:-.06em; }.network-map-panel { margin-top:0; padding:0; border:0; border-radius:12px; background:#102a43; box-shadow:0 20px 44px rgba(16,42,67,.2); }.map-header { display:flex; justify-content:space-between; align-items:end; gap:16px; padding:24px 26px 18px; color:#eef6fa; }.map-header .eyebrow { color:#9cc8e8; }.map-header h2 { margin:6px 0 0; font-size:30px; letter-spacing:-.05em; }.map-legend { display:flex; gap:12px; color:#b7cad9; font-size:11px; }.map-legend span { display:flex; align-items:center; gap:5px; }.map-legend i { width:8px; height:8px; border-radius:50%; background:#4fa3df; }.map-legend i.target { background:#3bca99; border-radius:2px; }.map-legend i.hot { background:#ffb454; }.map-toolbar { align-items:end; margin:0; padding:0 26px 18px; border-bottom:1px solid rgba(156,200,232,.16); }.map-toolbar label { flex:0 1 180px; color:#9cc8e8; }.map-toolbar .search-field { flex:1 1 240px; }.map-toolbar input,.map-toolbar select,.map-toolbar button { min-height:40px; border:1px solid rgba(156,200,232,.24); border-radius:6px; background:#17374e; color:#eef6fa; font:600 12px 'Roboto',sans-serif; }.map-toolbar button { cursor:pointer; background:#245a85; }.map-toolbar button:hover { background:#326f9f; }.map-actions { display:flex; gap:7px; }.network-map-layout { grid-template-columns:minmax(0,1fr) 280px; min-height:610px; border:0; border-radius:0; background:#0d1e2c; }.network-map-layout #network-map { min-height:610px; background-image:radial-gradient(circle at 50% 0,rgba(79,163,223,.1),transparent 28rem),linear-gradient(rgba(156,200,232,.045) 1px,transparent 1px),linear-gradient(90deg,rgba(156,200,232,.045) 1px,transparent 1px); background-size:auto,36px 36px,36px 36px; }.network-map-layout aside { padding:24px; border-left:1px solid rgba(156,200,232,.16); background:#112b3d; color:#eef6fa; }.network-map-layout aside .eyebrow { color:#9cc8e8; }.network-map-layout aside p { color:#b7cad9; }.network-map-layout dt { color:#82b7dc; }.network-map-layout dd { color:#eef6fa; }.inspector-arrow { color:#b8e36a; }.legend.source { background:#4fa3df; }.legend.target { background:#3bca99; } @media (max-width:850px) { .map-hero { flex-direction:column; align-items:start; margin:0 0 20px; }.map-stat-strip { grid-template-columns:repeat(2,1fr); margin:0 0 20px; }.map-header { align-items:start; flex-direction:column; }.map-toolbar { padding:0 18px 18px; }.network-map-layout { grid-template-columns:1fr; }.network-map-layout aside { border-top:1px solid rgba(156,200,232,.16); border-left:0; } } diff --git a/infra/debug-dashboard/templates/device.html b/infra/debug-dashboard/templates/device.html index 417fe32..1384bc5 100644 --- a/infra/debug-dashboard/templates/device.html +++ b/infra/debug-dashboard/templates/device.html @@ -4,6 +4,7 @@

{{ report[0] }}

{{ report[6] }}{% if report[8] %}{{ 'aktuell' if event.is_current else 'historisch' }}: {{ event.label }}{% endif %}
Ereignisse{{ report[8] }}
Quell-IPs{{ report[9] }}
CVEs{{ report[10] }}
Kritische CVEs{{ report[11] }}
{% if ransomware_beta.enabled %}
Passive Beta

Ransomware-Frueherkennung {{ ransomware_beta.state }}

{{ ransomware_beta.reason }}

{% for signal in ransomware_beta.signals %}{% else %}{% endfor %}
ZeitpunktSignalProzessQuelleBewertung
{{ signal.timestamp }}{{ signal.category }}{{ signal.process }}{{ signal.source }}{{ signal.confidence }}
Keine Signale im aktuellen Beta-Zeitfenster.
{% endif %} +{% if ransomware_beta.smbSessions %}
Incident-Kontext

Aktive SMB-Sitzungen Nur bei Ransomware-Warnung oder kritisch erfasst

{% for session in ransomware_beta.smbSessions %}{% endfor %}
ClientBenutzerOffene DateienSitzung
{{ session.clientComputerName }}{{ session.clientUserName }}{{ session.openFileCount }}{{ session.sessionId }}
{% endif %}
Schnelluebersicht

Erkannte Sicherheitsereignisse

Fehlgeschlagene Anmeldungen und weitere Vorfaelle aus dem letzten Scan, nach Konto und Quell-IP zusammengefasst.

{% for entry in security_events %}{% else %}{% endfor %}
VorfallKontoQuell-IPLetzter ZeitpunktAnzahl
{{ entry.type }}{{ entry.account }}{{ entry.source_ip }}{{ entry.latest }}{{ entry.count }}
Keine sicherheitsrelevanten Ereignisse im letzten Scan.
Technische Daten

Roh-Export

Vollstaendige, unveraenderte Nutzlast des zuletzt eingegangenen Scans.

JSON-Rohdaten
{{ payload_pretty }}
{% endblock %} diff --git a/infra/debug-dashboard/templates/network.html b/infra/debug-dashboard/templates/network.html index d281104..468af89 100644 --- a/infra/debug-dashboard/templates/network.html +++ b/infra/debug-dashboard/templates/network.html @@ -1,31 +1,51 @@ {% 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 }}
Anzeige{{ path_count }} Pfade
-
Interaktive Karte

Zugriffs-Topologie Zoomen, verschieben oder einen Knoten bzw. Pfad auswaehlen

Quell-IP Zielgeraet Verbindung mit hoher Aktivitaet

+
+
Security topology

Zugriffswege
sichtbar machen.

Verdichtete fehlgeschlagene Anmeldungen aus den letzten {{ days }} Tagen. Die Karte zeigt nur beobachtete Pfade, keinen vollstaendigen Netzwerkverkehr.

+
LIVE DATASET{{ total_events }}beobachtete Versuche
+
+
Quellen{{ source_count }}externe IPs
Ziele{{ target_count }}Systeme
Pfade{{ path_count }}korrelierte Kanten
Zeitraum{{ days }}Tage Rueckblick
+
+
Interaktive Analyse

Access graph

Quell-IPZielgeraetHohe Aktivitaet
+
+ + + + +
+
+
+
diff --git a/src/OCSentinelCli/Configuration.cs b/src/OCSentinelCli/Configuration.cs index 576d980..c6a2599 100644 --- a/src/OCSentinelCli/Configuration.cs +++ b/src/OCSentinelCli/Configuration.cs @@ -34,6 +34,8 @@ internal sealed record ScannerConfiguration public int RansomwareCriticalSignalCount { get; init; } = 3; + public bool RansomwareCaptureSmbSessions { get; init; } = true; + public List RansomwareExcludedProcesses { get; init; } = []; public List FtpRoots { get; init; } = []; diff --git a/src/OCSentinelCli/Models.cs b/src/OCSentinelCli/Models.cs index e9ba721..acf717a 100644 --- a/src/OCSentinelCli/Models.cs +++ b/src/OCSentinelCli/Models.cs @@ -126,6 +126,19 @@ internal sealed record RansomwareBetaSummary public int LookbackMinutes { get; init; } public List Signals { get; init; } = []; + + public List SmbSessions { get; init; } = []; +} + +internal sealed record RansomwareSmbSession +{ + public string ClientComputerName { get; init; } = string.Empty; + + public string ClientUserName { get; init; } = string.Empty; + + public long SessionId { get; init; } + + public long OpenFileCount { get; init; } } internal sealed record RansomwareSignal diff --git a/src/OCSentinelCli/RansomwareBetaDetector.cs b/src/OCSentinelCli/RansomwareBetaDetector.cs index ef84bf6..ab112f4 100644 --- a/src/OCSentinelCli/RansomwareBetaDetector.cs +++ b/src/OCSentinelCli/RansomwareBetaDetector.cs @@ -1,5 +1,7 @@ +using System.Diagnostics; using System.Diagnostics.Eventing.Reader; using System.Runtime.Versioning; +using System.Text.Json; namespace OCSentinelCli; @@ -42,13 +44,17 @@ internal static class RansomwareBetaDetector _ => $"Ransomware beta found no suspicious process activity in the last {lookbackMinutes} minutes." }; + List smbSessions = state is "warning" or "critical" && configuration.RansomwareCaptureSmbSessions + ? CaptureSmbSessions(errors) + : []; return new RansomwareBetaSummary { Enabled = true, State = state, Reason = reason, LookbackMinutes = lookbackMinutes, - Signals = distinctSignals + Signals = distinctSignals, + SmbSessions = smbSessions }; } @@ -169,4 +175,49 @@ internal static class RansomwareBetaDetector return string.Empty; } } + + private static List CaptureSmbSessions(List errors) + { + try + { + using var process = Process.Start(new ProcessStartInfo + { + FileName = "powershell.exe", + Arguments = "-NoProfile -NonInteractive -Command \"Get-SmbSession | Select-Object ClientComputerName,ClientUserName,SessionId,NumOpens | ConvertTo-Json -Compress\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }); + if (process is null || !process.WaitForExit(5000) || process.ExitCode != 0) + { + return []; + } + + string json = process.StandardOutput.ReadToEnd(); + if (string.IsNullOrWhiteSpace(json)) + { + return []; + } + + JsonElement root = JsonSerializer.Deserialize(json, JsonOptions.Default); + IEnumerable rows = root.ValueKind == JsonValueKind.Array ? root.EnumerateArray().ToArray() : [root]; + return rows.Take(100).Select(row => new RansomwareSmbSession + { + ClientComputerName = GetJsonString(row, "ClientComputerName"), + ClientUserName = GetJsonString(row, "ClientUserName"), + SessionId = GetJsonLong(row, "SessionId"), + OpenFileCount = GetJsonLong(row, "NumOpens") + }).ToList(); + } + catch (Exception exception) + { + errors.Add($"Ransomware beta SMB snapshot failed: {exception.Message}"); + return []; + } + } + + private static string GetJsonString(JsonElement value, string name) => value.TryGetProperty(name, out JsonElement property) ? property.ToString() : string.Empty; + + private static long GetJsonLong(JsonElement value, string name) => value.TryGetProperty(name, out JsonElement property) && property.TryGetInt64(out long result) ? result : 0; }