// ----------------------------------------------------------------------------- // VulnerabilityComparisonLogic.cs // Sprint: SPRINT_5100_0008_0001_competitor_parity // Task: PARITY-5100-005 - Implement vulnerability finding comparison logic // Description: Logic for comparing vulnerability findings between scanners // ----------------------------------------------------------------------------- using System.Text.Json; namespace StellaOps.Parity.Tests; /// /// Compares vulnerability findings between different scanners. /// public sealed class VulnerabilityComparisonLogic { /// /// Compares vulnerability findings from two scanner outputs. /// public VulnerabilityComparisonResult Compare(ScannerOutput baseline, ScannerOutput candidate) { var result = new VulnerabilityComparisonResult { BaselineTool = baseline.ToolName, CandidateTool = candidate.ToolName, Image = baseline.Image }; if (baseline.FindingsJson is null || candidate.FindingsJson is null) { result.Error = "One or both findings outputs are null"; return result; } try { var baselineFindings = ExtractFindings(baseline.FindingsJson, baseline.ToolName); var candidateFindings = ExtractFindings(candidate.FindingsJson, candidate.ToolName); result.BaselineCveCount = baselineFindings.Count; result.CandidateCveCount = candidateFindings.Count; // Find CVEs in baseline but not in candidate (false negatives for candidate) result.OnlyInBaseline = baselineFindings .Where(bf => !candidateFindings.Any(cf => cf.CveId == bf.CveId)) .ToList(); // Find CVEs in candidate but not in baseline (potential false positives) result.OnlyInCandidate = candidateFindings .Where(cf => !baselineFindings.Any(bf => bf.CveId == cf.CveId)) .ToList(); // Find matching CVEs result.MatchingCves = baselineFindings .Where(bf => candidateFindings.Any(cf => cf.CveId == bf.CveId)) .ToList(); // Calculate severity distribution result.BaselineSeverityDistribution = CalculateSeverityDistribution(baselineFindings); result.CandidateSeverityDistribution = CalculateSeverityDistribution(candidateFindings); // Calculate recall (what percentage of baseline CVEs were found) result.Recall = result.BaselineCveCount > 0 ? (double)result.MatchingCves.Count / result.BaselineCveCount * 100 : 100; // Calculate precision (what percentage of candidate CVEs are true positives) // Note: This is relative to baseline, not ground truth result.Precision = result.CandidateCveCount > 0 ? (double)result.MatchingCves.Count / result.CandidateCveCount * 100 : 100; // F1 score if (result.Precision + result.Recall > 0) { result.F1Score = 2 * (result.Precision * result.Recall) / (result.Precision + result.Recall); } // Calculate false positive rate (CVEs only in candidate) result.FalsePositiveRate = result.CandidateCveCount > 0 ? (double)result.OnlyInCandidate.Count / result.CandidateCveCount * 100 : 0; // Calculate false negative rate (CVEs only in baseline, missed by candidate) result.FalseNegativeRate = result.BaselineCveCount > 0 ? (double)result.OnlyInBaseline.Count / result.BaselineCveCount * 100 : 0; result.Success = true; } catch (Exception ex) { result.Error = ex.Message; } return result; } private List ExtractFindings(JsonDocument findingsJson, string toolName) { var findings = new List(); var root = findingsJson.RootElement; if (toolName.Equals("grype", StringComparison.OrdinalIgnoreCase)) { findings.AddRange(ExtractGrypeFindings(root)); } else if (toolName.Equals("trivy", StringComparison.OrdinalIgnoreCase)) { findings.AddRange(ExtractTrivyFindings(root)); } return findings; } private IEnumerable ExtractGrypeFindings(JsonElement root) { if (!root.TryGetProperty("matches", out var matches)) yield break; foreach (var match in matches.EnumerateArray()) { if (!match.TryGetProperty("vulnerability", out var vuln)) continue; var cveId = vuln.TryGetProperty("id", out var id) ? id.GetString() : null; if (string.IsNullOrEmpty(cveId)) continue; var severity = vuln.TryGetProperty("severity", out var sev) ? ParseSeverity(sev.GetString()) : VulnerabilitySeverity.Unknown; var packageName = ""; var packageVersion = ""; if (match.TryGetProperty("artifact", out var artifact)) { packageName = artifact.TryGetProperty("name", out var name) ? name.GetString() ?? "" : ""; packageVersion = artifact.TryGetProperty("version", out var version) ? version.GetString() ?? "" : ""; } yield return new ExtractedFinding { CveId = cveId, Severity = severity, PackageName = packageName, PackageVersion = packageVersion, FixedVersion = vuln.TryGetProperty("fix", out var fix) && fix.TryGetProperty("versions", out var fixVer) ? string.Join(", ", fixVer.EnumerateArray().Select(v => v.GetString())) : null }; } } private IEnumerable ExtractTrivyFindings(JsonElement root) { if (!root.TryGetProperty("Results", out var results)) yield break; foreach (var result in results.EnumerateArray()) { if (!result.TryGetProperty("Vulnerabilities", out var vulnerabilities)) continue; foreach (var vuln in vulnerabilities.EnumerateArray()) { var cveId = vuln.TryGetProperty("VulnerabilityID", out var id) ? id.GetString() : null; if (string.IsNullOrEmpty(cveId)) continue; var severity = vuln.TryGetProperty("Severity", out var sev) ? ParseSeverity(sev.GetString()) : VulnerabilitySeverity.Unknown; yield return new ExtractedFinding { CveId = cveId, Severity = severity, PackageName = vuln.TryGetProperty("PkgName", out var name) ? name.GetString() ?? "" : "", PackageVersion = vuln.TryGetProperty("InstalledVersion", out var version) ? version.GetString() ?? "" : "", FixedVersion = vuln.TryGetProperty("FixedVersion", out var fix) ? fix.GetString() : null }; } } } private static VulnerabilitySeverity ParseSeverity(string? severity) { return severity?.ToUpperInvariant() switch { "CRITICAL" => VulnerabilitySeverity.Critical, "HIGH" => VulnerabilitySeverity.High, "MEDIUM" => VulnerabilitySeverity.Medium, "LOW" => VulnerabilitySeverity.Low, "NEGLIGIBLE" or "NONE" => VulnerabilitySeverity.None, _ => VulnerabilitySeverity.Unknown }; } private static SeverityDistribution CalculateSeverityDistribution(List findings) { return new SeverityDistribution { Critical = findings.Count(f => f.Severity == VulnerabilitySeverity.Critical), High = findings.Count(f => f.Severity == VulnerabilitySeverity.High), Medium = findings.Count(f => f.Severity == VulnerabilitySeverity.Medium), Low = findings.Count(f => f.Severity == VulnerabilitySeverity.Low), None = findings.Count(f => f.Severity == VulnerabilitySeverity.None), Unknown = findings.Count(f => f.Severity == VulnerabilitySeverity.Unknown) }; } } /// /// Result of comparing vulnerability findings. /// public sealed class VulnerabilityComparisonResult { public required string BaselineTool { get; init; } public required string CandidateTool { get; init; } public required string Image { get; init; } public bool Success { get; set; } public string? Error { get; set; } // CVE counts public int BaselineCveCount { get; set; } public int CandidateCveCount { get; set; } // Matching public List OnlyInBaseline { get; set; } = []; public List OnlyInCandidate { get; set; } = []; public List MatchingCves { get; set; } = []; // Accuracy metrics (relative to baseline) public double Recall { get; set; } public double Precision { get; set; } public double F1Score { get; set; } public double FalsePositiveRate { get; set; } public double FalseNegativeRate { get; set; } // Severity distribution public SeverityDistribution BaselineSeverityDistribution { get; set; } = new(); public SeverityDistribution CandidateSeverityDistribution { get; set; } = new(); } /// /// Extracted vulnerability finding. /// public sealed class ExtractedFinding { public required string CveId { get; init; } public required VulnerabilitySeverity Severity { get; init; } public required string PackageName { get; init; } public required string PackageVersion { get; init; } public string? FixedVersion { get; set; } public double? CvssScore { get; set; } public override string ToString() => $"{CveId} ({Severity}) in {PackageName}@{PackageVersion}"; } /// /// Severity distribution counts. /// public sealed class SeverityDistribution { public int Critical { get; set; } public int High { get; set; } public int Medium { get; set; } public int Low { get; set; } public int None { get; set; } public int Unknown { get; set; } public int Total => Critical + High + Medium + Low + None + Unknown; } /// /// Vulnerability severity levels. /// public enum VulnerabilitySeverity { Unknown = 0, None = 1, Low = 2, Medium = 3, High = 4, Critical = 5 }