save checkpoint

This commit is contained in:
master
2026-02-11 01:32:14 +02:00
parent 5593212b41
commit cf5b72974f
2316 changed files with 68799 additions and 3808 deletions

View File

@@ -25,13 +25,35 @@ public sealed class CvssKevProvider : IRiskScoreProvider
{
ArgumentNullException.ThrowIfNull(request);
var cvssScore = await cvss.GetCvssAsync(request.Subject, cancellationToken).ConfigureAwait(false) ?? 0d;
var cvssScore = request.Signals.TryGetValue("Cvss", out var inlineCvss)
? inlineCvss
: await cvss.GetCvssAsync(request.Subject, cancellationToken).ConfigureAwait(false) ?? 0d;
cvssScore = Math.Clamp(cvssScore, 0d, 10d);
var kevFlag = await kev.IsKevAsync(request.Subject, cancellationToken).ConfigureAwait(false) ?? false;
var kevFlag = TryGetKevFlag(request, out var inlineKev)
? inlineKev
: await kev.IsKevAsync(request.Subject, cancellationToken).ConfigureAwait(false) ?? false;
var kevBonus = kevFlag ? 0.2d : 0d;
var raw = (cvssScore / 10d) + kevBonus;
return Math.Round(Math.Min(1d, raw), 6, MidpointRounding.ToEven);
}
private static bool TryGetKevFlag(ScoreRequest request, out bool kevFlag)
{
if (request.Signals.TryGetValue("Kev", out var kev))
{
kevFlag = kev >= 1d;
return true;
}
if (request.Signals.TryGetValue("IsKev", out var isKev))
{
kevFlag = isKev >= 1d;
return true;
}
kevFlag = false;
return false;
}
}

View File

@@ -24,7 +24,10 @@ public sealed class EpssProvider : IRiskScoreProvider
{
ArgumentNullException.ThrowIfNull(request);
var epssData = await epss.GetEpssAsync(request.Subject, cancellationToken).ConfigureAwait(false);
var signalScore = TryGetSignalScore(request);
var epssData = signalScore.HasValue
? new EpssData(signalScore.Value, request.Signals.TryGetValue("EpssPercentile", out var percentile) ? percentile : 0d)
: await epss.GetEpssAsync(request.Subject, cancellationToken).ConfigureAwait(false);
if (epssData is null)
return 0d; // Unknown = no additional risk signal
@@ -35,6 +38,21 @@ public sealed class EpssProvider : IRiskScoreProvider
return Math.Round(score, 6, MidpointRounding.ToEven);
}
private static double? TryGetSignalScore(ScoreRequest request)
{
if (request.Signals.TryGetValue("EpssScore", out var epssScore))
{
return epssScore;
}
if (request.Signals.TryGetValue("Epss", out var epss))
{
return epss;
}
return null;
}
}
/// <summary>
@@ -82,16 +100,28 @@ public sealed class CvssKevEpssProvider : IRiskScoreProvider
{
ArgumentNullException.ThrowIfNull(request);
// Fetch all signals in parallel
// Fetch all signals in parallel; explicit request signals take precedence.
var cvssTask = cvss.GetCvssAsync(request.Subject, cancellationToken);
var kevTask = kev.IsKevAsync(request.Subject, cancellationToken);
var epssTask = epss.GetEpssAsync(request.Subject, cancellationToken);
await Task.WhenAll(cvssTask, kevTask, epssTask).ConfigureAwait(false);
var cvssScore = Math.Clamp(cvssTask.Result ?? 0d, 0d, 10d);
var kevFlag = kevTask.Result ?? false;
var epssData = epssTask.Result;
var cvssScore = request.Signals.TryGetValue("Cvss", out var inlineCvss)
? inlineCvss
: cvssTask.Result ?? 0d;
cvssScore = Math.Clamp(cvssScore, 0d, 10d);
var kevFlag = TryGetKevFlag(request, out var inlineKev)
? inlineKev
: kevTask.Result ?? false;
var epssScore = request.Signals.TryGetValue("EpssScore", out var inlineEpssScore)
? inlineEpssScore
: (request.Signals.TryGetValue("Epss", out var inlineEpss) ? inlineEpss : epssTask.Result?.Score);
var epssPercentile = request.Signals.TryGetValue("EpssPercentile", out var inlinePercentile)
? inlinePercentile
: epssTask.Result?.Percentile;
// Base score from CVSS (normalized to 0-1)
var baseScore = cvssScore / 10d;
@@ -100,10 +130,17 @@ public sealed class CvssKevEpssProvider : IRiskScoreProvider
var kevBonusValue = kevFlag ? KevBonus : 0d;
// EPSS bonus based on percentile thresholds
var epssBonusValue = ComputeEpssBonus(epssData?.Percentile);
var epssBonusValue = ComputeEpssBonus(epssPercentile);
// If CVSS+KEV are absent, fall back to raw EPSS score contribution.
var epssBase = Math.Clamp(epssScore ?? 0d, 0d, 1d);
// Combined score
var raw = baseScore + kevBonusValue + epssBonusValue;
if (baseScore <= 0d && !kevFlag)
{
raw = epssBase + epssBonusValue;
}
return Math.Round(Math.Min(1d, raw), 6, MidpointRounding.ToEven);
}
@@ -121,4 +158,22 @@ public sealed class CvssKevEpssProvider : IRiskScoreProvider
return 0d;
}
private static bool TryGetKevFlag(ScoreRequest request, out bool kevFlag)
{
if (request.Signals.TryGetValue("Kev", out var kev))
{
kevFlag = kev >= 1d;
return true;
}
if (request.Signals.TryGetValue("IsKev", out var isKev))
{
kevFlag = isKev >= 1d;
return true;
}
kevFlag = false;
return false;
}
}

