201 lines
7.2 KiB
C#
201 lines
7.2 KiB
C#
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;
|
|
}
|
|
}
|