Add fileserver context and map controls
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 49s

This commit is contained in:
OfficeCom Codex
2026-07-31 00:39:36 +02:00
parent 0207d84775
commit b97f8819f6
8 changed files with 129 additions and 26 deletions

View File

@@ -14,6 +14,7 @@
"ransomwareLookbackMinutes": 15, "ransomwareLookbackMinutes": 15,
"ransomwareWarningSignalCount": 2, "ransomwareWarningSignalCount": 2,
"ransomwareCriticalSignalCount": 3, "ransomwareCriticalSignalCount": 3,
"ransomwareCaptureSmbSessions": true,
"ransomwareExcludedProcesses": [], "ransomwareExcludedProcesses": [],
"ftpRoots": [ "ftpRoots": [
"C:\\inetpub\\logs\\LogFiles", "C:\\inetpub\\logs\\LogFiles",

View File

@@ -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: with db_connection() as connection, connection.cursor() as cursor:
cursor.execute( cursor.execute(
""" """
SELECT machine_name, received_at, payload SELECT machine_name, received_at, payload
FROM ocsentinel.current_device_status FROM ocsentinel.current_device_status
WHERE received_at >= now() - interval '14 days' WHERE received_at >= now() - (%s * interval '1 day')
""" """,
(days,),
) )
reports = cursor.fetchall() reports = cursor.fetchall()
flows = {} flows = {}
for machine_name, received_at, payload in reports: 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", []): for event in (payload or {}).get("Events", []):
source_ip = event.get("SourceIp") or "" source_ip = event.get("SourceIp") or ""
if not source_ip or source_ip in {"-", "127.0.0.1", "::1"}: if not source_ip or source_ip in {"-", "127.0.0.1", "::1"}:
continue continue
account = event.get("Username") or "[unbekannt]" account = event.get("Username") or "[unbekannt]"
target = event.get("Target") or "Anmeldung" 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( entry = flows.setdefault(
key, key,
{ {
@@ -272,6 +277,8 @@ def load_network_flows():
"machine_name": machine_name, "machine_name": machine_name,
"account": account, "account": account,
"target": target, "target": target,
"organization_id": organization_id,
"organization_name": organization_name,
"count": 0, "count": 0,
"last_seen": received_at, "last_seen": received_at,
}, },
@@ -290,19 +297,24 @@ def load_network_flows():
@app.get("/network") @app.get("/network")
def 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( return render_template(
"network.html", "network.html",
source_count=source_count, source_count=source_count,
target_count=target_count, target_count=target_count,
total_events=sum(entry["count"] for entry in flows), total_events=sum(entry["count"] for entry in flows),
path_count=len(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") @app.get("/api/network")
def network_api(): 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 = {} nodes = {}
edges = [] edges = []
for index, flow in enumerate(flows): for index, flow in enumerate(flows):
@@ -321,6 +333,8 @@ def network_api():
"event_type": flow["target"], "event_type": flow["target"],
"last_seen": str(flow["last_seen"] or "-"), "last_seen": str(flow["last_seen"] or "-"),
"machine_name": flow["machine_name"], "machine_name": flow["machine_name"],
"organization_id": flow["organization_id"],
"organization_name": flow["organization_name"],
} }
} }
) )

View File

@@ -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); } .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-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; } } .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; } }

View File

