14 Commits

Author SHA1 Message Date
OfficeCom Codex
072a4257ed Report ransomware sensor coverage in beta
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 22s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 51s
2026-08-01 01:41:57 +02:00
OfficeCom Codex
47fe9448b7 Publish beta 1.5.0-beta.4 manifest
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 47s
2026-08-01 01:39:17 +02:00
OfficeCom Codex
fd990b698f Enable passive ransomware detection by default for beta
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 50s
2026-08-01 01:35:58 +02:00
OfficeCom Codex
d1bbd8838f Publish beta 1.5.0-beta.3 manifest
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 48s
2026-08-01 01:32:55 +02:00
OfficeCom Codex
feeeeedcf1 Remove redundant Ninja client version field
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 50s
2026-08-01 01:29:21 +02:00
OfficeCom Codex
ceefb7a5dc Publish beta 1.5.0-beta.2 manifest
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 49s
2026-08-01 01:19:58 +02:00
OfficeCom Codex
8407d0c5b2 Add passive file churn sensor beta
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 24s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 50s
2026-08-01 01:15:40 +02:00
OfficeCom Codex
22be9689e2 Use cache-safe stable release asset URL
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 24s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 48s
2026-08-01 01:00:25 +02:00
OfficeCom Codex
f69f14e0b1 Fail context migration when upload is unavailable
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 25s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 50s
2026-08-01 00:42:11 +02:00
OfficeCom Codex
94f5be8953 Persist NinjaOne context for scheduled scans
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 24s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 50s
2026-07-31 01:59:40 +02:00
OfficeCom Codex
b97f8819f6 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
2026-07-31 00:39:36 +02:00
OfficeCom Codex
0207d84775 Build interactive access topology map
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 48s
2026-07-30 01:37:50 +02:00
OfficeCom Codex
c65001aa17 Visualize observed access paths
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 48s
2026-07-30 01:16:20 +02:00
OfficeCom Codex
42b387f3ef Align beta manifest with runner artifact
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 48s
2026-07-30 01:09:18 +02:00
32 changed files with 1097 additions and 48 deletions

View File

@@ -10,10 +10,19 @@
"criticalSprayAccountCount": 10, "criticalSprayAccountCount": 10,
"correlationWarningCveThreshold": 1, "correlationWarningCveThreshold": 1,
"correlationCriticalCveThreshold": 1, "correlationCriticalCveThreshold": 1,
"ransomwareBetaEnabled": false, "ransomwareBetaEnabled": true,
"ransomwareBetaAlertingEnabled": false,
"ransomwareLookbackMinutes": 15, "ransomwareLookbackMinutes": 15,
"ransomwareWarningSignalCount": 2, "ransomwareWarningSignalCount": 2,
"ransomwareCriticalSignalCount": 3, "ransomwareCriticalSignalCount": 3,
"ransomwareCaptureSmbSessions": true,
"ransomwareFileChurnEnabled": false,
"ransomwareFileChurnWindowMinutes": 15,
"ransomwareFileChurnWarningDeleteCount": 50,
"ransomwareFileChurnWarningWriteCount": 250,
"ransomwareFileChurnCriticalDeleteCount": 200,
"ransomwareFileChurnCriticalWriteCount": 1000,
"ransomwareFileChurnMaxAuditEvents": 5000,
"ransomwareExcludedProcesses": [], "ransomwareExcludedProcesses": [],
"ftpRoots": [ "ftpRoots": [
"C:\\inetpub\\logs\\LogFiles", "C:\\inetpub\\logs\\LogFiles",

View File

@@ -19,15 +19,27 @@ Die Stable-Aufgabe verwendet keinen Kanalwert oder den Wert `stable`.
## Passive Ransomware-Beta ## Passive Ransomware-Beta
Die Beta ist nach der Installation weiterhin deaktiviert. Auf einem Die Ransomware-Beta ist im Beta-Kanal standardmaessig aktiviert. Die Auswertung
Pilotgeraet wird in `C:\Program Files\OCSentinel\config\ocsentinel-settings.json` bleibt passiv, solange `ransomwareBetaAlertingEnabled` auf `false` steht:
der Wert `ransomwareBetaEnabled` auf `true` gesetzt. Die erste Auswertung Hinweise, Warnungen und kritische Beta-Signale erscheinen im JSON-Report und
liest nur die letzten 15 Minuten der vorhandenen Prozess- und PowerShell- Dashboard, veraendern aber keine NinjaOne-Alarmfelder. Fuer eine lokale
Ereignisse; sie installiert weder Sysmon noch Windows-Dateiauditing. Ausnahme kann `ransomwareBetaEnabled` in
`C:\Program Files\OCSentinel\config\ocsentinel-settings.json` auf `false`
gesetzt werden.
Ein Hinweis wird nur im JSON-Report und Dashboard sichtbar. Warnung und Jeder Beta-Report enthaelt zudem eine datensparsame Sensorabdeckung fuer
kritisch werden erst nach dem kontrollierten Alarmierungs-Pilot an NinjaOne Security-Ereignis 4688, PowerShell 4104, Sysmon 1 und das optionale
weitergegeben. Datei-Auditing 4663. Damit bedeutet ein Status `ok` nicht mehr stillschweigend,
dass alle Datenquellen vorhanden waren. Es werden nur Verfuegbarkeit,
technischer Zustand und gezaehlte Ereignisse uebertragen.
Der optionale Datei-Churn-Sensor wird nur mit
`ransomwareFileChurnEnabled: true` aktiviert. Er wertet ausschliesslich bereits
vorhandene Security-Ereignisse 4663 aus, setzt keine Audit-Richtlinie und
aendert keine SACLs. Es werden nur Zaehler sowie Prozessnamen gespeichert und
uebertragen, niemals Datei- oder Freigabenamen. Eine Auswertung ist auf 5.000
Audit-Ereignisse und ein 15-Minuten-Fenster begrenzt; ein gekappter Lauf erzeugt
kein Churn-Signal.
## Rueckfall ## Rueckfall

View File

@@ -76,7 +76,6 @@ write access:
| `ocsentinelqueuedreports` | Integer | Reports waiting for delivery | | `ocsentinelqueuedreports` | Integer | Reports waiting for delivery |
| `ocsentinellastuploadutc` | Date/Time | Last successful upload time | | `ocsentinellastuploadutc` | Date/Time | Last successful upload time |
| `ocsentinellasterror` | Text | Last upload error, if any | | `ocsentinellasterror` | Text | Last upload error, if any |
| `ocsentinelclientversion` | Text | Installed client version |
## NinjaOne Tasks ## NinjaOne Tasks
@@ -112,6 +111,29 @@ Runtime:
-OutputPath "..\reports\ocsentinel-summary.json" -OutputPath "..\reports\ocsentinel-summary.json"
``` ```
## Repair Missing NinjaOne Context
NinjaOne exposes `NINJA_ORGANIZATION_ID`, `NINJA_ORGANIZATION_NAME`,
`NINJA_AGENT_MACHINE_ID`, and location values only while an automation runs.
The scheduled OCSentinel task runs later as `SYSTEM`, so those values must be
persisted during an actual NinjaOne automation.
If the console shows `Organisation unbekannt`, create a temporary NinjaOne
PowerShell automation named `OCSentinel - NinjaOne Kontext aktualisieren` and
copy `scripts/refresh-ocsentinel-ninja-context.ps1` into the editor. Run it as
`SYSTEM` in 64-bit PowerShell once against the affected devices or policy.
The script has no script variables and does the following safely:
1. updates the installed client through the stable, version-independent manifest;
2. stores the current NinjaOne organization, location, node, and machine values;
3. starts one signed status scan and upload using that stored context.
Expected output includes `OCSENTINEL_NINJA_CONTEXT=updated`; this is emitted
only after the immediate upload succeeds. Do not run this script from an
interactive PowerShell session, because NinjaOne does not expose the required
environment values there.
## Secret Bootstrap ## Secret Bootstrap
```powershell ```powershell

View File

@@ -120,11 +120,14 @@ Ein Pilot wird pausiert und zurueckgesetzt, wenn eines dieser Kriterien eintritt
Systemereignisse sowie Aenderungszaehler seit dem letzten Pruefpunkt. Systemereignisse sowie Aenderungszaehler seit dem letzten Pruefpunkt.
- Erkennung hochrelevanter Manipulationen wie Schattenkopie-, Recovery- und - Erkennung hochrelevanter Manipulationen wie Schattenkopie-, Recovery- und
Backup-Loeschbefehle sowie verdaechtiger Verschluesselungswerkzeuge. Backup-Loeschbefehle sowie verdaechtiger Verschluesselungswerkzeuge.
- Lokaler Ringpuffer mit aggregierten Datei-Churn-Signalen, etwa ungewoehnliche - Lokaler Ringpuffer mit aggregierten Datei-Churn-Signalen aus vorhandenen
Umbenennungen, Loeschungen und neue Erweiterungen. Datei-Audit-Ereignissen. Die erste Beta wertet Loesch- und Schreibzugriffe
ohne Datei- oder Freigabenamen aus.
- Snapshot der SMB-Sitzungen und des Incident-Kontexts erst bei einer - Snapshot der SMB-Sitzungen und des Incident-Kontexts erst bei einer
Auffaelligkeit. Auffaelligkeit.
- Verdichtetes Ransomware-Incident-Protokoll fuer n8n und die Uebersicht. - Verdichtetes Ransomware-Incident-Protokoll fuer n8n und die Uebersicht.
- Strukturierte Sensorabdeckung im Report, damit fehlende Audit- oder Sysmon-
Quellen von einem echten unauffaelligen Zeitraum unterscheidbar sind.
**Bewertung** **Bewertung**

View File

@@ -5,7 +5,7 @@ import os
from datetime import datetime, timezone from datetime import datetime, timezone
import psycopg import psycopg
from flask import Flask, abort, redirect, render_template, request, url_for from flask import Flask, abort, jsonify, redirect, render_template, request, url_for
app = Flask(__name__) app = Flask(__name__)
@@ -245,6 +245,109 @@ def organization(organization_id):
) )
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() - (%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, organization_id)
entry = flows.setdefault(
key,
{
"source_ip": source_ip,
"machine_name": machine_name,
"account": account,
"target": target,
"organization_id": organization_id,
"organization_name": organization_name,
"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 flow_rows, max_count, source_count, target_count
@app.get("/network")
def network():
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():
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):
source_id = f"source:{flow['source_ip']}"
target_id = f"target:{flow['machine_name']}"
nodes[source_id] = {"data": {"id": source_id, "label": flow["source_ip"], "kind": "source"}}
nodes[target_id] = {"data": {"id": target_id, "label": flow["machine_name"], "kind": "target"}}
edges.append(
{
"data": {
"id": f"flow:{index}",
"source": source_id,
"target": target_id,
"count": flow["count"],
"account": flow["account"],
"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"],
}
}
)
return jsonify(
{
"elements": {"nodes": list(nodes.values()), "edges": edges},
"max_count": max_count,
"source_count": source_count,
"target_count": target_count,
}
)
@app.get("/device/<machine_name>") @app.get("/device/<machine_name>")
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:

