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 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(); 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 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(); 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; } }