View File

@@ -172,7 +172,7 @@ public sealed class ExploitMaturityApiTests : IClassFixture<WebApplicationFactor
var content = await response.Content.ReadAsStringAsync();
// Count occurrences - should have single result
var occurrences = content.Split("CVE-2024-1234").Length - 1;
occurrences.Should().BeGreaterOrEqualTo(1);
occurrences.Should().BeGreaterThanOrEqualTo(1);
}
#endregion

View File

@@ -233,13 +233,13 @@ public sealed class ExploitMaturityServiceTests
#region Error Handling
[Fact]
public async Task NullCveId_ThrowsArgumentException()
public async Task NullCveId_ThrowsArgumentNullException()
{
// Arrange
var sut = new ExploitMaturityService(new TestEpssSource(), new TestKevSource(), null, null, _timeProvider);
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(() => sut.AssessMaturityAsync(null!));
await Assert.ThrowsAsync<ArgumentNullException>(() => sut.AssessMaturityAsync(null!));
}
[Fact]

View File

@@ -30,6 +30,9 @@ public class RiskEngineApiTests : IClassFixture<WebApplicationFactory<Program>>
var payload = await response.Content.ReadFromJsonAsync<ProvidersResponse>(cancellationToken: ct);
Assert.NotNull(payload);
Assert.Contains(DefaultTransformsProvider.ProviderName, payload!.Providers);
Assert.Contains(CvssKevProvider.ProviderName, payload.Providers);
Assert.Contains(EpssProvider.ProviderName, payload.Providers);
Assert.Contains(CvssKevEpssProvider.ProviderName, payload.Providers);
}
[Trait("Category", TestCategories.Unit)]
@@ -116,6 +119,86 @@ public class RiskEngineApiTests : IClassFixture<WebApplicationFactory<Program>>
third => Assert.Equal("asset-low", third.Subject));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Simulations_CvssKev_UsesInlineSignals()
{
var client = factory.CreateClient();
var ct = CancellationToken.None;
var requests = new[]
{
new ScoreRequest(CvssKevProvider.ProviderName, "CVE-LOCAL-1001", new Dictionary<string, double>
{
["Cvss"] = 7.5,
["Kev"] = 1
})
};
var response = await client.PostAsJsonAsync("/risk-scores/simulations", requests, ct);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<SimulationResponse>(cancellationToken: ct);
Assert.NotNull(payload);
Assert.Single(payload!.Results);
Assert.True(payload.Results[0].Success);
Assert.Equal(0.95d, payload.Results[0].Score);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Simulations_Epss_UsesInlineSignals()
{
var client = factory.CreateClient();
var ct = CancellationToken.None;
var requests = new[]
{
new ScoreRequest(EpssProvider.ProviderName, "CVE-LOCAL-1002", new Dictionary<string, double>
{
["EpssScore"] = 0.77,
["EpssPercentile"] = 0.93
})
};
var response = await client.PostAsJsonAsync("/risk-scores/simulations", requests, ct);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<SimulationResponse>(cancellationToken: ct);
Assert.NotNull(payload);
Assert.Single(payload!.Results);
Assert.True(payload.Results[0].Success);
Assert.Equal(0.77d, payload.Results[0].Score);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Simulations_CvssKevEpss_UsesInlineSignals()
{
var client = factory.CreateClient();
var ct = CancellationToken.None;
var requests = new[]
{
new ScoreRequest(CvssKevEpssProvider.ProviderName, "CVE-LOCAL-1003", new Dictionary<string, double>
{
["Cvss"] = 5.0,
["Kev"] = 0,
["EpssScore"] = 0.35,
["EpssPercentile"] = 0.92
})
};
var response = await client.PostAsJsonAsync("/risk-scores/simulations", requests, ct);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<SimulationResponse>(cancellationToken: ct);
Assert.NotNull(payload);
Assert.Single(payload!.Results);
Assert.True(payload.Results[0].Success);
Assert.Equal(0.55d, payload.Results[0].Score);
}
private sealed record ProvidersResponse(IReadOnlyList<string> Providers);
private sealed record JobAccepted(Guid JobId, RiskScoreResult Result);
private sealed record SimulationResponse(IReadOnlyList<RiskScoreResult> Results);

View File

@@ -139,6 +139,28 @@ public class RiskScoreWorkerTests
Assert.Equal(1.0d, result.Score); // 0.98 + 0.2 capped at 1.0
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CvssKevProvider_UsesInlineSignalsWhenProvided()
{
var provider = new CvssKevProvider(new FakeCvssSource(new Dictionary<string, double>()), new FakeKevSource(new Dictionary<string, bool>()));
var registry = new RiskScoreProviderRegistry(new[] { provider });
var queue = new RiskScoreQueue();
var worker = new RiskScoreWorker(queue, registry);
var request = new ScoreRequest(CvssKevProvider.ProviderName, "CVE-LOCAL-0001", new Dictionary<string, double>
{
["Cvss"] = 7.5,
["Kev"] = 1
});
await queue.EnqueueAsync(request, CancellationToken.None);
var result = await worker.ProcessNextAsync(CancellationToken.None);
Assert.True(result.Success);
Assert.Equal(0.95d, result.Score); // (7.5/10) + 0.2
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CvssKevProviderHandlesMissingCvss()
@@ -296,6 +318,28 @@ public class RiskScoreWorkerTests
Assert.Equal(0.75d, result.Score);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EpssProvider_UsesInlineSignalsWhenProvided()
{
var provider = new EpssProvider(new FakeEpssSource(new Dictionary<string, EpssData>()));
var registry = new RiskScoreProviderRegistry(new[] { provider });
var queue = new RiskScoreQueue();
var worker = new RiskScoreWorker(queue, registry);
var request = new ScoreRequest(EpssProvider.ProviderName, "CVE-LOCAL-0002", new Dictionary<string, double>
{
["EpssScore"] = 0.77,
["EpssPercentile"] = 0.93
});
await queue.EnqueueAsync(request, CancellationToken.None);
var result = await worker.ProcessNextAsync(CancellationToken.None);
Assert.True(result.Success);
Assert.Equal(0.77d, result.Score);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EpssProviderReturnsZeroForUnknown()
@@ -376,6 +420,33 @@ public class RiskScoreWorkerTests
Assert.Equal(0.55d, result.Score);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CvssKevEpssProvider_UsesInlineSignalsWhenProvided()
{
var provider = new CvssKevEpssProvider(
new FakeCvssSource(new Dictionary<string, double>()),
new FakeKevSource(new Dictionary<string, bool>()),
new FakeEpssSource(new Dictionary<string, EpssData>()));
var registry = new RiskScoreProviderRegistry(new[] { provider });
var queue = new RiskScoreQueue();
var worker = new RiskScoreWorker(queue, registry);
var request = new ScoreRequest(CvssKevEpssProvider.ProviderName, "CVE-LOCAL-0003", new Dictionary<string, double>
{
["Cvss"] = 5.0,
["Kev"] = 0,
["EpssScore"] = 0.35,
["EpssPercentile"] = 0.92
});
await queue.EnqueueAsync(request, CancellationToken.None);
var result = await worker.ProcessNextAsync(CancellationToken.None);
Assert.True(result.Success);
Assert.Equal(0.55d, result.Score); // 0.5 + 0 + 0.05
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CvssKevEpssProviderApplies50thPercentileBonus()

View File

@@ -18,8 +18,7 @@ public static class ExploitMaturityEndpoints
public static IEndpointRouteBuilder MapExploitMaturityEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/exploit-maturity")
.WithTags("ExploitMaturity")
.WithOpenApi();
.WithTags("ExploitMaturity");
// GET /exploit-maturity/{cveId} - Assess exploit maturity for a CVE
group.MapGet("/{cveId}", async (

View File

@@ -20,6 +20,8 @@ builder.Services.AddSingleton<IRiskScoreProviderRegistry>(_ =>
{
new DefaultTransformsProvider(),
new CvssKevProvider(new NullCvssSource(), new NullKevSource()),
new EpssProvider(new NullEpssSource()),
new CvssKevEpssProvider(new NullCvssSource(), new NullKevSource(), new NullEpssSource()),
new VexGateProvider(),
new FixExposureProvider()
}));

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" ?>
<Project Sdk="Microsoft.NET.Sdk.Worker">
<Project Sdk="Microsoft.NET.Sdk.Web">
@@ -16,18 +16,10 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- FrameworkReference Microsoft.AspNetCore.App is provided by Sdk.Web -->
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<!-- Microsoft.Extensions.Hosting is provided by Sdk.Worker -->
@@ -40,7 +32,7 @@
<ProjectReference Include="..\StellaOps.RiskEngine.Infrastructure\StellaOps.RiskEngine.Infrastructure.csproj"/>
<ProjectReference Include="../../../../__Libraries/StellaOps.Worker.Health/StellaOps.Worker.Health.csproj"/>
<ProjectReference Include="../../../__Libraries/StellaOps.Worker.Health/StellaOps.Worker.Health.csproj"/>
</ItemGroup>