View File

@@ -28,3 +28,6 @@ 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; } .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; } } .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); } .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; } }

View File

@@ -0,0 +1,18 @@
Cytoscape.js 3.34.0
MIT License
Copyright (c) 2016-2026, The Cytoscape Consortium
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.

File diff suppressed because one or more lines are too long

View File

@@ -12,7 +12,7 @@
<body class="app-shell"> <body class="app-shell">
<header class="masthead"> <header class="masthead">
<a class="wordmark" href="{{ url_for('overview') }}"><span>OC</span>Sentinel</a> <a class="wordmark" href="{{ url_for('overview') }}"><span>OC</span>Sentinel</a>
<nav class="header-links"><a class="{{ 'active' if request.endpoint in ('overview', 'organization') else '' }}" href="{{ url_for('overview') }}">Lagebild</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> <nav class="header-links"><a class="{{ 'active' if request.endpoint in ('overview', 'organization') else '' }}" href="{{ url_for('overview') }}">Lagebild</a><a class="{{ 'active' if request.endpoint == 'network' else '' }}" href="{{ url_for('network') }}">Zugriffswege</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>
</header> </header>
<main>{% block content %}{% endblock %}</main> <main>{% block content %}{% endblock %}</main>
</body> </body>

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

@@ -0,0 +1,52 @@
{% extends "base.html" %}
{% block title %}Zugriffswege - OfficeCom Sentinel{% endblock %}
{% block content %}
<section class="map-hero">
<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>
<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>
(() => {
const inspector = document.getElementById('network-inspector');
const filter = document.getElementById('map-filter');
const organization = document.getElementById('map-organization');
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: [
{ 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': '#4fa3df', 'shape': 'ellipse' } },
{ selector: 'node[kind = "target"]', style: { 'background-color': '#3bca99', 'shape': 'round-rectangle' } },
{ 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': '#ffb454', 'target-arrow-color': '#ffb454' } },
{ selector: '.selected', style: { 'border-color': '#e7ff88', 'border-width': 6, 'line-color': '#e7ff88', 'target-arrow-color': '#e7ff88', 'opacity': 1, 'z-index': 20 } },
{ selector: '.hidden', style: { 'display': 'none' } }
], layout: { name: 'cose', animate: false, padding: 52, nodeRepulsion: 9000, idealEdgeLength: 145, gravity: .2 } });
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>';
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>'; } };
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(); };
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); });
cy.on('tap', event => { if (event.target === cy) { cy.elements().removeClass('selected'); resetInspector(); } });
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.'; });
})();
</script>
{% endblock %}

View File

@@ -149,7 +149,6 @@ function Publish-NinjaCustomFields {
[pscustomobject]@{ Name = "ocsentinelqueuedreports"; Type = "Integer"; Value = $queuedReports } [pscustomobject]@{ Name = "ocsentinelqueuedreports"; Type = "Integer"; Value = $queuedReports }
[pscustomobject]@{ Name = "ocsentinellastuploadutc"; Type = "DateTime"; Value = $lastUploadUtc } [pscustomobject]@{ Name = "ocsentinellastuploadutc"; Type = "DateTime"; Value = $lastUploadUtc }
[pscustomobject]@{ Name = "ocsentinellasterror"; Type = "Text"; Value = $lastUploadError } [pscustomobject]@{ Name = "ocsentinellasterror"; Type = "Text"; Value = $lastUploadError }
[pscustomobject]@{ Name = "ocsentinelclientversion"; Type = "Text"; Value = [string]$Report.ClientVersion }
) )
$updated = 0 $updated = 0

View File

@@ -1,8 +1,8 @@
{ {
"channel": "beta", "channel": "beta",
"version": "1.5.0-beta.1", "version": "1.5.0-beta.4",
"publishedAtUtc": "2026-07-29T23:04:51.9334773Z", "publishedAtUtc": "2026-07-31T23:38:20.1674646Z",
"artifactUrl": "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v1.5.0-beta.1/OCSentinelClient-win-x64.zip", "artifactUrl": "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v1.5.0-beta.4/OCSentinelClient-win-x64.zip",
"sha256": "2bd6db7261f7ca9b47741968b922aea5969251539530e58441e882f53686ae6b", "sha256": "f8e8714a4ea1426674bca27a403c33e166a31b38f77f0c638cb4a6a51b8f3343",
"minUpdaterVersion": "1.0.0" "minUpdaterVersion": "1.0.0"
} }