@@ -4,6 +4,7 @@
<section class="panel"><div class="panel-heading"><h2>{{ report[0] }}</h2><span class="state {{ report[6] }}">{{ report[6] }}</span>{% if report[8] %}<span class="state {{ 'current' if event.is_current else 'historic' }}">{{ 'aktuell' if event.is_current else 'historisch' }}: {{ event.label }}</span>{% endif %}</div></section> <section class="panel"><div class="panel-heading"><h2>{{ report[0] }}</h2><span class="state {{ report[6] }}">{{ report[6] }}</span>{% if report[8] %}<span class="state {{ 'current' if event.is_current else 'historic' }}">{{ 'aktuell' if event.is_current else 'historisch' }}: {{ event.label }}</span>{% endif %}</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>
{% if ransomware_beta.enabled %}<section class="panel ransomware-panel {{ ransomware_beta.state }}"><div class="panel-heading"><span class="eyebrow">Passive Beta</span><h2>Ransomware-Frueherkennung <small>{{ ransomware_beta.state }}</small></h2><p>{{ ransomware_beta.reason }}</p></div><div class="table-wrap"><table><thead><tr><th>Zeitpunkt</th><th>Signal</th><th>Prozess</th><th>Quelle</th><th>Bewertung</th></tr></thead><tbody>{% for signal in ransomware_beta.signals %}<tr><td>{{ signal.timestamp }}</td><td>{{ signal.category }}</td><td>{{ signal.process }}</td><td>{{ signal.source }}</td><td><span class="state {{ 'critical' if signal.confidence == 'high' else 'warning' }}">{{ signal.confidence }}</span></td></tr>{% else %}<tr><td colspan="5" class="empty-state">Keine Signale im aktuellen Beta-Zeitfenster.</td></tr>{% endfor %}</tbody></table></div></section>{% endif %} {% if ransomware_beta.enabled %}<section class="panel ransomware-panel {{ ransomware_beta.state }}"><div class="panel-heading"><span class="eyebrow">Passive Beta</span><h2>Ransomware-Frueherkennung <small>{{ ransomware_beta.state }}</small></h2><p>{{ ransomware_beta.reason }}</p></div><div class="table-wrap"><table><thead><tr><th>Zeitpunkt</th><th>Signal</th><th>Prozess</th><th>Quelle</th><th>Bewertung</th></tr></thead><tbody>{% for signal in ransomware_beta.signals %}<tr><td>{{ signal.timestamp }}</td><td>{{ signal.category }}</td><td>{{ signal.process }}</td><td>{{ signal.source }}</td><td><span class="state {{ 'critical' if signal.confidence == 'high' else 'warning' }}">{{ signal.confidence }}</span></td></tr>{% else %}<tr><td colspan="5" class="empty-state">Keine Signale im aktuellen Beta-Zeitfenster.</td></tr>{% endfor %}</tbody></table></div></section>{% endif %}
{% if ransomware_beta.smbSessions %}<section class="panel smb-context-panel"><div class="panel-heading"><span class="eyebrow">Incident-Kontext</span><h2>Aktive SMB-Sitzungen <small>Nur bei Ransomware-Warnung oder kritisch erfasst</small></h2></div><div class="table-wrap"><table><thead><tr><th>Client</th><th>Benutzer</th><th>Offene Dateien</th><th>Sitzung</th></tr></thead><tbody>{% for session in ransomware_beta.smbSessions %}<tr><td>{{ session.clientComputerName }}</td><td>{{ session.clientUserName }}</td><td>{{ session.openFileCount }}</td><td>{{ session.sessionId }}</td></tr>{% endfor %}</tbody></table></div></section>{% endif %}
<section class="panel event-summary-panel"><div class="panel-heading"><span class="eyebrow">Schnelluebersicht</span><h2>Erkannte Sicherheitsereignisse</h2><p>Fehlgeschlagene Anmeldungen und weitere Vorfaelle aus dem letzten Scan, nach Konto und Quell-IP zusammengefasst.</p></div><div class="table-wrap"><table><thead><tr><th>Vorfall</th><th>Konto</th><th>Quell-IP</th><th>Letzter Zeitpunkt</th><th>Anzahl</th></tr></thead><tbody>{% for entry in security_events %}<tr><td><strong>{{ entry.type }}</strong></td><td>{{ entry.account }}</td><td>{{ entry.source_ip }}</td><td>{{ entry.latest }}</td><td><span class="event-count">{{ entry.count }}</span></td></tr>{% else %}<tr><td colspan="5" class="empty-state">Keine sicherheitsrelevanten Ereignisse im letzten Scan.</td></tr>{% endfor %}</tbody></table></div></section> <section class="panel event-summary-panel"><div class="panel-heading"><span class="eyebrow">Schnelluebersicht</span><h2>Erkannte Sicherheitsereignisse</h2><p>Fehlgeschlagene Anmeldungen und weitere Vorfaelle aus dem letzten Scan, nach Konto und Quell-IP zusammengefasst.</p></div><div class="table-wrap"><table><thead><tr><th>Vorfall</th><th>Konto</th><th>Quell-IP</th><th>Letzter Zeitpunkt</th><th>Anzahl</th></tr></thead><tbody>{% for entry in security_events %}<tr><td><strong>{{ entry.type }}</strong></td><td>{{ entry.account }}</td><td>{{ entry.source_ip }}</td><td>{{ entry.latest }}</td><td><span class="event-count">{{ entry.count }}</span></td></tr>{% else %}<tr><td colspan="5" class="empty-state">Keine sicherheitsrelevanten Ereignisse im letzten Scan.</td></tr>{% endfor %}</tbody></table></div></section>
<section class="panel raw-export-panel"><div class="panel-heading"><span class="eyebrow">Technische Daten</span><h2>Roh-Export</h2><p>Vollstaendige, unveraenderte Nutzlast des zuletzt eingegangenen Scans.</p></div><details class="raw-json" open><summary>JSON-Rohdaten</summary><pre>{{ payload_pretty }}</pre></details></section> <section class="panel raw-export-panel"><div class="panel-heading"><span class="eyebrow">Technische Daten</span><h2>Roh-Export</h2><p>Vollstaendige, unveraenderte Nutzlast des zuletzt eingegangenen Scans.</p></div><details class="raw-json" open><summary>JSON-Rohdaten</summary><pre>{{ payload_pretty }}</pre></details></section>
{% endblock %} {% endblock %}

