5100* tests strengthtenen work
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// Compares vulnerability findings between different scanners.
|
||||
/// </summary>
|
||||
public sealed class VulnerabilityComparisonLogic
|
||||
{
|
||||
/// <summary>
|
||||
/// Compares vulnerability findings from two scanner outputs.
|
||||
/// </summary>
|
||||
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<ExtractedFinding> ExtractFindings(JsonDocument findingsJson, string toolName)
|
||||
{
|
||||
var findings = new List<ExtractedFinding>();
|
||||
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<ExtractedFinding> 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<ExtractedFinding> 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<ExtractedFinding> 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of comparing vulnerability findings.
|
||||
/// </summary>
|
||||
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<ExtractedFinding> OnlyInBaseline { get; set; } = [];
|
||||
public List<ExtractedFinding> OnlyInCandidate { get; set; } = [];
|
||||
public List<ExtractedFinding> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracted vulnerability finding.
|
||||
/// </summary>
|
||||
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}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Severity distribution counts.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerability severity levels.
|
||||
/// </summary>
|
||||
public enum VulnerabilitySeverity
|
||||
{
|
||||
Unknown = 0,
|
||||
None = 1,
|
||||
Low = 2,
|
||||
Medium = 3,
|
||||
High = 4,
|
||||
Critical = 5
|
||||
}
|
||||
Reference in New Issue
Block a user