// -----------------------------------------------------------------------------
// 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
}