View File

@@ -1,31 +1,51 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Zugriffswege - OfficeCom Sentinel{% endblock %} {% block title %}Zugriffswege - OfficeCom Sentinel{% endblock %}
{% block content %} {% block content %}
<section class="hero compact"><span class="eyebrow">Letzte 14 Tage</span><h1>Zugriffswege im Blick</h1><p>Verdichtete fehlgeschlagene Anmeldungen. Jeder Pfad verbindet Quell-IP, Zielgeraet und betroffenes Konto.</p></section> <section class="map-hero">
<section class="metrics compact-metrics"><article><span>Quell-IPs</span><strong>{{ source_count }}</strong></article><article><span>Zielgeraete</span><strong>{{ target_count }}</strong></article><article><span>Beobachtete Versuche</span><strong>{{ total_events }}</strong></article><article><span>Anzeige</span><strong class="timestamp">{{ path_count }} Pfade</strong></article></section> <div><span class="eyebrow">Security topology</span><h1>Zugriffswege<br><em>sichtbar machen.</em></h1><p>Verdichtete fehlgeschlagene Anmeldungen aus den letzten {{ days }} Tagen. Die Karte zeigt nur beobachtete Pfade, keinen vollstaendigen Netzwerkverkehr.</p></div>
<section class="panel network-map-panel"><div class="panel-heading"><span class="eyebrow">Interaktive Karte</span><h2>Zugriffs-Topologie <small>Zoomen, verschieben oder einen Knoten bzw. Pfad auswaehlen</small></h2></div><div class="map-toolbar"><label>Filtern<input id="map-filter" type="search" placeholder="IP oder Geraet suchen"></label><button type="button" id="map-fit">Gesamtansicht</button><button type="button" id="map-clear">Auswahl loeschen</button></div><div class="network-map-layout"><div id="network-map" aria-label="Interaktive Netzwerk- und Zugriffskarte"></div><aside id="network-inspector"><span class="eyebrow">Details</span><strong>Kein Element ausgewaehlt</strong><p>Waehle eine Quell-IP, ein Zielgeraet oder eine Verbindung in der Karte.</p></aside></div><p class="chart-note"><span class="legend source"></span>Quell-IP <span class="legend target"></span>Zielgeraet <span class="legend critical"></span>Verbindung mit hoher Aktivitaet</p></section> <div class="map-hero-status"><span>LIVE DATASET</span><strong>{{ total_events }}</strong><small>beobachtete Versuche</small></div>
</section>
<section class="map-stat-strip"><article><span>Quellen</span><strong>{{ source_count }}</strong><small>externe IPs</small></article><article><span>Ziele</span><strong>{{ target_count }}</strong><small>Systeme</small></article><article><span>Pfade</span><strong>{{ path_count }}</strong><small>korrelierte Kanten</small></article><article><span>Zeitraum</span><strong>{{ days }}</strong><small>Tage Rueckblick</small></article></section>
<section class="network-map-panel">
<header class="map-header"><div><span class="eyebrow">Interaktive Analyse</span><h2>Access graph</h2></div><div class="map-legend"><span><i class="source"></i>Quell-IP</span><span><i class="target"></i>Zielgeraet</span><span><i class="hot"></i>Hohe Aktivitaet</span></div></header>
<div class="map-toolbar">
<label class="search-field"><span>Suchen</span><input id="map-filter" type="search" placeholder="IP, Geraet oder Konto"></label>
<label><span>Zeitraum</span><select id="map-range"><option value="1" {% if days == 1 %}selected{% endif %}>24 Stunden</option><option value="7" {% if days == 7 %}selected{% endif %}>7 Tage</option><option value="14" {% if days == 14 %}selected{% endif %}>14 Tage</option><option value="30" {% if days == 30 %}selected{% endif %}>30 Tage</option></select></label>
<label><span>Organisation</span><select id="map-organization"><option value="">Alle Organisationen</option>{% for organization in organizations %}<option value="{{ organization[0] }}">{{ organization[1] }}</option>{% endfor %}</select></label>
<label><span>Vorfall</span><select id="map-event"><option value="">Alle Vorfaelle</option>{% for event_type in event_types %}<option value="{{ event_type }}">{{ event_type }}</option>{% endfor %}</select></label>
<div class="map-actions"><button type="button" id="map-fit">Gesamtansicht</button><button type="button" id="map-export">JSON</button><button type="button" id="map-print">Drucken</button></div>
</div>
<div class="network-map-layout"><div id="network-map" aria-label="Interaktive Netzwerk- und Zugriffskarte"></div><aside id="network-inspector"><span class="eyebrow">Inspector</span><strong>Kein Element ausgewaehlt</strong><p>Waehle einen Knoten oder einen Pfad. Zugehoerige Verbindungen werden hervorgehoben.</p></aside></div>
</section>
<script src="{{ url_for('static', filename='vendor/cytoscape.min.js') }}"></script> <script src="{{ url_for('static', filename='vendor/cytoscape.min.js') }}"></script>
<script> <script>
(() => { (() => {
const inspector = document.getElementById('network-inspector'); const inspector = document.getElementById('network-inspector');
const filter = document.getElementById('map-filter'); const filter = document.getElementById('map-filter');
fetch('{{ url_for("network_api") }}').then(response => response.json()).then(graph => { const organization = document.getElementById('map-organization');
const cy = cytoscape({ container: document.getElementById('network-map'), elements: graph.elements, minZoom: 0.3, maxZoom: 2.4, const eventType = document.getElementById('map-event');
const apiUrl = '{{ url_for("network_api") }}?days={{ days }}';
fetch(apiUrl).then(response => response.json()).then(graph => {
const cy = cytoscape({ container: document.getElementById('network-map'), elements: graph.elements, minZoom: .3, maxZoom: 2.4,
style: [ style: [
{ selector: 'node', style: { 'label': 'data(label)', 'font-family': 'Roboto', 'font-size': 11, 'font-weight': 700, 'color': '#132a3d', 'text-valign': 'bottom', 'text-margin-y': 7, 'width': 46, 'height': 46, 'border-width': 3, 'border-color': '#fff' } }, { selector: 'node', style: { 'label': 'data(label)', 'font-family': 'Roboto', 'font-size': 11, 'font-weight': 700, 'color': '#eaf2f8', 'text-valign': 'bottom', 'text-margin-y': 8, 'text-outline-width': 3, 'text-outline-color': '#0d1e2c', 'width': 48, 'height': 48, 'border-width': 2, 'border-color': '#d9f5ed' } },
{ selector: 'node[kind = "source"]', style: { 'background-color': '#245a85', 'shape': 'ellipse' } }, { selector: 'node[kind = "source"]', style: { 'background-color': '#4fa3df', 'shape': 'ellipse' } },
{ selector: 'node[kind = "target"]', style: { 'background-color': '#14735b', 'shape': 'round-rectangle' } }, { selector: 'node[kind = "target"]', style: { 'background-color': '#3bca99', 'shape': 'round-rectangle' } },
{ selector: 'edge', style: { 'width': 'mapData(count, 1, ' + graph.max_count + ', 2, 9)', 'line-color': '#91b5ca', 'target-arrow-color': '#91b5ca', 'target-arrow-shape': 'triangle', 'curve-style': 'bezier', 'opacity': .72 } }, { selector: 'edge', style: { 'width': 'mapData(count, 1, ' + graph.max_count + ', 2, 10)', 'line-color': '#406b86', 'target-arrow-color': '#406b86', 'target-arrow-shape': 'triangle', 'curve-style': 'bezier', 'opacity': .75 } },
{ selector: 'edge[count >= 5]', style: { 'line-color': '#a55a0a', 'target-arrow-color': '#a55a0a' } }, { selector: 'edge[count >= 5]', style: { 'line-color': '#ffb454', 'target-arrow-color': '#ffb454' } },
{ selector: '.selected', style: { 'border-color': '#b8e36a', 'border-width': 6, 'line-color': '#b8e36a', 'target-arrow-color': '#b8e36a', 'opacity': 1, 'z-index': 20 } }, { selector: '.selected', style: { 'border-color': '#e7ff88', 'border-width': 6, 'line-color': '#e7ff88', 'target-arrow-color': '#e7ff88', 'opacity': 1, 'z-index': 20 } },
{ selector: '.muted', style: { 'opacity': .12 } } { selector: '.hidden', style: { 'display': 'none' } }
], layout: { name: 'cose', animate: false, padding: 38, nodeRepulsion: 8200, idealEdgeLength: 130, gravity: .22 } }); ], layout: { name: 'cose', animate: false, padding: 52, nodeRepulsion: 9000, idealEdgeLength: 145, gravity: .2 } });
const show = element => { const data = element.data(); if (element.isEdge()) { inspector.innerHTML = '<span class="eyebrow">Zugriffsweg</span><strong>' + data.source.replace('source:', '') + ' <span class="inspector-arrow">to</span> ' + data.machine_name + '</strong><dl><dt>Versuche</dt><dd>' + data.count + '</dd><dt>Konto</dt><dd>' + data.account + '</dd><dt>Vorfall</dt><dd>' + data.event_type + '</dd><dt>Letzter Scan</dt><dd>' + data.last_seen + '</dd></dl>'; } else { const connected = element.connectedEdges(); inspector.innerHTML = '<span class="eyebrow">' + (data.kind === 'source' ? 'Quell-IP' : 'Zielgeraet') + '</span><strong>' + data.label + '</strong><p>' + connected.length + ' beobachtete Zugriffswege im Zeitraum.</p>'; } }; const resetInspector = () => inspector.innerHTML = '<span class="eyebrow">Inspector</span><strong>Kein Element ausgewaehlt</strong><p>Waehle einen Knoten oder einen Pfad. Zugehoerige Verbindungen werden hervorgehoben.</p>';
cy.on('tap', 'node, edge', event => { cy.elements().removeClass('selected'); event.target.addClass('selected'); if (event.target.isNode()) { event.target.connectedEdges().addClass('selected'); } show(event.target); }); const show = element => { const data = element.data(); if (element.isEdge()) { inspector.innerHTML = '<span class="eyebrow">Observed path</span><strong>' + data.source.replace('source:', '') + ' <span class="inspector-arrow">to</span> ' + data.machine_name + '</strong><dl><dt>Versuche</dt><dd>' + data.count + '</dd><dt>Organisation</dt><dd>' + data.organization_name + '</dd><dt>Konto</dt><dd>' + data.account + '</dd><dt>Vorfall</dt><dd>' + data.event_type + '</dd><dt>Letzter Scan</dt><dd>' + data.last_seen + '</dd></dl>'; } else { const connected = element.connectedEdges(':visible'); inspector.innerHTML = '<span class="eyebrow">' + (data.kind === 'source' ? 'Quell-IP' : 'Zielgeraet') + '</span><strong>' + data.label + '</strong><p>' + connected.length + ' sichtbare Zugriffswege im aktuellen Filter.</p>'; } };
cy.on('tap', event => { if (event.target === cy) { cy.elements().removeClass('selected'); inspector.innerHTML = '<span class="eyebrow">Details</span><strong>Kein Element ausgewaehlt</strong><p>Waehle eine Quell-IP, ein Zielgeraet oder eine Verbindung in der Karte.</p>'; } }); const applyFilters = () => { const term = filter.value.trim().toLowerCase(); const org = organization.value; const type = eventType.value; cy.elements().addClass('hidden'); const visible = cy.edges().filter(edge => { const d = edge.data(); return (!term || [d.source, d.machine_name, d.account, d.event_type].join(' ').toLowerCase().includes(term)) && (!org || d.organization_id === org) && (!type || d.event_type === type); }); visible.removeClass('hidden'); visible.connectedNodes().removeClass('hidden'); cy.layout({ name:'cose', animate:false, padding:52, nodeRepulsion:9000, idealEdgeLength:145, gravity:.2 }).run(); resetInspector(); };
filter.addEventListener('input', () => { const value = filter.value.trim().toLowerCase(); cy.elements().removeClass('muted'); if (!value) return; cy.elements().addClass('muted'); const hits = cy.nodes().filter(node => node.data('label').toLowerCase().includes(value)); hits.removeClass('muted'); hits.connectedEdges().removeClass('muted'); hits.connectedNodes().removeClass('muted'); }); cy.on('tap', 'node, edge', event => { cy.elements().removeClass('selected'); event.target.addClass('selected'); if (event.target.isNode()) event.target.connectedEdges(':visible').addClass('selected'); show(event.target); });
document.getElementById('map-fit').addEventListener('click', () => cy.fit(cy.elements(), 35)); cy.on('tap', event => { if (event.target === cy) { cy.elements().removeClass('selected'); resetInspector(); } });
document.getElementById('map-clear').addEventListener('click', () => { filter.value = ''; cy.elements().removeClass('muted selected'); }); filter.addEventListener('input', applyFilters); organization.addEventListener('change', applyFilters); eventType.addEventListener('change', applyFilters);
document.getElementById('map-range').addEventListener('change', e => { window.location.search = 'days=' + e.target.value; });
document.getElementById('map-fit').addEventListener('click', () => cy.fit(cy.elements(':visible'), 48));
document.getElementById('map-export').addEventListener('click', () => { const data = cy.json().elements; const blob = new Blob([JSON.stringify(data, null, 2)], {type:'application/json'}); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'ocsentinel-access-map.json'; link.click(); URL.revokeObjectURL(link.href); });
document.getElementById('map-print').addEventListener('click', () => window.print());
}).catch(() => { document.getElementById('network-map').textContent = 'Die Netzwerkdaten konnten nicht geladen werden.'; }); }).catch(() => { document.getElementById('network-map').textContent = 'Die Netzwerkdaten konnten nicht geladen werden.'; });
})(); })();
</script> </script>

