Remove legacy AttackTracer repo content
Some checks failed
OfficeCom Sentinel Client / build-client (push) Has been cancelled
Some checks failed
OfficeCom Sentinel Client / build-client (push) Has been cancelled
This commit is contained in:
200
src/OCSentinelCli/VulnerabilityCorrelation.cs
Normal file
200
src/OCSentinelCli/VulnerabilityCorrelation.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace OCSentinelCli;
|
||||
|
||||
internal static class VulnerabilityCorrelation
|
||||
{
|
||||
private static readonly string[] DeviceNameHeaders = ["device", "device_name", "hostname", "computer", "computername", "endpoint", "machine", "system"];
|
||||
private static readonly string[] CveHeaders = ["cve", "cve_id", "cveid", "vulnerability", "vulnerability_id"];
|
||||
private static readonly string[] SeverityHeaders = ["severity", "risk", "level"];
|
||||
private static readonly string[] CvssHeaders = ["cvss", "cvss_score", "score", "base_score"];
|
||||
private static readonly string[] RemediationHeaders = ["remediation", "patch", "kb", "fix", "solution"];
|
||||
|
||||
public static VulnerabilityCorrelationSummary LoadForMachine(string machineName, string csvPath, List<string> errors)
|
||||
{
|
||||
try
|
||||
{
|
||||
string fullPath = Path.GetFullPath(csvPath);
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
errors.Add($"Vulnerability correlation file not found: {fullPath}");
|
||||
return VulnerabilityCorrelationSummary.Empty(fullPath);
|
||||
}
|
||||
|
||||
string[] lines = File.ReadAllLines(fullPath);
|
||||
if (lines.Length == 0)
|
||||
{
|
||||
return VulnerabilityCorrelationSummary.Empty(fullPath);
|
||||
}
|
||||
|
||||
string[] headers = SplitCsvLine(lines[0]);
|
||||
var headerMap = headers
|
||||
.Select((header, index) => new { Header = header.Trim(), Index = index })
|
||||
.ToDictionary(static pair => Normalize(pair.Header), static pair => pair.Index, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
int deviceIndex = FindHeaderIndex(headerMap, DeviceNameHeaders);
|
||||
int cveIndex = FindHeaderIndex(headerMap, CveHeaders);
|
||||
int severityIndex = FindHeaderIndex(headerMap, SeverityHeaders);
|
||||
int cvssIndex = FindHeaderIndex(headerMap, CvssHeaders);
|
||||
int remediationIndex = FindHeaderIndex(headerMap, RemediationHeaders);
|
||||
|
||||
if (deviceIndex < 0 || cveIndex < 0)
|
||||
{
|
||||
errors.Add($"Vulnerability correlation file is missing a device or CVE column: {fullPath}");
|
||||
return VulnerabilityCorrelationSummary.Empty(fullPath);
|
||||
}
|
||||
|
||||
var findings = new List<VulnerabilityFinding>();
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
string line = lines[i];
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string[] cells = SplitCsvLine(line);
|
||||
string deviceName = GetCell(cells, deviceIndex);
|
||||
if (!string.Equals(deviceName, machineName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string cveId = GetCell(cells, cveIndex);
|
||||
if (string.IsNullOrWhiteSpace(cveId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string severity = severityIndex >= 0 ? GetCell(cells, severityIndex) : string.Empty;
|
||||
string remediation = remediationIndex >= 0 ? GetCell(cells, remediationIndex) : string.Empty;
|
||||
double? cvss = TryParseDouble(cvssIndex >= 0 ? GetCell(cells, cvssIndex) : string.Empty);
|
||||
|
||||
findings.Add(new VulnerabilityFinding
|
||||
{
|
||||
DeviceName = deviceName,
|
||||
CveId = cveId,
|
||||
Severity = severity,
|
||||
CvssScore = cvss,
|
||||
Remediation = remediation
|
||||
});
|
||||
}
|
||||
|
||||
return VulnerabilityCorrelationSummary.FromFindings(fullPath, findings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add($"Vulnerability correlation load failed for {csvPath}: {ex.Message}");
|
||||
return VulnerabilityCorrelationSummary.Empty(Path.GetFullPath(csvPath));
|
||||
}
|
||||
}
|
||||
|
||||
public static CorrelationAssessment Assess(
|
||||
string currentAlertState,
|
||||
int totalEvents,
|
||||
VulnerabilityCorrelationSummary vulnerabilitySummary,
|
||||
ScannerConfiguration configuration)
|
||||
{
|
||||
string finalState = currentAlertState;
|
||||
string reason = "No CVE correlation applied.";
|
||||
|
||||
bool hasAttacks = totalEvents > 0;
|
||||
bool hasCriticalCvEs = vulnerabilitySummary.CriticalCount >= configuration.CorrelationCriticalCveThreshold;
|
||||
bool hasWarningCvEs = vulnerabilitySummary.TotalCount >= configuration.CorrelationWarningCveThreshold;
|
||||
|
||||
if (hasAttacks && hasCriticalCvEs)
|
||||
{
|
||||
finalState = "critical";
|
||||
reason = $"Attack activity correlated with {vulnerabilitySummary.CriticalCount} critical/high CVE findings on this endpoint.";
|
||||
}
|
||||
else if (hasAttacks && hasWarningCvEs && string.Equals(finalState, "ok", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
finalState = "warning";
|
||||
reason = $"Attack activity correlated with {vulnerabilitySummary.TotalCount} CVE findings on this endpoint.";
|
||||
}
|
||||
else if (vulnerabilitySummary.TotalCount > 0)
|
||||
{
|
||||
reason = $"Loaded {vulnerabilitySummary.TotalCount} CVE findings for this endpoint, but no alert escalation was required.";
|
||||
}
|
||||
|
||||
return new CorrelationAssessment
|
||||
{
|
||||
FinalAlertState = finalState,
|
||||
CorrelationReason = reason
|
||||
};
|
||||
}
|
||||
|
||||
private static int FindHeaderIndex(Dictionary<string, int> headerMap, string[] candidates)
|
||||
{
|
||||
foreach (string candidate in candidates)
|
||||
{
|
||||
if (headerMap.TryGetValue(Normalize(candidate), out int index))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static string Normalize(string value)
|
||||
{
|
||||
return value.Trim().Replace(" ", "_").Replace("-", "_").ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string[] SplitCsvLine(string line)
|
||||
{
|
||||
var result = new List<string>();
|
||||
var current = new System.Text.StringBuilder();
|
||||
bool inQuotes = false;
|
||||
|
||||
for (int i = 0; i < line.Length; i++)
|
||||
{
|
||||
char c = line[i];
|
||||
if (c == '"')
|
||||
{
|
||||
if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
|
||||
{
|
||||
current.Append('"');
|
||||
i++;
|
||||
}
|
||||
else
|
||||
{
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
}
|
||||
else if (c == ',' && !inQuotes)
|
||||
{
|
||||
result.Add(current.ToString());
|
||||
current.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
current.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(current.ToString());
|
||||
return [.. result];
|
||||
}
|
||||
|
||||
private static string GetCell(string[] cells, int index)
|
||||
{
|
||||
return index >= 0 && index < cells.Length ? cells[index].Trim() : string.Empty;
|
||||
}
|
||||
|
||||
private static double? TryParseDouble(string value)
|
||||
{
|
||||
if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out double result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (double.TryParse(value, NumberStyles.Any, CultureInfo.CurrentCulture, out result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user