Gaps fill up, fixes, ui restructuring
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Gates;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for BeaconRateGate.
|
||||
/// Sprint: SPRINT_20260219_014 (BEA-03)
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Sprint", "20260219.014")]
|
||||
public sealed class BeaconRateGateTests
|
||||
{
|
||||
private readonly TimeProvider _fixedTimeProvider = new FixedTimeProvider(
|
||||
new DateTimeOffset(2026, 2, 19, 14, 0, 0, TimeSpan.Zero));
|
||||
|
||||
#region Gate disabled
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WhenDisabled_ReturnsPass()
|
||||
{
|
||||
var gate = CreateGate(enabled: false);
|
||||
var context = CreateContext("production");
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.True(result.Passed);
|
||||
Assert.Contains("disabled", result.Reason!);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Environment filtering
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_NonRequiredEnvironment_ReturnsPass()
|
||||
{
|
||||
var gate = CreateGate();
|
||||
var context = CreateContext("development");
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.True(result.Passed);
|
||||
Assert.Contains("not required", result.Reason!);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Missing beacon data
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_MissingBeaconData_WarnMode_ReturnsPassWithWarning()
|
||||
{
|
||||
var gate = CreateGate(missingAction: PolicyGateDecisionType.Warn);
|
||||
var context = CreateContext("production");
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.True(result.Passed);
|
||||
Assert.Contains("warn", result.Reason!, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_MissingBeaconData_BlockMode_ReturnsFail()
|
||||
{
|
||||
var gate = CreateGate(missingAction: PolicyGateDecisionType.Block);
|
||||
var context = CreateContext("production");
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.False(result.Passed);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rate threshold enforcement
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_RateAboveThreshold_ReturnsPass()
|
||||
{
|
||||
var gate = CreateGate(minRate: 0.8);
|
||||
var context = CreateContext("production", beaconRate: 0.95, beaconCount: 100);
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.True(result.Passed);
|
||||
Assert.Contains("meets", result.Reason!, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_RateBelowThreshold_WarnMode_ReturnsPassWithWarning()
|
||||
{
|
||||
var gate = CreateGate(minRate: 0.8, belowAction: PolicyGateDecisionType.Warn);
|
||||
var context = CreateContext("production", beaconRate: 0.5, beaconCount: 100);
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.True(result.Passed);
|
||||
Assert.Contains("below threshold", result.Reason!, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_RateBelowThreshold_BlockMode_ReturnsFail()
|
||||
{
|
||||
var gate = CreateGate(minRate: 0.8, belowAction: PolicyGateDecisionType.Block);
|
||||
var context = CreateContext("production", beaconRate: 0.5, beaconCount: 100);
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.False(result.Passed);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Minimum beacon count
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_BelowMinBeaconCount_SkipsRateEnforcement()
|
||||
{
|
||||
var gate = CreateGate(minRate: 0.8, minBeaconCount: 50);
|
||||
// Rate is bad but count is too low to enforce.
|
||||
var context = CreateContext("production", beaconRate: 0.3, beaconCount: 5);
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.True(result.Passed);
|
||||
Assert.Contains("deferred", result.Reason!, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Boundary conditions
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_ExactlyAtThreshold_ReturnsPass()
|
||||
{
|
||||
var gate = CreateGate(minRate: 0.8);
|
||||
var context = CreateContext("production", beaconRate: 0.8, beaconCount: 100);
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.True(result.Passed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_JustBelowThreshold_TriggersAction()
|
||||
{
|
||||
var gate = CreateGate(minRate: 0.8, belowAction: PolicyGateDecisionType.Block);
|
||||
var context = CreateContext("production", beaconRate: 0.7999, beaconCount: 100);
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.False(result.Passed);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
private BeaconRateGate CreateGate(
|
||||
bool enabled = true,
|
||||
double minRate = 0.8,
|
||||
int minBeaconCount = 10,
|
||||
PolicyGateDecisionType missingAction = PolicyGateDecisionType.Warn,
|
||||
PolicyGateDecisionType belowAction = PolicyGateDecisionType.Warn)
|
||||
{
|
||||
var opts = new PolicyGateOptions
|
||||
{
|
||||
BeaconRate = new BeaconRateGateOptions
|
||||
{
|
||||
Enabled = enabled,
|
||||
MinVerificationRate = minRate,
|
||||
MinBeaconCount = minBeaconCount,
|
||||
MissingBeaconAction = missingAction,
|
||||
BelowThresholdAction = belowAction,
|
||||
RequiredEnvironments = new List<string> { "production" },
|
||||
},
|
||||
};
|
||||
var monitor = new StaticOptionsMonitor<PolicyGateOptions>(opts);
|
||||
return new BeaconRateGate(monitor, NullLogger<BeaconRateGate>.Instance, _fixedTimeProvider);
|
||||
}
|
||||
|
||||
private static PolicyGateContext CreateContext(
|
||||
string environment,
|
||||
double? beaconRate = null,
|
||||
int? beaconCount = null)
|
||||
{
|
||||
var metadata = new Dictionary<string, string>();
|
||||
if (beaconRate.HasValue)
|
||||
{
|
||||
metadata["beacon_verification_rate"] = beaconRate.Value.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
if (beaconCount.HasValue)
|
||||
{
|
||||
metadata["beacon_verified_count"] = beaconCount.Value.ToString();
|
||||
}
|
||||
|
||||
return new PolicyGateContext
|
||||
{
|
||||
Environment = environment,
|
||||
SubjectKey = "test-subject",
|
||||
Metadata = metadata,
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _fixedTime;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset fixedTime) => _fixedTime = fixedTime;
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _fixedTime;
|
||||
}
|
||||
|
||||
private sealed class StaticOptionsMonitor<T> : IOptionsMonitor<T>
|
||||
{
|
||||
private readonly T _value;
|
||||
|
||||
public StaticOptionsMonitor(T value) => _value = value;
|
||||
|
||||
public T CurrentValue => _value;
|
||||
public T Get(string? name) => _value;
|
||||
public IDisposable? OnChange(Action<T, string?> listener) => null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Gates;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for ExecutionEvidenceGate.
|
||||
/// Sprint: SPRINT_20260219_013 (SEE-03)
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Sprint", "20260219.013")]
|
||||
public sealed class ExecutionEvidenceGateTests
|
||||
{
|
||||
private readonly TimeProvider _fixedTimeProvider = new FixedTimeProvider(
|
||||
new DateTimeOffset(2026, 2, 19, 12, 0, 0, TimeSpan.Zero));
|
||||
|
||||
#region Gate disabled
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WhenDisabled_ReturnsPass()
|
||||
{
|
||||
var gate = CreateGate(enabled: false);
|
||||
var context = CreateContext("production", hasEvidence: false);
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.True(result.Passed);
|
||||
Assert.Contains("disabled", result.Reason!);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Environment filtering
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_NonRequiredEnvironment_ReturnsPass()
|
||||
{
|
||||
var gate = CreateGate();
|
||||
var context = CreateContext("development", hasEvidence: false);
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.True(result.Passed);
|
||||
Assert.Contains("not required", result.Reason!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_RequiredEnvironment_EnforcesEvidence()
|
||||
{
|
||||
var gate = CreateGate(missingAction: PolicyGateDecisionType.Block);
|
||||
var context = CreateContext("production", hasEvidence: false);
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.False(result.Passed);
|
||||
Assert.Contains("required", result.Reason!);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Evidence present
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_EvidencePresent_ReturnsPass()
|
||||
{
|
||||
var gate = CreateGate();
|
||||
var context = CreateContext("production", hasEvidence: true);
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.True(result.Passed);
|
||||
Assert.Contains("present", result.Reason!);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Missing evidence actions
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_MissingEvidence_WarnMode_ReturnsPassWithWarning()
|
||||
{
|
||||
var gate = CreateGate(missingAction: PolicyGateDecisionType.Warn);
|
||||
var context = CreateContext("production", hasEvidence: false);
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.True(result.Passed);
|
||||
Assert.Contains("warn", result.Reason!, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_MissingEvidence_BlockMode_ReturnsFail()
|
||||
{
|
||||
var gate = CreateGate(missingAction: PolicyGateDecisionType.Block);
|
||||
var context = CreateContext("production", hasEvidence: false);
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.False(result.Passed);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Quality checks
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_InsufficientHotSymbols_ReturnsPassWithWarning()
|
||||
{
|
||||
var gate = CreateGate(minHotSymbols: 10);
|
||||
var context = CreateContext("production", hasEvidence: true, hotSymbolCount: 2);
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.True(result.Passed);
|
||||
Assert.Contains("insufficient", result.Reason!, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_SufficientHotSymbols_ReturnsCleanPass()
|
||||
{
|
||||
var gate = CreateGate(minHotSymbols: 3);
|
||||
var context = CreateContext("production", hasEvidence: true, hotSymbolCount: 15);
|
||||
|
||||
var result = await gate.EvaluateAsync(context);
|
||||
|
||||
Assert.True(result.Passed);
|
||||
Assert.Contains("meets", result.Reason!, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
private ExecutionEvidenceGate CreateGate(
|
||||
bool enabled = true,
|
||||
PolicyGateDecisionType missingAction = PolicyGateDecisionType.Warn,
|
||||
int minHotSymbols = 3,
|
||||
int minCallPaths = 1)
|
||||
{
|
||||
var opts = new PolicyGateOptions
|
||||
{
|
||||
ExecutionEvidence = new ExecutionEvidenceGateOptions
|
||||
{
|
||||
Enabled = enabled,
|
||||
MissingEvidenceAction = missingAction,
|
||||
MinHotSymbolCount = minHotSymbols,
|
||||
MinUniqueCallPaths = minCallPaths,
|
||||
RequiredEnvironments = new List<string> { "production" },
|
||||
},
|
||||
};
|
||||
var monitor = new StaticOptionsMonitor<PolicyGateOptions>(opts);
|
||||
return new ExecutionEvidenceGate(monitor, NullLogger<ExecutionEvidenceGate>.Instance, _fixedTimeProvider);
|
||||
}
|
||||
|
||||
private static PolicyGateContext CreateContext(
|
||||
string environment,
|
||||
bool hasEvidence,
|
||||
int? hotSymbolCount = null,
|
||||
int? uniqueCallPaths = null)
|
||||
{
|
||||
var metadata = new Dictionary<string, string>();
|
||||
if (hasEvidence)
|
||||
{
|
||||
metadata["has_execution_evidence"] = "true";
|
||||
}
|
||||
if (hotSymbolCount.HasValue)
|
||||
{
|
||||
metadata["execution_evidence_hot_symbol_count"] = hotSymbolCount.Value.ToString();
|
||||
}
|
||||
if (uniqueCallPaths.HasValue)
|
||||
{
|
||||
metadata["execution_evidence_unique_call_paths"] = uniqueCallPaths.Value.ToString();
|
||||
}
|
||||
|
||||
return new PolicyGateContext
|
||||
{
|
||||
Environment = environment,
|
||||
SubjectKey = "test-subject",
|
||||
Metadata = metadata,
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _fixedTime;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset fixedTime) => _fixedTime = fixedTime;
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _fixedTime;
|
||||
}
|
||||
|
||||
private sealed class StaticOptionsMonitor<T> : IOptionsMonitor<T>
|
||||
{
|
||||
private readonly T _value;
|
||||
|
||||
public StaticOptionsMonitor(T value) => _value = value;
|
||||
|
||||
public T CurrentValue => _value;
|
||||
public T Get(string? name) => _value;
|
||||
public IDisposable? OnChange(Action<T, string?> listener) => null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Gateway.Endpoints;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Gateway.Tests;
|
||||
|
||||
public sealed class AdvisorySourceEndpointsTests : IClassFixture<TestPolicyGatewayFactory>
|
||||
{
|
||||
private readonly TestPolicyGatewayFactory _factory;
|
||||
|
||||
public AdvisorySourceEndpointsTests(TestPolicyGatewayFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetImpact_ReturnsPolicyImpactPayload()
|
||||
{
|
||||
using var client = CreateTenantClient();
|
||||
|
||||
var response = await client.GetAsync(
|
||||
"/api/v1/advisory-sources/nvd/impact?region=us-east&environment=prod&sourceFamily=nvd",
|
||||
CancellationToken.None);
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var payload = await response.Content.ReadFromJsonAsync<AdvisorySourceImpactResponse>(cancellationToken: CancellationToken.None);
|
||||
payload.Should().NotBeNull();
|
||||
payload!.SourceId.Should().Be("nvd");
|
||||
payload.ImpactedDecisionsCount.Should().Be(4);
|
||||
payload.ImpactSeverity.Should().Be("high");
|
||||
payload.DecisionRefs.Should().ContainSingle();
|
||||
payload.DecisionRefs[0].DecisionId.Should().Be("APR-2201");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetImpact_WithoutTenant_ReturnsBadRequest()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/advisory-sources/nvd/impact", CancellationToken.None);
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetConflicts_DefaultStatus_ReturnsOpenConflicts()
|
||||
{
|
||||
using var client = CreateTenantClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/advisory-sources/nvd/conflicts", CancellationToken.None);
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var payload = await response.Content.ReadFromJsonAsync<AdvisorySourceConflictListResponse>(cancellationToken: CancellationToken.None);
|
||||
payload.Should().NotBeNull();
|
||||
payload!.SourceId.Should().Be("nvd");
|
||||
payload.Status.Should().Be("open");
|
||||
payload.TotalCount.Should().Be(1);
|
||||
payload.Items.Should().ContainSingle();
|
||||
payload.Items[0].AdvisoryId.Should().Be("CVE-2026-1188");
|
||||
payload.Items[0].Status.Should().Be("open");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetConflicts_WithResolvedStatus_ReturnsResolvedConflicts()
|
||||
{
|
||||
using var client = CreateTenantClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/advisory-sources/nvd/conflicts?status=resolved", CancellationToken.None);
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var payload = await response.Content.ReadFromJsonAsync<AdvisorySourceConflictListResponse>(cancellationToken: CancellationToken.None);
|
||||
payload.Should().NotBeNull();
|
||||
payload!.TotalCount.Should().Be(1);
|
||||
payload.Items.Should().ContainSingle();
|
||||
payload.Items[0].Status.Should().Be("resolved");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetConflicts_WithInvalidStatus_ReturnsBadRequest()
|
||||
{
|
||||
using var client = CreateTenantClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/advisory-sources/nvd/conflicts?status=invalid", CancellationToken.None);
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
private HttpClient CreateTenantClient()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "test-tenant");
|
||||
return client;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Policy.Exceptions.Models;
|
||||
using StellaOps.Policy.Exceptions.Repositories;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using System.Text.Json;
|
||||
using IAuditableExceptionRepository = StellaOps.Policy.Exceptions.Repositories.IExceptionRepository;
|
||||
using GatewayProgram = StellaOps.Policy.Gateway.Program;
|
||||
|
||||
@@ -82,6 +83,8 @@ public sealed class TestPolicyGatewayFactory : WebApplicationFactory<GatewayProg
|
||||
services.AddSingleton<IAuditableExceptionRepository, InMemoryExceptionRepository>();
|
||||
services.RemoveAll<IGateDecisionHistoryRepository>();
|
||||
services.AddSingleton<IGateDecisionHistoryRepository, InMemoryGateDecisionHistoryRepository>();
|
||||
services.RemoveAll<IAdvisorySourcePolicyReadRepository>();
|
||||
services.AddSingleton<IAdvisorySourcePolicyReadRepository, InMemoryAdvisorySourcePolicyReadRepository>();
|
||||
|
||||
// Override JWT bearer auth to accept test tokens without real OIDC discovery
|
||||
services.PostConfigure<JwtBearerOptions>(StellaOpsAuthenticationDefaults.AuthenticationScheme, options =>
|
||||
@@ -330,3 +333,124 @@ internal sealed class InMemoryGateDecisionHistoryRepository : IGateDecisionHisto
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of advisory source policy read models for endpoint tests.
|
||||
/// </summary>
|
||||
internal sealed class InMemoryAdvisorySourcePolicyReadRepository : IAdvisorySourcePolicyReadRepository
|
||||
{
|
||||
private readonly AdvisorySourceImpactSnapshot _impact = new(
|
||||
SourceKey: "nvd",
|
||||
SourceFamily: "nvd",
|
||||
Region: "us-east",
|
||||
Environment: "prod",
|
||||
ImpactedDecisionsCount: 4,
|
||||
ImpactSeverity: "high",
|
||||
LastDecisionAt: DateTimeOffset.Parse("2026-02-19T08:10:00Z"),
|
||||
UpdatedAt: DateTimeOffset.Parse("2026-02-19T08:11:00Z"),
|
||||
DecisionRefsJson: """
|
||||
[
|
||||
{
|
||||
"decisionId": "APR-2201",
|
||||
"decisionType": "approval",
|
||||
"label": "Approval APR-2201",
|
||||
"route": "/release-control/approvals/apr-2201"
|
||||
}
|
||||
]
|
||||
""");
|
||||
|
||||
private readonly List<AdvisorySourceConflictRecord> _conflicts =
|
||||
[
|
||||
new(
|
||||
ConflictId: Guid.Parse("49b08f4c-474e-4a88-9f71-b7f74572f9d3"),
|
||||
AdvisoryId: "CVE-2026-1188",
|
||||
PairedSourceKey: "ghsa",
|
||||
ConflictType: "severity_mismatch",
|
||||
Severity: "high",
|
||||
Status: "open",
|
||||
Description: "Severity mismatch between NVD and GHSA.",
|
||||
FirstDetectedAt: DateTimeOffset.Parse("2026-02-19T07:40:00Z"),
|
||||
LastDetectedAt: DateTimeOffset.Parse("2026-02-19T08:05:00Z"),
|
||||
ResolvedAt: null,
|
||||
DetailsJson: """{"lhs":"high","rhs":"critical"}"""),
|
||||
new(
|
||||
ConflictId: Guid.Parse("cb605891-90d5-4081-a17c-e55327ffce34"),
|
||||
AdvisoryId: "CVE-2026-2001",
|
||||
PairedSourceKey: "osv",
|
||||
ConflictType: "remediation_mismatch",
|
||||
Severity: "medium",
|
||||
Status: "resolved",
|
||||
Description: "Remediation mismatch resolved after triage.",
|
||||
FirstDetectedAt: DateTimeOffset.Parse("2026-02-18T11:00:00Z"),
|
||||
LastDetectedAt: DateTimeOffset.Parse("2026-02-18T13:15:00Z"),
|
||||
ResolvedAt: DateTimeOffset.Parse("2026-02-18T14:00:00Z"),
|
||||
DetailsJson: JsonSerializer.Serialize(new { resolution = "accepted_nvd", actor = "security-bot" }))
|
||||
];
|
||||
|
||||
public Task<AdvisorySourceImpactSnapshot> GetImpactAsync(
|
||||
string tenantId,
|
||||
string sourceKey,
|
||||
string? region,
|
||||
string? environment,
|
||||
string? sourceFamily,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!string.Equals(tenantId, "test-tenant", StringComparison.OrdinalIgnoreCase) ||
|
||||
!string.Equals(sourceKey, _impact.SourceKey, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(new AdvisorySourceImpactSnapshot(
|
||||
SourceKey: sourceKey,
|
||||
SourceFamily: sourceFamily,
|
||||
Region: region,
|
||||
Environment: environment,
|
||||
ImpactedDecisionsCount: 0,
|
||||
ImpactSeverity: "none",
|
||||
LastDecisionAt: null,
|
||||
UpdatedAt: null,
|
||||
DecisionRefsJson: "[]"));
|
||||
}
|
||||
|
||||
return Task.FromResult(_impact with
|
||||
{
|
||||
Region = region ?? _impact.Region,
|
||||
Environment = environment ?? _impact.Environment,
|
||||
SourceFamily = sourceFamily ?? _impact.SourceFamily
|
||||
});
|
||||
}
|
||||
|
||||
public Task<AdvisorySourceConflictPage> ListConflictsAsync(
|
||||
string tenantId,
|
||||
string sourceKey,
|
||||
string? status,
|
||||
int limit,
|
||||
int offset,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!string.Equals(tenantId, "test-tenant", StringComparison.OrdinalIgnoreCase) ||
|
||||
!string.Equals(sourceKey, _impact.SourceKey, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(new AdvisorySourceConflictPage(Array.Empty<AdvisorySourceConflictRecord>(), 0));
|
||||
}
|
||||
|
||||
var filtered = _conflicts
|
||||
.Where(item => string.IsNullOrWhiteSpace(status) || string.Equals(item.Status, status, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(static item => item.Severity switch
|
||||
{
|
||||
"critical" => 4,
|
||||
"high" => 3,
|
||||
"medium" => 2,
|
||||
"low" => 1,
|
||||
_ => 0
|
||||
})
|
||||
.ThenByDescending(static item => item.LastDetectedAt)
|
||||
.ThenBy(static item => item.ConflictId)
|
||||
.ToList();
|
||||
|
||||
var page = filtered
|
||||
.Skip(Math.Max(offset, 0))
|
||||
.Take(Math.Clamp(limit, 1, 200))
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult(new AdvisorySourceConflictPage(page, filtered.Count));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user