View File

@@ -34,6 +34,8 @@ internal sealed record ScannerConfiguration
public int RansomwareCriticalSignalCount { get; init; } = 3; public int RansomwareCriticalSignalCount { get; init; } = 3;
public bool RansomwareCaptureSmbSessions { get; init; } = true;
public List<string> RansomwareExcludedProcesses { get; init; } = []; public List<string> RansomwareExcludedProcesses { get; init; } = [];
public List<string> FtpRoots { get; init; } = []; public List<string> FtpRoots { get; init; } = [];

View File

@@ -126,6 +126,19 @@ internal sealed record RansomwareBetaSummary
public int LookbackMinutes { get; init; } public int LookbackMinutes { get; init; }
public List<RansomwareSignal> Signals { get; init; } = []; public List<RansomwareSignal> Signals { get; init; } = [];
public List<RansomwareSmbSession> 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 internal sealed record RansomwareSignal

View File

@@ -1,5 +1,7 @@
using System.Diagnostics;
using System.Diagnostics.Eventing.Reader; using System.Diagnostics.Eventing.Reader;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Text.Json;
namespace OCSentinelCli; namespace OCSentinelCli;
@@ -42,13 +44,17 @@ internal static class RansomwareBetaDetector
_ => $"Ransomware beta found no suspicious process activity in the last {lookbackMinutes} minutes." _ => $"Ransomware beta found no suspicious process activity in the last {lookbackMinutes} minutes."
}; };
List<RansomwareSmbSession> smbSessions = state is "warning" or "critical" && configuration.RansomwareCaptureSmbSessions
? CaptureSmbSessions(errors)
: [];
return new RansomwareBetaSummary return new RansomwareBetaSummary
{ {
Enabled = true, Enabled = true,
State = state, State = state,
Reason = reason, Reason = reason,
LookbackMinutes = lookbackMinutes, LookbackMinutes = lookbackMinutes,
Signals = distinctSignals Signals = distinctSignals,
SmbSessions = smbSessions
}; };
} }
@@ -169,4 +175,49 @@ internal static class RansomwareBetaDetector
return string.Empty; return string.Empty;
} }
} }
private static List<RansomwareSmbSession> CaptureSmbSessions(List<string> 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<JsonElement>(json, JsonOptions.Default);
IEnumerable<JsonElement> 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;
} }