Implement VEX document verification system with issuer management and signature verification
- Added IIssuerDirectory interface for managing VEX document issuers, including methods for registration, revocation, and trust validation. - Created InMemoryIssuerDirectory class as an in-memory implementation of IIssuerDirectory for testing and single-instance deployments. - Introduced ISignatureVerifier interface for verifying signatures on VEX documents, with support for multiple signature formats. - Developed SignatureVerifier class as the default implementation of ISignatureVerifier, allowing extensibility for different signature formats. - Implemented handlers for DSSE and JWS signature formats, including methods for verification and signature extraction. - Defined various records and enums for issuer and signature metadata, enhancing the structure and clarity of the verification process.
This commit is contained in:
@@ -0,0 +1,662 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Policy.Engine.Simulation;
|
||||
using StellaOps.Policy.RiskProfile.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Simulation;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for RiskSimulationBreakdownService.
|
||||
/// Per POLICY-RISK-67-003.
|
||||
/// </summary>
|
||||
public sealed class RiskSimulationBreakdownServiceTests
|
||||
{
|
||||
private readonly RiskSimulationBreakdownService _service;
|
||||
|
||||
public RiskSimulationBreakdownServiceTests()
|
||||
{
|
||||
_service = new RiskSimulationBreakdownService(
|
||||
NullLogger<RiskSimulationBreakdownService>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_WithValidInput_ReturnsBreakdown()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = CreateTestFindings(5);
|
||||
var result = CreateTestResult(findings, profile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
breakdown.Should().NotBeNull();
|
||||
breakdown.SimulationId.Should().Be(result.SimulationId);
|
||||
breakdown.ProfileRef.Should().NotBeNull();
|
||||
breakdown.ProfileRef.Id.Should().Be(profile.Id);
|
||||
breakdown.SignalAnalysis.Should().NotBeNull();
|
||||
breakdown.OverrideAnalysis.Should().NotBeNull();
|
||||
breakdown.ScoreDistribution.Should().NotBeNull();
|
||||
breakdown.SeverityBreakdown.Should().NotBeNull();
|
||||
breakdown.ActionBreakdown.Should().NotBeNull();
|
||||
breakdown.DeterminismHash.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_SignalAnalysis_ComputesCorrectCoverage()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = CreateTestFindings(10);
|
||||
var result = CreateTestResult(findings, profile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
breakdown.SignalAnalysis.TotalSignals.Should().Be(profile.Signals.Count);
|
||||
breakdown.SignalAnalysis.SignalsUsed.Should().BeGreaterThan(0);
|
||||
breakdown.SignalAnalysis.SignalCoverage.Should().BeGreaterThan(0);
|
||||
breakdown.SignalAnalysis.SignalStats.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_SignalAnalysis_IdentifiesTopContributors()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = CreateTestFindings(20);
|
||||
var result = CreateTestResult(findings, profile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
breakdown.SignalAnalysis.TopContributors.Should().NotBeEmpty();
|
||||
breakdown.SignalAnalysis.TopContributors.Length.Should().BeLessOrEqualTo(10);
|
||||
|
||||
// Top contributors should be ordered by contribution
|
||||
for (var i = 1; i < breakdown.SignalAnalysis.TopContributors.Length; i++)
|
||||
{
|
||||
breakdown.SignalAnalysis.TopContributors[i - 1].TotalContribution
|
||||
.Should().BeGreaterOrEqualTo(breakdown.SignalAnalysis.TopContributors[i].TotalContribution);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_OverrideAnalysis_TracksApplications()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfileWithOverrides();
|
||||
var findings = CreateTestFindingsWithKev(5);
|
||||
var result = CreateTestResultWithOverrides(findings, profile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
breakdown.OverrideAnalysis.Should().NotBeNull();
|
||||
breakdown.OverrideAnalysis.TotalOverridesEvaluated.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_ScoreDistribution_ComputesStatistics()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = CreateTestFindings(50);
|
||||
var result = CreateTestResult(findings, profile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
breakdown.ScoreDistribution.Should().NotBeNull();
|
||||
breakdown.ScoreDistribution.RawScoreStats.Should().NotBeNull();
|
||||
breakdown.ScoreDistribution.NormalizedScoreStats.Should().NotBeNull();
|
||||
breakdown.ScoreDistribution.ScoreBuckets.Should().HaveCount(10);
|
||||
breakdown.ScoreDistribution.Percentiles.Should().ContainKey("p50");
|
||||
breakdown.ScoreDistribution.Percentiles.Should().ContainKey("p90");
|
||||
breakdown.ScoreDistribution.Percentiles.Should().ContainKey("p99");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_ScoreDistribution_ComputesSkewnessAndKurtosis()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = CreateTestFindings(100);
|
||||
var result = CreateTestResult(findings, profile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
var stats = breakdown.ScoreDistribution.NormalizedScoreStats;
|
||||
stats.Skewness.Should().NotBe(0); // With random data, unlikely to be exactly 0
|
||||
// Kurtosis can be any value, just verify it's computed
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_ScoreDistribution_IdentifiesOutliers()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = CreateTestFindings(50);
|
||||
var result = CreateTestResult(findings, profile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
breakdown.ScoreDistribution.Outliers.Should().NotBeNull();
|
||||
breakdown.ScoreDistribution.Outliers.OutlierThreshold.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_SeverityBreakdown_GroupsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = CreateTestFindings(30);
|
||||
var result = CreateTestResult(findings, profile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
breakdown.SeverityBreakdown.Should().NotBeNull();
|
||||
breakdown.SeverityBreakdown.BySeverity.Should().NotBeEmpty();
|
||||
|
||||
// Total count should match findings
|
||||
var totalCount = breakdown.SeverityBreakdown.BySeverity.Values.Sum(b => b.Count);
|
||||
totalCount.Should().Be(findings.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_SeverityBreakdown_ComputesConcentration()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = CreateTestFindings(20);
|
||||
var result = CreateTestResult(findings, profile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
// HHI ranges from 1/n to 1
|
||||
breakdown.SeverityBreakdown.SeverityConcentration.Should().BeGreaterOrEqualTo(0);
|
||||
breakdown.SeverityBreakdown.SeverityConcentration.Should().BeLessOrEqualTo(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_ActionBreakdown_GroupsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = CreateTestFindings(25);
|
||||
var result = CreateTestResult(findings, profile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
breakdown.ActionBreakdown.Should().NotBeNull();
|
||||
breakdown.ActionBreakdown.ByAction.Should().NotBeEmpty();
|
||||
|
||||
// Total count should match findings
|
||||
var totalCount = breakdown.ActionBreakdown.ByAction.Values.Sum(b => b.Count);
|
||||
totalCount.Should().Be(findings.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_ActionBreakdown_ComputesStability()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = CreateTestFindings(20);
|
||||
var result = CreateTestResult(findings, profile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
// Stability ranges from 0 to 1
|
||||
breakdown.ActionBreakdown.DecisionStability.Should().BeGreaterOrEqualTo(0);
|
||||
breakdown.ActionBreakdown.DecisionStability.Should().BeLessOrEqualTo(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_ComponentBreakdown_IncludedByDefault()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = CreateTestFindings(15);
|
||||
var result = CreateTestResult(findings, profile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
breakdown.ComponentBreakdown.Should().NotBeNull();
|
||||
breakdown.ComponentBreakdown!.TotalComponents.Should().BeGreaterThan(0);
|
||||
breakdown.ComponentBreakdown.TopRiskComponents.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_ComponentBreakdown_ExtractsEcosystems()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = CreateMixedEcosystemFindings();
|
||||
var result = CreateTestResult(findings, profile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
breakdown.ComponentBreakdown.Should().NotBeNull();
|
||||
breakdown.ComponentBreakdown!.EcosystemBreakdown.Should().NotBeEmpty();
|
||||
breakdown.ComponentBreakdown.EcosystemBreakdown.Should().ContainKey("npm");
|
||||
breakdown.ComponentBreakdown.EcosystemBreakdown.Should().ContainKey("maven");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_WithQuickOptions_ExcludesComponentBreakdown()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = CreateTestFindings(10);
|
||||
var result = CreateTestResult(findings, profile);
|
||||
var options = RiskSimulationBreakdownOptions.Quick;
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings, options);
|
||||
|
||||
// Assert
|
||||
breakdown.ComponentBreakdown.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_DeterminismHash_IsConsistent()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = CreateTestFindings(10);
|
||||
var result = CreateTestResult(findings, profile);
|
||||
|
||||
// Act
|
||||
var breakdown1 = _service.GenerateBreakdown(result, profile, findings);
|
||||
var breakdown2 = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
breakdown1.DeterminismHash.Should().Be(breakdown2.DeterminismHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateComparisonBreakdown_IncludesRiskTrends()
|
||||
{
|
||||
// Arrange
|
||||
var baseProfile = CreateTestProfile();
|
||||
var compareProfile = CreateTestProfileVariant();
|
||||
var findings = CreateTestFindings(20);
|
||||
var baseResult = CreateTestResult(findings, baseProfile);
|
||||
var compareResult = CreateTestResult(findings, compareProfile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateComparisonBreakdown(
|
||||
baseResult, compareResult,
|
||||
baseProfile, compareProfile,
|
||||
findings);
|
||||
|
||||
// Assert
|
||||
breakdown.RiskTrends.Should().NotBeNull();
|
||||
breakdown.RiskTrends!.ComparisonType.Should().Be("profile_comparison");
|
||||
breakdown.RiskTrends.ScoreTrend.Should().NotBeNull();
|
||||
breakdown.RiskTrends.SeverityTrend.Should().NotBeNull();
|
||||
breakdown.RiskTrends.ActionTrend.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateComparisonBreakdown_TracksImprovementsAndRegressions()
|
||||
{
|
||||
// Arrange
|
||||
var baseProfile = CreateTestProfile();
|
||||
var compareProfile = CreateTestProfile(); // Same profile = no changes
|
||||
var findings = CreateTestFindings(15);
|
||||
var baseResult = CreateTestResult(findings, baseProfile);
|
||||
var compareResult = CreateTestResult(findings, compareProfile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateComparisonBreakdown(
|
||||
baseResult, compareResult,
|
||||
baseProfile, compareProfile,
|
||||
findings);
|
||||
|
||||
// Assert
|
||||
var trends = breakdown.RiskTrends!;
|
||||
var total = trends.FindingsImproved + trends.FindingsWorsened + trends.FindingsUnchanged;
|
||||
total.Should().Be(findings.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_EmptyFindings_ReturnsValidBreakdown()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = Array.Empty<SimulationFinding>();
|
||||
var result = CreateEmptyResult(profile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
breakdown.Should().NotBeNull();
|
||||
breakdown.ScoreDistribution.RawScoreStats.Count.Should().Be(0);
|
||||
breakdown.SeverityBreakdown.BySeverity.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBreakdown_MissingSignals_ReportsImpact()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateTestProfile();
|
||||
var findings = CreateFindingsWithMissingSignals();
|
||||
var result = CreateTestResult(findings, profile);
|
||||
|
||||
// Act
|
||||
var breakdown = _service.GenerateBreakdown(result, profile, findings);
|
||||
|
||||
// Assert
|
||||
breakdown.SignalAnalysis.MissingSignalImpact.Should().NotBeNull();
|
||||
// Some findings have missing signals
|
||||
breakdown.SignalAnalysis.SignalsMissing.Should().BeGreaterOrEqualTo(0);
|
||||
}
|
||||
|
||||
#region Test Helpers
|
||||
|
||||
private static RiskProfileModel CreateTestProfile()
|
||||
{
|
||||
return new RiskProfileModel
|
||||
{
|
||||
Id = "test-profile",
|
||||
Version = "1.0.0",
|
||||
Description = "Test profile for unit tests",
|
||||
Signals = new List<RiskSignal>
|
||||
{
|
||||
new() { Name = "cvss", Source = "nvd", Type = RiskSignalType.Numeric },
|
||||
new() { Name = "kev", Source = "cisa", Type = RiskSignalType.Boolean },
|
||||
new() { Name = "reachability", Source = "scanner", Type = RiskSignalType.Numeric },
|
||||
new() { Name = "exploit_maturity", Source = "epss", Type = RiskSignalType.Categorical }
|
||||
},
|
||||
Weights = new Dictionary<string, double>
|
||||
{
|
||||
["cvss"] = 0.4,
|
||||
["kev"] = 0.3,
|
||||
["reachability"] = 0.2,
|
||||
["exploit_maturity"] = 0.1
|
||||
},
|
||||
Overrides = new RiskOverrides()
|
||||
};
|
||||
}
|
||||
|
||||
private static RiskProfileModel CreateTestProfileWithOverrides()
|
||||
{
|
||||
var profile = CreateTestProfile();
|
||||
profile.Overrides = new RiskOverrides
|
||||
{
|
||||
Severity = new List<SeverityOverride>
|
||||
{
|
||||
new()
|
||||
{
|
||||
When = new Dictionary<string, object> { ["kev"] = true },
|
||||
Set = RiskSeverity.Critical
|
||||
}
|
||||
},
|
||||
Decisions = new List<DecisionOverride>
|
||||
{
|
||||
new()
|
||||
{
|
||||
When = new Dictionary<string, object> { ["kev"] = true },
|
||||
Action = RiskAction.Deny,
|
||||
Reason = "KEV findings must be denied"
|
||||
}
|
||||
}
|
||||
};
|
||||
return profile;
|
||||
}
|
||||
|
||||
private static RiskProfileModel CreateTestProfileVariant()
|
||||
{
|
||||
var profile = CreateTestProfile();
|
||||
profile.Id = "test-profile-variant";
|
||||
profile.Weights = new Dictionary<string, double>
|
||||
{
|
||||
["cvss"] = 0.5, // Higher weight for CVSS
|
||||
["kev"] = 0.2,
|
||||
["reachability"] = 0.2,
|
||||
["exploit_maturity"] = 0.1
|
||||
};
|
||||
return profile;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SimulationFinding> CreateTestFindings(int count)
|
||||
{
|
||||
var random = new Random(42); // Deterministic seed
|
||||
return Enumerable.Range(1, count)
|
||||
.Select(i => new SimulationFinding(
|
||||
$"finding-{i}",
|
||||
$"pkg:npm/package-{i}@{i}.0.0",
|
||||
$"CVE-2024-{i:D4}",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["cvss"] = Math.Round(random.NextDouble() * 10, 1),
|
||||
["kev"] = random.Next(10) < 2, // 20% chance of KEV
|
||||
["reachability"] = Math.Round(random.NextDouble(), 2),
|
||||
["exploit_maturity"] = random.Next(4) switch
|
||||
{
|
||||
0 => "none",
|
||||
1 => "low",
|
||||
2 => "medium",
|
||||
_ => "high"
|
||||
}
|
||||
}))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SimulationFinding> CreateTestFindingsWithKev(int count)
|
||||
{
|
||||
return Enumerable.Range(1, count)
|
||||
.Select(i => new SimulationFinding(
|
||||
$"finding-{i}",
|
||||
$"pkg:npm/package-{i}@{i}.0.0",
|
||||
$"CVE-2024-{i:D4}",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["cvss"] = 8.0 + (i % 3),
|
||||
["kev"] = true, // All have KEV
|
||||
["reachability"] = 0.9,
|
||||
["exploit_maturity"] = "high"
|
||||
}))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SimulationFinding> CreateMixedEcosystemFindings()
|
||||
{
|
||||
return new List<SimulationFinding>
|
||||
{
|
||||
new("f1", "pkg:npm/lodash@4.17.0", "CVE-2024-0001", CreateSignals(7.5)),
|
||||
new("f2", "pkg:npm/express@4.0.0", "CVE-2024-0002", CreateSignals(6.0)),
|
||||
new("f3", "pkg:maven/org.apache.log4j/log4j-core@2.0.0", "CVE-2024-0003", CreateSignals(9.8)),
|
||||
new("f4", "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.0", "CVE-2024-0004", CreateSignals(7.2)),
|
||||
new("f5", "pkg:pypi/requests@2.25.0", "CVE-2024-0005", CreateSignals(5.5)),
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SimulationFinding> CreateFindingsWithMissingSignals()
|
||||
{
|
||||
return new List<SimulationFinding>
|
||||
{
|
||||
new("f1", "pkg:npm/a@1.0.0", "CVE-2024-0001",
|
||||
new Dictionary<string, object?> { ["cvss"] = 7.0 }), // Missing kev, reachability
|
||||
new("f2", "pkg:npm/b@1.0.0", "CVE-2024-0002",
|
||||
new Dictionary<string, object?> { ["cvss"] = 6.0, ["kev"] = false }), // Missing reachability
|
||||
new("f3", "pkg:npm/c@1.0.0", "CVE-2024-0003",
|
||||
new Dictionary<string, object?> { ["cvss"] = 8.0, ["kev"] = true, ["reachability"] = 0.5 }), // All present
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> CreateSignals(double cvss)
|
||||
{
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["cvss"] = cvss,
|
||||
["kev"] = cvss >= 9.0,
|
||||
["reachability"] = 0.7,
|
||||
["exploit_maturity"] = cvss >= 8.0 ? "high" : "medium"
|
||||
};
|
||||
}
|
||||
|
||||
private static RiskSimulationResult CreateTestResult(
|
||||
IReadOnlyList<SimulationFinding> findings,
|
||||
RiskProfileModel profile)
|
||||
{
|
||||
var findingScores = findings.Select(f =>
|
||||
{
|
||||
var cvss = f.Signals.GetValueOrDefault("cvss") switch
|
||||
{
|
||||
double d => d,
|
||||
_ => 5.0
|
||||
};
|
||||
var kev = f.Signals.GetValueOrDefault("kev") switch
|
||||
{
|
||||
bool b => b,
|
||||
_ => false
|
||||
};
|
||||
var reachability = f.Signals.GetValueOrDefault("reachability") switch
|
||||
{
|
||||
double d => d,
|
||||
_ => 0.5
|
||||
};
|
||||
|
||||
var rawScore = cvss * 0.4 + (kev ? 1.0 : 0.0) * 0.3 + reachability * 0.2;
|
||||
var normalizedScore = Math.Clamp(rawScore * 10, 0, 100);
|
||||
var severity = normalizedScore switch
|
||||
{
|
||||
>= 90 => RiskSeverity.Critical,
|
||||
>= 70 => RiskSeverity.High,
|
||||
>= 40 => RiskSeverity.Medium,
|
||||
>= 10 => RiskSeverity.Low,
|
||||
_ => RiskSeverity.Informational
|
||||
};
|
||||
var action = severity switch
|
||||
{
|
||||
RiskSeverity.Critical or RiskSeverity.High => RiskAction.Deny,
|
||||
RiskSeverity.Medium => RiskAction.Review,
|
||||
_ => RiskAction.Allow
|
||||
};
|
||||
|
||||
var contributions = new List<SignalContribution>
|
||||
{
|
||||
new("cvss", cvss, 0.4, cvss * 0.4, rawScore > 0 ? cvss * 0.4 / rawScore * 100 : 0),
|
||||
new("kev", kev, 0.3, (kev ? 1.0 : 0.0) * 0.3, rawScore > 0 ? (kev ? 0.3 : 0.0) / rawScore * 100 : 0),
|
||||
new("reachability", reachability, 0.2, reachability * 0.2, rawScore > 0 ? reachability * 0.2 / rawScore * 100 : 0)
|
||||
};
|
||||
|
||||
return new FindingScore(
|
||||
f.FindingId,
|
||||
rawScore,
|
||||
normalizedScore,
|
||||
severity,
|
||||
action,
|
||||
contributions,
|
||||
null);
|
||||
}).ToList();
|
||||
|
||||
var aggregateMetrics = new AggregateRiskMetrics(
|
||||
findings.Count,
|
||||
findingScores.Count > 0 ? findingScores.Average(s => s.NormalizedScore) : 0,
|
||||
findingScores.Count > 0 ? findingScores.OrderBy(s => s.NormalizedScore).ElementAt(findingScores.Count / 2).NormalizedScore : 0,
|
||||
0, // std dev
|
||||
findingScores.Count > 0 ? findingScores.Max(s => s.NormalizedScore) : 0,
|
||||
findingScores.Count > 0 ? findingScores.Min(s => s.NormalizedScore) : 0,
|
||||
findingScores.Count(s => s.Severity == RiskSeverity.Critical),
|
||||
findingScores.Count(s => s.Severity == RiskSeverity.High),
|
||||
findingScores.Count(s => s.Severity == RiskSeverity.Medium),
|
||||
findingScores.Count(s => s.Severity == RiskSeverity.Low),
|
||||
findingScores.Count(s => s.Severity == RiskSeverity.Informational));
|
||||
|
||||
return new RiskSimulationResult(
|
||||
SimulationId: $"rsim-test-{Guid.NewGuid():N}",
|
||||
ProfileId: profile.Id,
|
||||
ProfileVersion: profile.Version,
|
||||
ProfileHash: $"sha256:test{profile.Id.GetHashCode():x8}",
|
||||
Timestamp: DateTimeOffset.UtcNow,
|
||||
FindingScores: findingScores,
|
||||
Distribution: null,
|
||||
TopMovers: null,
|
||||
AggregateMetrics: aggregateMetrics,
|
||||
ExecutionTimeMs: 10.5);
|
||||
}
|
||||
|
||||
private static RiskSimulationResult CreateTestResultWithOverrides(
|
||||
IReadOnlyList<SimulationFinding> findings,
|
||||
RiskProfileModel profile)
|
||||
{
|
||||
var result = CreateTestResult(findings, profile);
|
||||
|
||||
// Add overrides to findings with KEV
|
||||
var findingScoresWithOverrides = result.FindingScores.Select(fs =>
|
||||
{
|
||||
var finding = findings.FirstOrDefault(f => f.FindingId == fs.FindingId);
|
||||
var kev = finding?.Signals.GetValueOrDefault("kev") switch { bool b => b, _ => false };
|
||||
|
||||
if (kev)
|
||||
{
|
||||
return fs with
|
||||
{
|
||||
Severity = RiskSeverity.Critical,
|
||||
RecommendedAction = RiskAction.Deny,
|
||||
OverridesApplied = new List<AppliedOverride>
|
||||
{
|
||||
new("severity",
|
||||
new Dictionary<string, object> { ["kev"] = true },
|
||||
fs.Severity.ToString(),
|
||||
RiskSeverity.Critical.ToString(),
|
||||
null),
|
||||
new("decision",
|
||||
new Dictionary<string, object> { ["kev"] = true },
|
||||
fs.RecommendedAction.ToString(),
|
||||
RiskAction.Deny.ToString(),
|
||||
"KEV findings must be denied")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return fs;
|
||||
}).ToList();
|
||||
|
||||
return result with { FindingScores = findingScoresWithOverrides };
|
||||
}
|
||||
|
||||
private static RiskSimulationResult CreateEmptyResult(RiskProfileModel profile)
|
||||
{
|
||||
return new RiskSimulationResult(
|
||||
SimulationId: "rsim-empty",
|
||||
ProfileId: profile.Id,
|
||||
ProfileVersion: profile.Version,
|
||||
ProfileHash: "sha256:empty",
|
||||
Timestamp: DateTimeOffset.UtcNow,
|
||||
FindingScores: Array.Empty<FindingScore>(),
|
||||
Distribution: null,
|
||||
TopMovers: null,
|
||||
AggregateMetrics: new AggregateRiskMetrics(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
|
||||
ExecutionTimeMs: 1.0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user