Gaps fill up, fixes, ui restructuring

This commit is contained in:
master
2026-02-19 22:10:54 +02:00
parent b5829dce5c
commit 04cacdca8a
331 changed files with 42859 additions and 2174 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}