save checkpoint
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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()
|
||||
}));
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user