View File

@@ -2,7 +2,7 @@
"channel": "stable", "channel": "stable",
"version": "1.4.0", "version": "1.4.0",
"publishedAtUtc": "2026-07-28T22:50:17.7779552Z", "publishedAtUtc": "2026-07-28T22:50:17.7779552Z",
"artifactUrl": "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v1.4.0/OCSentinelClient-win-x64.zip", "artifactUrl": "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v1.4.0/OCSentinelClient-win-x64.zip?asset-revision=ece66505c4146fec",
"sha256": "ece66505c4146fec72ba12cb59d3d0a39bb91a3aee44e338d19c186e56e2fad7", "sha256": "ece66505c4146fec72ba12cb59d3d0a39bb91a3aee44e338d19c186e56e2fad7",
"minUpdaterVersion": "1.0.0" "minUpdaterVersion": "1.0.0"
} }

View File

@@ -112,6 +112,30 @@ function Get-OCSentinelArtifact {
} }
} }
function Enable-OCSentinelBetaDefaults {
param([Parameter(Mandatory)][string]$SettingsPath)
if (-not (Test-Path -LiteralPath $SettingsPath)) {
return
}
$settings = Get-Content -LiteralPath $SettingsPath -Raw | ConvertFrom-Json
if ($null -ne $settings.PSObject.Properties["ransomwareBetaDefaultApplied"]) {
return
}
if ($null -eq $settings.PSObject.Properties["ransomwareBetaEnabled"]) {
$settings | Add-Member -NotePropertyName "ransomwareBetaEnabled" -NotePropertyValue $true
}
else {
$settings.ransomwareBetaEnabled = $true
}
$settings | Add-Member -NotePropertyName "ransomwareBetaDefaultApplied" -NotePropertyValue "1.5.0-beta.4"
$settings | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $SettingsPath -Encoding UTF8
Write-Host "Enabled passive ransomware beta defaults."
}
Initialize-OCSentinelTls Initialize-OCSentinelTls
if ($ReleaseChannel -eq "stable" -and -not [string]::IsNullOrWhiteSpace($env:ReleaseChannel)) { if ($ReleaseChannel -eq "stable" -and -not [string]::IsNullOrWhiteSpace($env:ReleaseChannel)) {
@@ -133,6 +157,7 @@ $updaterPath = Join-Path $installRoot "scripts\update-ocsentinel.ps1"
$monitorPath = Join-Path $installRoot "scripts\run-ocsentinel-monitor.ps1" $monitorPath = Join-Path $installRoot "scripts\run-ocsentinel-monitor.ps1"
$appPath = Join-Path $installRoot "app\OCSentinelCli.exe" $appPath = Join-Path $installRoot "app\OCSentinelCli.exe"
$clientConfigPath = Join-Path $installRoot "config\ocsentinel-client.json" $clientConfigPath = Join-Path $installRoot "config\ocsentinel-client.json"
$settingsPath = Join-Path $installRoot "config\ocsentinel-settings.json"
$secretScriptPath = Join-Path $installRoot "scripts\protect-ocsentinel-secret.ps1" $secretScriptPath = Join-Path $installRoot "scripts\protect-ocsentinel-secret.ps1"
$secretPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat" $secretPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"
@@ -214,6 +239,10 @@ if (-not (Test-Path -LiteralPath $appPath)) {
throw "OCSentinel installation completed, but the client executable was not found." throw "OCSentinel installation completed, but the client executable was not found."
} }
if ($ReleaseChannel -eq "beta") {
Enable-OCSentinelBetaDefaults -SettingsPath $settingsPath
}
if (-not [string]::IsNullOrWhiteSpace($WebhookUrl)) { if (-not [string]::IsNullOrWhiteSpace($WebhookUrl)) {
if (-not (Test-Path -LiteralPath $clientConfigPath)) { if (-not (Test-Path -LiteralPath $clientConfigPath)) {
throw "OCSentinel client configuration was not found: $clientConfigPath" throw "OCSentinel client configuration was not found: $clientConfigPath"

View File

@@ -44,6 +44,20 @@ foreach ($path in @($configPath, $secretScript, $monitorScript)) {
$config = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json $config = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
$config.n8nWebhookUrl = $WebhookUrl $config.n8nWebhookUrl = $WebhookUrl
$config.environment = "production" $config.environment = "production"
$ninjaContext = @(
@{ EnvironmentName = "NINJA_ORGANIZATION_ID"; PropertyName = "ninjaOrganizationId" },
@{ EnvironmentName = "NINJA_ORGANIZATION_NAME"; PropertyName = "ninjaOrganizationName" },
@{ EnvironmentName = "NINJA_AGENT_MACHINE_ID"; PropertyName = "ninjaMachineId" },
@{ EnvironmentName = "NINJA_AGENT_NODE_ID"; PropertyName = "ninjaNodeId" },
@{ EnvironmentName = "NINJA_LOCATION_ID"; PropertyName = "ninjaLocationId" },
@{ EnvironmentName = "NINJA_LOCATION_NAME"; PropertyName = "ninjaLocationName" }
)
foreach ($entry in $ninjaContext) {
$value = [Environment]::GetEnvironmentVariable($entry.EnvironmentName, "Process")
if (-not [string]::IsNullOrWhiteSpace($value)) {
$config | Add-Member -NotePropertyName $entry.PropertyName -NotePropertyValue $value.Trim() -Force
}
}
$config | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $configPath -Encoding UTF8 $config | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $configPath -Encoding UTF8
Write-Host "OCSentinel upload endpoint configured." Write-Host "OCSentinel upload endpoint configured."

View File

@@ -96,6 +96,20 @@ $secretPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"
$config = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json $config = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
$config.n8nWebhookUrl = $WebhookUrl $config.n8nWebhookUrl = $WebhookUrl
$config.environment = "production" $config.environment = "production"
$ninjaContext = @(
@{ EnvironmentName = "NINJA_ORGANIZATION_ID"; PropertyName = "ninjaOrganizationId" },
@{ EnvironmentName = "NINJA_ORGANIZATION_NAME"; PropertyName = "ninjaOrganizationName" },
@{ EnvironmentName = "NINJA_AGENT_MACHINE_ID"; PropertyName = "ninjaMachineId" },
@{ EnvironmentName = "NINJA_AGENT_NODE_ID"; PropertyName = "ninjaNodeId" },
@{ EnvironmentName = "NINJA_LOCATION_ID"; PropertyName = "ninjaLocationId" },
@{ EnvironmentName = "NINJA_LOCATION_NAME"; PropertyName = "ninjaLocationName" }
)
foreach ($entry in $ninjaContext) {
$value = [Environment]::GetEnvironmentVariable($entry.EnvironmentName, "Process")
if (-not [string]::IsNullOrWhiteSpace($value)) {
$config | Add-Member -NotePropertyName $entry.PropertyName -NotePropertyValue $value.Trim() -Force
}
}
$config | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $configPath -Encoding UTF8 $config | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $configPath -Encoding UTF8
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $secretScript -SecretValue $SecretValue & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $secretScript -SecretValue $SecretValue

View File

@@ -0,0 +1,110 @@
[CmdletBinding()]
param(
[string]$ManifestUrl = "https://gitea.officecom.cloud/officecom/oc-sentinel/raw/main/release/stable/version.json"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Initialize-OCSentinelTls {
$protocols = [Net.SecurityProtocolType]::Tls12
if ([Enum]::GetNames([Net.SecurityProtocolType]) -contains "Tls13") {
$protocols = $protocols -bor [Net.SecurityProtocolType]::Tls13
}
[Net.ServicePointManager]::SecurityProtocol = $protocols
[Net.ServicePointManager]::Expect100Continue = $false
}
function Read-NinjaEnvironmentValue {
param([Parameter(Mandatory)][string]$Name)
$value = [Environment]::GetEnvironmentVariable($Name, "Process")
if ($null -eq $value) {
return ""
}
return $value.Trim()
}
Initialize-OCSentinelTls
$installRoot = Join-Path $env:ProgramFiles "OCSentinel"
$updaterPath = Join-Path $installRoot "scripts\update-ocsentinel.ps1"
$monitorPath = Join-Path $installRoot "scripts\run-ocsentinel-monitor.ps1"
$clientConfigPath = Join-Path $installRoot "config\ocsentinel-client.json"
$secretPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"
foreach ($path in @($updaterPath, $clientConfigPath, $monitorPath)) {
if (-not (Test-Path -LiteralPath $path)) {
throw "OCSentinel installation is incomplete. Missing: $path"
}
}
# The NinjaOne context exists only during this script execution. Upgrade first so
# future scheduled scans restore the context from the local client configuration.
$escapedUpdaterPath = $updaterPath.Replace("'", "''")
$escapedManifestUrl = $ManifestUrl.Replace("'", "''")
$updateCommand = @"
`$protocols = [Net.SecurityProtocolType]::Tls12
if ([Enum]::GetNames([Net.SecurityProtocolType]) -contains 'Tls13') { `$protocols = `$protocols -bor [Net.SecurityProtocolType]::Tls13 }
[Net.ServicePointManager]::SecurityProtocol = `$protocols
[Net.ServicePointManager]::Expect100Continue = `$false
& '$escapedUpdaterPath' -ManifestUrl '$escapedManifestUrl'
exit `$LASTEXITCODE
"@
& powershell.exe -NoProfile -ExecutionPolicy Bypass -Command $updateCommand | ForEach-Object { Write-Host $_ }
if ($LASTEXITCODE -ne 0) {
throw "OCSentinel updater exited with code $LASTEXITCODE"
}
$mappings = @(
@{ EnvironmentName = "NINJA_ORGANIZATION_ID"; PropertyName = "ninjaOrganizationId"; Required = $true },
@{ EnvironmentName = "NINJA_ORGANIZATION_NAME"; PropertyName = "ninjaOrganizationName"; Required = $true },
@{ EnvironmentName = "NINJA_AGENT_MACHINE_ID"; PropertyName = "ninjaMachineId"; Required = $true },
@{ EnvironmentName = "NINJA_AGENT_NODE_ID"; PropertyName = "ninjaNodeId"; Required = $false },
@{ EnvironmentName = "NINJA_LOCATION_ID"; PropertyName = "ninjaLocationId"; Required = $false },
@{ EnvironmentName = "NINJA_LOCATION_NAME"; PropertyName = "ninjaLocationName"; Required = $false }
)
$clientConfig = Get-Content -LiteralPath $clientConfigPath -Raw | ConvertFrom-Json
$missing = @()
$captured = 0
foreach ($mapping in $mappings) {
$value = Read-NinjaEnvironmentValue -Name $mapping.EnvironmentName
if ([string]::IsNullOrWhiteSpace($value)) {
if ($mapping.Required) { $missing += $mapping.EnvironmentName }
continue
}
$clientConfig | Add-Member -NotePropertyName $mapping.PropertyName -NotePropertyValue $value -Force
$captured++
}
if ($missing.Count -gt 0) {
throw "NinjaOne did not provide required context: $($missing -join ', '). Run this only from a NinjaOne automation, not from an interactive PowerShell session."
}
$clientConfig | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $clientConfigPath -Encoding UTF8
Write-Host "OCSentinel NinjaOne context captured: $captured of $($mappings.Count) values."
if ([string]::IsNullOrWhiteSpace([string]$clientConfig.n8nWebhookUrl)) {
throw "NinjaOne context was stored, but this client has no configured n8n webhook URL. Run the OCSentinel installation/configuration automation with its WebhookUrl variable first."
}
if (-not (Test-Path -LiteralPath $secretPath)) {
throw "NinjaOne context was stored, but the protected upload secret is missing. Run the OCSentinel installation/configuration automation with its SecretValue variable first."
}
Write-Host "Running an immediate status scan and upload with the refreshed NinjaOne context."
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $monitorPath `
-Mode status `
-ClientConfigPath $clientConfigPath `
-SecretPath $secretPath `
-UploadMode required `
-SuppressTriggerExit
if ($LASTEXITCODE -ne 0) {
throw "OCSentinel context refresh scan exited with code $LASTEXITCODE"
}
Write-Host "OCSENTINEL_NINJA_CONTEXT=updated"

View File

@@ -149,7 +149,6 @@ function Publish-NinjaCustomFields {
[pscustomobject]@{ Name = "ocsentinelqueuedreports"; Type = "Integer"; Value = $queuedReports } [pscustomobject]@{ Name = "ocsentinelqueuedreports"; Type = "Integer"; Value = $queuedReports }
[pscustomobject]@{ Name = "ocsentinellastuploadutc"; Type = "DateTime"; Value = $lastUploadUtc } [pscustomobject]@{ Name = "ocsentinellastuploadutc"; Type = "DateTime"; Value = $lastUploadUtc }
[pscustomobject]@{ Name = "ocsentinellasterror"; Type = "Text"; Value = $lastUploadError } [pscustomobject]@{ Name = "ocsentinellasterror"; Type = "Text"; Value = $lastUploadError }
[pscustomobject]@{ Name = "ocsentinelclientversion"; Type = "Text"; Value = [string]$Report.ClientVersion }
) )
$updated = 0 $updated = 0

View File

@@ -3,6 +3,7 @@ using System.Globalization;
using System.Net; using System.Net;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using OCSentinelCli.Configuration;
namespace OCSentinelCli; namespace OCSentinelCli;
@@ -54,7 +55,7 @@ internal sealed class AttackScanner
.ToList(); .ToList();
int uniqueIpCount = attacks.Select(static attack => attack.SourceIp).Distinct(StringComparer.OrdinalIgnoreCase).Count(); int uniqueIpCount = attacks.Select(static attack => attack.SourceIp).Distinct(StringComparer.OrdinalIgnoreCase).Count();
AlertAssessment baseAssessment = MergeRansomwareAssessment(AssessAttackActivity(attacks, uniqueIpCount, configuration), ransomwareBeta); AlertAssessment baseAssessment = MergeRansomwareAssessment(AssessAttackActivity(attacks, uniqueIpCount, configuration), ransomwareBeta, configuration.RansomwareBetaAlertingEnabled);
string baseAlertState = baseAssessment.State; string baseAlertState = baseAssessment.State;
string baseAlertReason = baseAssessment.Reason; string baseAlertReason = baseAssessment.Reason;
VulnerabilityCorrelationSummary vulnerabilityCorrelation = string.IsNullOrWhiteSpace(options.VulnerabilityCsvPath) VulnerabilityCorrelationSummary vulnerabilityCorrelation = string.IsNullOrWhiteSpace(options.VulnerabilityCsvPath)
@@ -68,7 +69,7 @@ internal sealed class AttackScanner
{ {
SchemaVersion = "2.0", SchemaVersion = "2.0",
MachineName = Environment.MachineName, MachineName = Environment.MachineName,
NinjaOne = GetNinjaOneContext(), NinjaOne = GetNinjaOneContext(options, errors),
GeneratedAtLocal = generatedAtLocal, GeneratedAtLocal = generatedAtLocal,
GeneratedAtUtc = generatedAtUtc, GeneratedAtUtc = generatedAtUtc,
ClientVersion = BuildMetadata.Version, ClientVersion = BuildMetadata.Version,
@@ -93,19 +94,38 @@ internal sealed class AttackScanner
}; };
} }
private static NinjaOneContext GetNinjaOneContext() private static NinjaOneContext GetNinjaOneContext(ScanOptions options, List<string> errors)
{ {
ClientConfiguration? clientConfiguration = null;
if (!string.IsNullOrWhiteSpace(options.ClientConfigPath))
{
try
{
clientConfiguration = ClientConfiguration.Load(options.ClientConfigPath);
}
catch (Exception exception)
{
errors.Add($"Could not load persisted NinjaOne context: {exception.Message}");
}
}
return new NinjaOneContext return new NinjaOneContext
{ {
OrganizationId = ReadEnvironmentVariable("NINJA_ORGANIZATION_ID"), OrganizationId = ReadContextValue("NINJA_ORGANIZATION_ID", clientConfiguration?.NinjaOrganizationId),
OrganizationName = ReadEnvironmentVariable("NINJA_ORGANIZATION_NAME"), OrganizationName = ReadContextValue("NINJA_ORGANIZATION_NAME", clientConfiguration?.NinjaOrganizationName),
MachineId = ReadEnvironmentVariable("NINJA_AGENT_MACHINE_ID"), MachineId = ReadContextValue("NINJA_AGENT_MACHINE_ID", clientConfiguration?.NinjaMachineId),
NodeId = ReadEnvironmentVariable("NINJA_AGENT_NODE_ID"), NodeId = ReadContextValue("NINJA_AGENT_NODE_ID", clientConfiguration?.NinjaNodeId),
LocationId = ReadEnvironmentVariable("NINJA_LOCATION_ID"), LocationId = ReadContextValue("NINJA_LOCATION_ID", clientConfiguration?.NinjaLocationId),
LocationName = ReadEnvironmentVariable("NINJA_LOCATION_NAME") LocationName = ReadContextValue("NINJA_LOCATION_NAME", clientConfiguration?.NinjaLocationName)
}; };
} }
private static string ReadContextValue(string environmentName, string? persistedValue)
{
string currentValue = ReadEnvironmentVariable(environmentName);
return string.IsNullOrWhiteSpace(currentValue) ? persistedValue?.Trim() ?? string.Empty : currentValue;
}
private static string ReadEnvironmentVariable(string name) private static string ReadEnvironmentVariable(string name)
{ {
return Environment.GetEnvironmentVariable(name)?.Trim() ?? string.Empty; return Environment.GetEnvironmentVariable(name)?.Trim() ?? string.Empty;
@@ -519,9 +539,9 @@ internal sealed class AttackScanner
return new AlertAssessment("ok", $"Low-volume login errors observed: events={attacks.Count}, unique IPs={uniqueIpCount}; no burst or password-spraying pattern detected."); return new AlertAssessment("ok", $"Low-volume login errors observed: events={attacks.Count}, unique IPs={uniqueIpCount}; no burst or password-spraying pattern detected.");
} }
private static AlertAssessment MergeRansomwareAssessment(AlertAssessment loginAssessment, RansomwareBetaSummary ransomwareBeta) private static AlertAssessment MergeRansomwareAssessment(AlertAssessment loginAssessment, RansomwareBetaSummary ransomwareBeta, bool ransomwareAlertingEnabled)
{ {
if (ransomwareBeta.State is not ("warning" or "critical")) if (!RansomwareAlertPolicy.CanElevate(ransomwareBeta, ransomwareAlertingEnabled))
{ {
return loginAssessment; return loginAssessment;
} }

View File

@@ -23,7 +23,10 @@ internal static class ScanAndUploadCommand
if (string.Equals(args[i], "--client-config", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) if (string.Equals(args[i], "--client-config", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
{ {
clientConfigPath = args[++i]; string configPathValue = args[++i];
clientConfigPath = configPathValue;
scanArgs.Add("--client-config");
scanArgs.Add(configPathValue);
continue; continue;
} }

View File

@@ -26,7 +26,9 @@ internal sealed record ScannerConfiguration
public int CorrelationCriticalCveThreshold { get; init; } = 1; public int CorrelationCriticalCveThreshold { get; init; } = 1;
public bool RansomwareBetaEnabled { get; init; } public bool RansomwareBetaEnabled { get; init; } = true;
public bool RansomwareBetaAlertingEnabled { get; init; }
public int RansomwareLookbackMinutes { get; init; } = 15; public int RansomwareLookbackMinutes { get; init; } = 15;
@@ -34,6 +36,22 @@ internal sealed record ScannerConfiguration
public int RansomwareCriticalSignalCount { get; init; } = 3; public int RansomwareCriticalSignalCount { get; init; } = 3;
public bool RansomwareCaptureSmbSessions { get; init; } = true;
public bool RansomwareFileChurnEnabled { get; init; }
public int RansomwareFileChurnWindowMinutes { get; init; } = 15;
public int RansomwareFileChurnWarningDeleteCount { get; init; } = 50;
public int RansomwareFileChurnWarningWriteCount { get; init; } = 250;
public int RansomwareFileChurnCriticalDeleteCount { get; init; } = 200;
public int RansomwareFileChurnCriticalWriteCount { get; init; } = 1000;
public int RansomwareFileChurnMaxAuditEvents { get; init; } = 5000;
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

@@ -14,6 +14,18 @@ internal sealed record ClientConfiguration
public string N8nWebhookUrl { get; init; } = string.Empty; public string N8nWebhookUrl { get; init; } = string.Empty;
public string NinjaOrganizationId { get; init; } = string.Empty;
public string NinjaOrganizationName { get; init; } = string.Empty;
public string NinjaMachineId { get; init; } = string.Empty;
public string NinjaNodeId { get; init; } = string.Empty;
public string NinjaLocationId { get; init; } = string.Empty;
public string NinjaLocationName { get; init; } = string.Empty;
public string DeviceIdentifierMode { get; init; } = "machineName"; public string DeviceIdentifierMode { get; init; } = "machineName";
public int UploadTimeoutSeconds { get; init; } = 30; public int UploadTimeoutSeconds { get; init; } = 30;

View File

@@ -119,6 +119,8 @@ internal sealed record RansomwareBetaSummary
{ {
public bool Enabled { get; init; } public bool Enabled { get; init; }
public bool AlertingEnabled { get; init; }
public string State { get; init; } = "disabled"; public string State { get; init; } = "disabled";
public string Reason { get; init; } = "Ransomware beta is disabled."; public string Reason { get; init; } = "Ransomware beta is disabled.";
@@ -126,6 +128,66 @@ 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<RansomwareSensorStatus> Sensors { get; init; } = [];
public List<RansomwareSmbSession> SmbSessions { get; init; } = [];
public RansomwareFileChurnSummary FileChurn { get; init; } = new();
}
internal sealed record RansomwareSensorStatus
{
public string Name { get; init; } = string.Empty;
public bool Enabled { get; init; }
public bool Available { get; init; }
public string State { get; init; } = "unknown";
public int EventCount { get; init; }
}
internal sealed record RansomwareFileChurnSummary
{
public bool Enabled { get; init; }
public bool DataAvailable { get; init; }
public bool IsTruncated { get; init; }
public int WindowMinutes { get; init; }
public int FileOperationCount { get; init; }
public int DeleteOperationCount { get; init; }
public int WriteOperationCount { get; init; }
public int DistinctProcessCount { get; init; }
public List<RansomwareFileChurnProcess> TopProcesses { get; init; } = [];
}
internal sealed record RansomwareFileChurnProcess
{
public string Process { get; init; } = string.Empty;
public int DeleteOperationCount { get; init; }
public int WriteOperationCount { 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

@@ -9,10 +9,10 @@
<RootNamespace>OCSentinelCli</RootNamespace> <RootNamespace>OCSentinelCli</RootNamespace>
<Product>OfficeCom Sentinel</Product> <Product>OfficeCom Sentinel</Product>
<Company>OfficeCom</Company> <Company>OfficeCom</Company>
<Version>1.5.0-beta.1</Version> <Version>1.5.0-beta.5</Version>
<AssemblyVersion>1.5.0.0</AssemblyVersion> <AssemblyVersion>1.5.0.0</AssemblyVersion>
<FileVersion>1.5.0.0</FileVersion> <FileVersion>1.5.0.0</FileVersion>
<InformationalVersion>1.5.0-beta.1</InformationalVersion> <InformationalVersion>1.5.0-beta.5</InformationalVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("OCSentinelCli.Tests")]

View File

@@ -0,0 +1,9 @@
namespace OCSentinelCli;
internal static class RansomwareAlertPolicy
{
public static bool CanElevate(RansomwareBetaSummary summary, bool alertingEnabled)
{
return alertingEnabled && summary.Enabled && (summary.State is "warning" or "critical");
}
}

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;
@@ -16,10 +18,27 @@ internal static class RansomwareBetaDetector
int lookbackMinutes = Math.Clamp(configuration.RansomwareLookbackMinutes, 1, 60); int lookbackMinutes = Math.Clamp(configuration.RansomwareLookbackMinutes, 1, 60);
DateTimeOffset since = DateTimeOffset.UtcNow.AddMinutes(-lookbackMinutes); DateTimeOffset since = DateTimeOffset.UtcNow.AddMinutes(-lookbackMinutes);
var signals = new List<RansomwareSignal>(); var signals = new List<RansomwareSignal>();
var sensors = new List<RansomwareSensorStatus>
{
ScanSecurityProcesses(signals, errors, since, configuration),
ScanPowerShellScriptBlocks(signals, errors, since, configuration),
ScanSysmonProcesses(signals, errors, since, configuration)
};
ScanSecurityProcesses(signals, errors, since, configuration); RansomwareFileChurnSummary fileChurn = RansomwareFileChurnDetector.Scan(configuration, errors);
ScanPowerShellScriptBlocks(signals, errors, since, configuration); sensors.Add(new RansomwareSensorStatus
ScanSysmonProcesses(signals, errors, since, configuration); {
Name = "security-file-audit-4663",
Enabled = fileChurn.Enabled,
Available = fileChurn.DataAvailable,
State = !fileChurn.Enabled ? "disabled" : fileChurn.IsTruncated ? "truncated" : fileChurn.DataAvailable ? "available" : "unavailable",
EventCount = fileChurn.FileOperationCount
});
RansomwareSignal? fileChurnSignal = RansomwareFileChurnDetector.CreateSignal(fileChurn, configuration);
if (fileChurnSignal is not null)
{
signals.Add(fileChurnSignal);
}
List<RansomwareSignal> distinctSignals = signals List<RansomwareSignal> distinctSignals = signals
.OrderBy(signal => signal.Timestamp) .OrderBy(signal => signal.Timestamp)
@@ -42,29 +61,36 @@ 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,
AlertingEnabled = configuration.RansomwareBetaAlertingEnabled,
State = state, State = state,
Reason = reason, Reason = reason,
LookbackMinutes = lookbackMinutes, LookbackMinutes = lookbackMinutes,
Signals = distinctSignals Signals = distinctSignals,
Sensors = sensors,
SmbSessions = smbSessions,
FileChurn = fileChurn
}; };
} }
private static void ScanSecurityProcesses(List<RansomwareSignal> signals, List<string> errors, DateTimeOffset since, ScannerConfiguration configuration) private static RansomwareSensorStatus ScanSecurityProcesses(List<RansomwareSignal> signals, List<string> errors, DateTimeOffset since, ScannerConfiguration configuration)
{ {
TryScan("Security", 4688, since, errors, record => AddSignal(signals, record, ReadProperty(record, 5), ReadProperty(record, 8), "Security", configuration)); return TryScan("security-process-4688", "Security", 4688, since, errors, record => AddSignal(signals, record, ReadProperty(record, 5), ReadProperty(record, 8), "Security", configuration));
} }
private static void ScanPowerShellScriptBlocks(List<RansomwareSignal> signals, List<string> errors, DateTimeOffset since, ScannerConfiguration configuration) private static RansomwareSensorStatus ScanPowerShellScriptBlocks(List<RansomwareSignal> signals, List<string> errors, DateTimeOffset since, ScannerConfiguration configuration)
{ {
TryScan("Microsoft-Windows-PowerShell/Operational", 4104, since, errors, record => AddSignal(signals, record, "powershell", FormatDescription(record), "PowerShell", configuration)); return TryScan("powershell-script-block-4104", "Microsoft-Windows-PowerShell/Operational", 4104, since, errors, record => AddSignal(signals, record, "powershell", FormatDescription(record), "PowerShell", configuration));
} }
private static void ScanSysmonProcesses(List<RansomwareSignal> signals, List<string> errors, DateTimeOffset since, ScannerConfiguration configuration) private static RansomwareSensorStatus ScanSysmonProcesses(List<RansomwareSignal> signals, List<string> errors, DateTimeOffset since, ScannerConfiguration configuration)
{ {
TryScan("Microsoft-Windows-Sysmon/Operational", 1, since, errors, record => AddSignal(signals, record, "sysmon-process", FormatDescription(record), "Sysmon", configuration)); return TryScan("sysmon-process-1", "Microsoft-Windows-Sysmon/Operational", 1, since, errors, record => AddSignal(signals, record, "sysmon-process", FormatDescription(record), "Sysmon", configuration));
} }
private static void AddSignal(List<RansomwareSignal> signals, EventRecord record, string process, string commandLine, string source, ScannerConfiguration configuration) private static void AddSignal(List<RansomwareSignal> signals, EventRecord record, string process, string commandLine, string source, ScannerConfiguration configuration)
@@ -132,8 +158,9 @@ internal static class RansomwareBetaDetector
private static bool ContainsAll(string value, params string[] needles) => needles.All(needle => value.Contains(needle, StringComparison.Ordinal)); private static bool ContainsAll(string value, params string[] needles) => needles.All(needle => value.Contains(needle, StringComparison.Ordinal));
private static void TryScan(string logName, int eventId, DateTimeOffset since, List<string> errors, Action<EventRecord> processRecord) private static RansomwareSensorStatus TryScan(string sensorName, string logName, int eventId, DateTimeOffset since, List<string> errors, Action<EventRecord> processRecord)
{ {
int eventCount = 0;
try try
{ {
long milliseconds = Math.Max(1, (long)(DateTimeOffset.UtcNow - since).TotalMilliseconds); long milliseconds = Math.Max(1, (long)(DateTimeOffset.UtcNow - since).TotalMilliseconds);
@@ -143,16 +170,39 @@ internal static class RansomwareBetaDetector
{ {
using (record) using (record)
{ {
eventCount++;
processRecord(record); processRecord(record);
} }
} }
return new RansomwareSensorStatus
{
Name = sensorName,
Enabled = true,
Available = true,
State = "available",
EventCount = eventCount
};
} }
catch (EventLogNotFoundException) catch (EventLogNotFoundException)
{ {
return new RansomwareSensorStatus
{
Name = sensorName,
Enabled = true,
State = "not-installed"
};
} }
catch (Exception exception) catch (Exception exception)
{ {
errors.Add($"Ransomware beta query failed for {logName}: {exception.Message}"); errors.Add($"Ransomware beta query failed for {logName}: {exception.Message}");
return new RansomwareSensorStatus
{
Name = sensorName,
Enabled = true,
State = "query-failed",
EventCount = eventCount
};
} }
} }
@@ -169,4 +219,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;
} }

View File

@@ -0,0 +1,296 @@
using System.Diagnostics.Eventing.Reader;
using System.Globalization;
using System.Runtime.Versioning;
using System.Text.Json;
using System.Xml.Linq;
namespace OCSentinelCli;
[SupportedOSPlatform("windows")]
internal static class RansomwareFileChurnDetector
{
private const uint DeleteAccessMask = 0x00010000;
private const uint FileWriteAccessMask = 0x00000156;
private const string StateFileName = "ransomware-file-churn.json";
public static RansomwareFileChurnSummary Scan(ScannerConfiguration configuration, List<string> errors)
{
if (!configuration.RansomwareFileChurnEnabled)
{
return new RansomwareFileChurnSummary();
}
int windowMinutes = Math.Clamp(configuration.RansomwareFileChurnWindowMinutes, 1, 60);
int maximumAuditEvents = Math.Clamp(configuration.RansomwareFileChurnMaxAuditEvents, 100, 20000);
DateTimeOffset windowStart = DateTimeOffset.UtcNow.AddMinutes(-windowMinutes);
var observed = new List<FileAuditActivity>();
bool isTruncated = false;
try
{
long milliseconds = Math.Max(1, (long)(DateTimeOffset.UtcNow - windowStart).TotalMilliseconds);
string query = $"*[System[(EventID=4663) and TimeCreated[timediff(@SystemTime) <= {milliseconds}]]]";
using var reader = new EventLogReader(new EventLogQuery("Security", PathType.LogName, query));
int inspected = 0;
for (EventRecord? record = reader.ReadEvent(); record is not null; record = reader.ReadEvent())
{
using (record)
{
if (++inspected > maximumAuditEvents)
{
isTruncated = true;
break;
}
if (TryCreateActivity(record, configuration, out FileAuditActivity? activity) && activity is not null)
{
observed.Add(activity);
}
}
}
}
catch (UnauthorizedAccessException exception)
{
errors.Add($"Ransomware file churn cannot read the Security log: {exception.Message}");
return Unavailable(windowMinutes);
}
catch (EventLogNotFoundException)
{
return Unavailable(windowMinutes);
}
catch (Exception exception)
{
errors.Add($"Ransomware file churn query failed: {exception.Message}");
return Unavailable(windowMinutes);
}
if (isTruncated)
{
return BuildSummary(observed, windowMinutes, isTruncated: true);
}
List<FileAuditActivity> rollingActivities = MergeWithState(observed, windowStart, errors);
return BuildSummary(rollingActivities, windowMinutes, isTruncated: false);
}
public static RansomwareSignal? CreateSignal(RansomwareFileChurnSummary summary, ScannerConfiguration configuration)
{
if (!summary.Enabled || !summary.DataAvailable || summary.IsTruncated)
{
return null;
}
int warningDeletes = Math.Max(1, configuration.RansomwareFileChurnWarningDeleteCount);
int warningWrites = Math.Max(1, configuration.RansomwareFileChurnWarningWriteCount);
int criticalDeletes = Math.Max(warningDeletes, configuration.RansomwareFileChurnCriticalDeleteCount);
int criticalWrites = Math.Max(warningWrites, configuration.RansomwareFileChurnCriticalWriteCount);
bool critical = summary.DeleteOperationCount >= criticalDeletes && summary.WriteOperationCount >= criticalWrites;
bool warning = summary.DeleteOperationCount >= warningDeletes && summary.WriteOperationCount >= warningWrites;
if (!warning)
{
return null;
}
RansomwareFileChurnProcess? topProcess = summary.TopProcesses.FirstOrDefault();
string evidence = $"delete={summary.DeleteOperationCount}; write={summary.WriteOperationCount}; processes={summary.DistinctProcessCount}; window={summary.WindowMinutes}m";
return new RansomwareSignal
{
Timestamp = DateTimeOffset.Now,
Category = critical ? "file-churn-critical" : "file-churn",
Confidence = critical ? "medium" : "low",
Process = topProcess?.Process ?? "[multiple]",
Source = "Security file audit",
EventId = 4663,
Evidence = evidence
};
}
private static RansomwareFileChurnSummary Unavailable(int windowMinutes)
{
return new RansomwareFileChurnSummary
{
Enabled = true,
WindowMinutes = windowMinutes
};
}
private static bool TryCreateActivity(EventRecord record, ScannerConfiguration configuration, out FileAuditActivity? activity)
{
activity = null;
if (!record.TimeCreated.HasValue)
{
return false;
}
IReadOnlyDictionary<string, string> data = ReadEventData(record);
if (!data.TryGetValue("ObjectType", out string? objectType) || !string.Equals(objectType, "File", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (!data.TryGetValue("AccessMask", out string? accessMaskText) || !TryParseAccessMask(accessMaskText, out uint accessMask))
{
return false;
}
bool isDelete = (accessMask & DeleteAccessMask) != 0;
bool isWrite = (accessMask & FileWriteAccessMask) != 0;
if (!isDelete && !isWrite)
{
return false;
}
string process = data.TryGetValue("ProcessName", out string? processPath) ? Path.GetFileName(processPath.Trim()) : string.Empty;
if (string.IsNullOrWhiteSpace(process))
{
process = "[unknown]";
}
if (configuration.RansomwareExcludedProcesses.Any(item => string.Equals(item, process, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
activity = new FileAuditActivity
{
Timestamp = new DateTimeOffset(record.TimeCreated.Value).ToUniversalTime(),
RecordId = record.RecordId ?? 0,
Process = process,
IsDelete = isDelete,
IsWrite = isWrite
};
return true;
}
private static IReadOnlyDictionary<string, string> ReadEventData(EventRecord record)
{
XDocument document = XDocument.Parse(record.ToXml());
return document.Descendants().Where(element => element.Name.LocalName == "Data")
.Where(element => element.Attribute("Name") is not null)
.ToDictionary(element => element.Attribute("Name")!.Value, element => element.Value.Trim(), StringComparer.OrdinalIgnoreCase);
}
private static bool TryParseAccessMask(string value, out uint result)
{
string normalized = value.Trim();
NumberStyles style = NumberStyles.Integer;
if (normalized.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
normalized = normalized[2..];
style = NumberStyles.AllowHexSpecifier;
}
return uint.TryParse(normalized, style, CultureInfo.InvariantCulture, out result);
}
private static List<FileAuditActivity> MergeWithState(List<FileAuditActivity> observed, DateTimeOffset windowStart, List<string> errors)
{
FileChurnState stored = LoadState(errors);
long highestObservedRecordId = observed.Count == 0 ? 0 : observed.Max(activity => activity.RecordId);
bool securityLogReset = stored.LastSecurityRecordId > 0 && highestObservedRecordId > 0 && highestObservedRecordId < stored.LastSecurityRecordId;
IEnumerable<FileAuditActivity> fresh = securityLogReset
? observed
: observed.Where(activity => activity.RecordId == 0 || activity.RecordId > stored.LastSecurityRecordId);
List<FileAuditActivity> rolling = (securityLogReset ? [] : stored.Activities)
.Concat(fresh)
.Where(activity => activity.Timestamp >= windowStart)
.GroupBy(activity => activity.RecordId > 0 ? activity.RecordId.ToString(CultureInfo.InvariantCulture) : $"{activity.Timestamp:O}|{activity.Process}|{activity.IsDelete}|{activity.IsWrite}")
.Select(group => group.First())
.OrderBy(activity => activity.Timestamp)
.ToList();
SaveState(new FileChurnState
{
LastSecurityRecordId = securityLogReset ? highestObservedRecordId : Math.Max(stored.LastSecurityRecordId, highestObservedRecordId),
Activities = rolling
}, errors);
return rolling;
}
private static RansomwareFileChurnSummary BuildSummary(List<FileAuditActivity> activities, int windowMinutes, bool isTruncated)
{
return new RansomwareFileChurnSummary
{
Enabled = true,
DataAvailable = true,
IsTruncated = isTruncated,
WindowMinutes = windowMinutes,
FileOperationCount = activities.Count,
DeleteOperationCount = activities.Count(activity => activity.IsDelete),
WriteOperationCount = activities.Count(activity => activity.IsWrite),
DistinctProcessCount = activities.Select(activity => activity.Process).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
TopProcesses = activities.GroupBy(activity => activity.Process, StringComparer.OrdinalIgnoreCase)
.Select(group => new RansomwareFileChurnProcess
{
Process = group.Key,
DeleteOperationCount = group.Count(activity => activity.IsDelete),
WriteOperationCount = group.Count(activity => activity.IsWrite)
})
.OrderByDescending(process => process.DeleteOperationCount + process.WriteOperationCount)
.ThenBy(process => process.Process, StringComparer.OrdinalIgnoreCase)
.Take(5)
.ToList()
};
}
private static FileChurnState LoadState(List<string> errors)
{
string path = GetStatePath();
if (!File.Exists(path))
{
return new FileChurnState();
}
try
{
return JsonSerializer.Deserialize<FileChurnState>(File.ReadAllText(path), JsonOptions.Default) ?? new FileChurnState();
}
catch (Exception exception)
{
errors.Add($"Ransomware file churn state could not be read: {exception.Message}");
return new FileChurnState();
}
}
private static void SaveState(FileChurnState state, List<string> errors)
{
try
{
string path = GetStatePath();
string directory = Path.GetDirectoryName(path)!;
Directory.CreateDirectory(directory);
string temporaryPath = path + ".tmp";
File.WriteAllText(temporaryPath, JsonSerializer.Serialize(state, JsonOptions.Default));
File.Move(temporaryPath, path, overwrite: true);
}
catch (Exception exception)
{
errors.Add($"Ransomware file churn state could not be saved: {exception.Message}");
}
}
private static string GetStatePath()
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "OCSentinel", "state", StateFileName);
}
private sealed record FileChurnState
{
public long LastSecurityRecordId { get; init; }
public List<FileAuditActivity> Activities { get; init; } = [];
}
private sealed record FileAuditActivity
{
public DateTimeOffset Timestamp { get; init; }
public long RecordId { get; init; }
public string Process { get; init; } = string.Empty;
public bool IsDelete { get; init; }
public bool IsWrite { get; init; }
}
}

View File

@@ -4,13 +4,14 @@ internal sealed record ScanOptions
{ {
public const string Usage = """ public const string Usage = """
Usage: Usage:
OCSentinelCli [--output <path>] [--lookback-days <n>] [--top <n>] [--config <path>] [--vulnerability-csv <path>] [--json-only] [--ninja-output] [--fail-on-attacks] [--fail-on-threshold] [--help] OCSentinelCli [--output <path>] [--lookback-days <n>] [--top <n>] [--config <path>] [--client-config <path>] [--vulnerability-csv <path>] [--json-only] [--ninja-output] [--fail-on-attacks] [--fail-on-threshold] [--help]
Options: Options:
--output <path> Write the JSON report to the given file. --output <path> Write the JSON report to the given file.
--lookback-days <n> Only include events newer than now minus n days. Default: 30 --lookback-days <n> Only include events newer than now minus n days. Default: 30
--top <n> Number of aggregated source IPs to show. Default: 10 --top <n> Number of aggregated source IPs to show. Default: 10
--config <path> Load thresholds, path overrides, and exclusions from JSON. --config <path> Load thresholds, path overrides, and exclusions from JSON.
--client-config <path> Load persisted NinjaOne identity and upload settings from JSON.
--vulnerability-csv <path> --vulnerability-csv <path>
Correlate local attack results with exported CVE data for this host. Correlate local attack results with exported CVE data for this host.
--json-only Print only JSON to stdout. --json-only Print only JSON to stdout.
@@ -36,6 +37,8 @@ Options:
public string? ConfigPath { get; init; } public string? ConfigPath { get; init; }
public string? ClientConfigPath { get; init; }
public string? VulnerabilityCsvPath { get; init; } public string? VulnerabilityCsvPath { get; init; }
public bool ShowHelp { get; init; } public bool ShowHelp { get; init; }
@@ -72,6 +75,9 @@ Options:
case "--config": case "--config":
options = options with { ConfigPath = ReadValue(args, ref i, arg) }; options = options with { ConfigPath = ReadValue(args, ref i, arg) };
break; break;
case "--client-config":
options = options with { ClientConfigPath = ReadValue(args, ref i, arg) };
break;
case "--vulnerability-csv": case "--vulnerability-csv":
options = options with { VulnerabilityCsvPath = ReadValue(args, ref i, arg) }; options = options with { VulnerabilityCsvPath = ReadValue(args, ref i, arg) };
break; break;

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OCSentinelCli\OCSentinelCli.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,87 @@
using System.Runtime.Versioning;
using System.Text.Json;
using Xunit;
namespace OCSentinelCli.Tests;
[SupportedOSPlatform("windows")]
public sealed class RansomwareBetaTests
{
[Fact]
public void RansomwareBetaIsEnabledByDefault()
{
Assert.True(new ScannerConfiguration().RansomwareBetaEnabled);
Assert.False(new ScannerConfiguration().RansomwareBetaAlertingEnabled);
}
[Fact]
public void DisabledRansomwareBetaDoesNotReportSensorCoverage()
{
var summary = RansomwareBetaDetector.Scan(new ScannerConfiguration { RansomwareBetaEnabled = false }, []);
Assert.False(summary.Enabled);
Assert.Empty(summary.Sensors);
}
[Fact]
public void SensorCoverageIsIncludedInTheJsonReport()
{
var summary = new RansomwareBetaSummary
{
Enabled = true,
Sensors = [new RansomwareSensorStatus { Name = "security-process-4688", Enabled = true, Available = true, State = "available", EventCount = 4 }]
};
string json = JsonSerializer.Serialize(summary, JsonOptions.Default);
Assert.Contains("\"Sensors\"", json);
Assert.Contains("security-process-4688", json);
}
[Fact]
public void FileChurnBelowBothThresholdsDoesNotCreateSignal()
{
var summary = new RansomwareFileChurnSummary
{
Enabled = true,
DataAvailable = true,
DeleteOperationCount = 49,
WriteOperationCount = 500
};
RansomwareSignal? signal = RansomwareFileChurnDetector.CreateSignal(summary, new ScannerConfiguration());
Assert.Null(signal);
}
[Fact]
public void CriticalFileChurnCreatesOnlyMediumConfidenceSignal()
{
var summary = new RansomwareFileChurnSummary
{
Enabled = true,
DataAvailable = true,
WindowMinutes = 15,
DeleteOperationCount = 200,
WriteOperationCount = 1000,
DistinctProcessCount = 1,
TopProcesses = [new RansomwareFileChurnProcess { Process = "encryptor.exe", DeleteOperationCount = 200, WriteOperationCount = 1000 }]
};
RansomwareSignal? signal = RansomwareFileChurnDetector.CreateSignal(summary, new ScannerConfiguration());
Assert.NotNull(signal);
Assert.Equal("file-churn-critical", signal.Category);
Assert.Equal("medium", signal.Confidence);
Assert.Equal("encryptor.exe", signal.Process);
}
[Fact]
public void PassiveBetaCannotElevateNinjaAlertState()
{
var summary = new RansomwareBetaSummary { Enabled = true, State = "critical" };
Assert.False(RansomwareAlertPolicy.CanElevate(summary, alertingEnabled: false));
Assert.True(RansomwareAlertPolicy.CanElevate(summary, alertingEnabled: true));
}
}