Files
oc-sentinel/src/OCSentinelCli/RansomwareBetaDetector.cs
OfficeCom Codex c3ca95dfa5
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 51s
Implement reversible Sentinel beta foundation
2026-07-30 01:05:30 +02:00

173 lines
7.7 KiB
C#

using System.Diagnostics.Eventing.Reader;
using System.Runtime.Versioning;
namespace OCSentinelCli;
[SupportedOSPlatform("windows")]
internal static class RansomwareBetaDetector
{
public static RansomwareBetaSummary Scan(ScannerConfiguration configuration, List<string> errors)
{
if (!configuration.RansomwareBetaEnabled)
{
return new RansomwareBetaSummary();
}
int lookbackMinutes = Math.Clamp(configuration.RansomwareLookbackMinutes, 1, 60);
DateTimeOffset since = DateTimeOffset.UtcNow.AddMinutes(-lookbackMinutes);
var signals = new List<RansomwareSignal>();
ScanSecurityProcesses(signals, errors, since, configuration);
ScanPowerShellScriptBlocks(signals, errors, since, configuration);
ScanSysmonProcesses(signals, errors, since, configuration);
List<RansomwareSignal> distinctSignals = signals
.OrderBy(signal => signal.Timestamp)
.GroupBy(signal => $"{signal.Category}\u001f{signal.Process}", StringComparer.OrdinalIgnoreCase)
.Select(group => group.First())
.Take(20)
.ToList();
int strongSignals = distinctSignals.Count(signal => signal.Confidence == "high");
string state = distinctSignals.Count >= Math.Max(2, configuration.RansomwareCriticalSignalCount)
? "critical"
: strongSignals > 0 || distinctSignals.Count >= Math.Max(2, configuration.RansomwareWarningSignalCount)
? "warning"
: distinctSignals.Count > 0 ? "hint" : "ok";
string reason = state switch
{
"critical" => $"Ransomware beta detected {distinctSignals.Count} independent high-risk signals within {lookbackMinutes} minutes.",
"warning" => $"Ransomware beta detected {strongSignals} high-confidence and {distinctSignals.Count - strongSignals} low-confidence signals within {lookbackMinutes} minutes.",
"hint" => $"Ransomware beta observed an isolated low-confidence signal within {lookbackMinutes} minutes.",
_ => $"Ransomware beta found no suspicious process activity in the last {lookbackMinutes} minutes."
};
return new RansomwareBetaSummary
{
Enabled = true,
State = state,
Reason = reason,
LookbackMinutes = lookbackMinutes,
Signals = distinctSignals
};
}
private static void 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));
}
private static void 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));
}
private static void 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));
}
private static void AddSignal(List<RansomwareSignal> signals, EventRecord record, string process, string commandLine, string source, ScannerConfiguration configuration)
{
if (!record.TimeCreated.HasValue || string.IsNullOrWhiteSpace(commandLine))
{
return;
}
string processName = Path.GetFileName(process.Trim());
if (configuration.RansomwareExcludedProcesses.Any(item => string.Equals(item, processName, StringComparison.OrdinalIgnoreCase)))
{
return;
}
RansomwareSignal? signal = Classify(record.TimeCreated.Value, record.Id, processName, commandLine, source);
if (signal is not null)
{
signals.Add(signal);
}
}
private static RansomwareSignal? Classify(DateTime timestamp, int eventId, string process, string commandLine, string source)
{
string value = commandLine.ToLowerInvariant();
string evidence = commandLine.Length > 512 ? commandLine[..512] : commandLine;
if (ContainsAll(value, "vssadmin", "delete", "shadow") || ContainsAll(value, "wmic", "shadowcopy", "delete") || ContainsAll(value, "win32_shadowcopy", "delete"))
{
return CreateSignal(timestamp, eventId, "shadow-copy-deletion", "high", process, source, evidence);
}
if (ContainsAll(value, "wbadmin", "delete") || ContainsAll(value, "catalog", "delete"))
{
return CreateSignal(timestamp, eventId, "backup-catalog-deletion", "high", process, source, evidence);
}
if (ContainsAll(value, "bcdedit", "recoveryenabled", "no") || ContainsAll(value, "bcdedit", "bootstatuspolicy", "ignoreallfailures"))
{
return CreateSignal(timestamp, eventId, "recovery-disable", "high", process, source, evidence);
}
if (ContainsAll(value, "wevtutil", " cl "))
{
return CreateSignal(timestamp, eventId, "event-log-clearing", "high", process, source, evidence);
}
return value.Contains("win32_shadowcopy", StringComparison.Ordinal)
? CreateSignal(timestamp, eventId, "shadow-copy-access", "low", process, source, evidence)
: null;
}
private static RansomwareSignal CreateSignal(DateTime timestamp, int eventId, string category, string confidence, string process, string source, string evidence)
{
return new RansomwareSignal
{
Timestamp = new DateTimeOffset(timestamp).ToLocalTime(),
Category = category,
Confidence = confidence,
Process = string.IsNullOrWhiteSpace(process) ? "[unknown]" : process,
Source = source,
EventId = eventId,
Evidence = evidence
};
}
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)
{
try
{
long milliseconds = Math.Max(1, (long)(DateTimeOffset.UtcNow - since).TotalMilliseconds);
string query = $"*[System[(EventID={eventId}) and TimeCreated[timediff(@SystemTime) <= {milliseconds}]]]";
using var reader = new EventLogReader(new EventLogQuery(logName, PathType.LogName, query));
for (EventRecord? record = reader.ReadEvent(); record is not null; record = reader.ReadEvent())
{
using (record)
{
processRecord(record);
}
}
}
catch (EventLogNotFoundException)
{
}
catch (Exception exception)
{
errors.Add($"Ransomware beta query failed for {logName}: {exception.Message}");
}
}
private static string ReadProperty(EventRecord record, int index) => index >= 0 && index < record.Properties.Count ? record.Properties[index].Value?.ToString()?.Trim() ?? string.Empty : string.Empty;
private static string FormatDescription(EventRecord record)
{
try
{
return record.FormatDescription() ?? string.Empty;
}
catch (EventLogException)
{
return string.Empty;
}
}
}