docs re-org, audit fixes, build fixes
This commit is contained in:
@@ -375,8 +375,8 @@ app.MapConflictsApi();
|
||||
|
||||
app.Run();
|
||||
|
||||
// Make Program class partial to allow integration testing while keeping it minimal
|
||||
// Make Program class internal to prevent type conflicts when referencing this assembly
|
||||
namespace StellaOps.Policy.Engine
|
||||
{
|
||||
public partial class Program { }
|
||||
internal partial class Program { }
|
||||
}
|
||||
|
||||
@@ -6,12 +6,18 @@ namespace StellaOps.Policy.Engine.Services;
|
||||
internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, PolicyPackRecord> packs = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryPolicyPackRepository(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<PolicyPackRecord> CreateAsync(string packId, string? displayName, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(packId);
|
||||
|
||||
var created = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, displayName, DateTimeOffset.UtcNow));
|
||||
var created = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, displayName, _timeProvider.GetUtcNow()));
|
||||
return Task.FromResult(created);
|
||||
}
|
||||
|
||||
@@ -25,15 +31,15 @@ internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository
|
||||
|
||||
public Task<PolicyRevisionRecord> UpsertRevisionAsync(string packId, int version, bool requiresTwoPersonApproval, PolicyRevisionStatus initialStatus, CancellationToken cancellationToken)
|
||||
{
|
||||
var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, DateTimeOffset.UtcNow));
|
||||
var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, _timeProvider.GetUtcNow()));
|
||||
int revisionVersion = version > 0 ? version : pack.GetNextVersion();
|
||||
var revision = pack.GetOrAddRevision(
|
||||
revisionVersion,
|
||||
v => new PolicyRevisionRecord(v, requiresTwoPersonApproval, initialStatus, DateTimeOffset.UtcNow));
|
||||
v => new PolicyRevisionRecord(v, requiresTwoPersonApproval, initialStatus, _timeProvider.GetUtcNow()));
|
||||
|
||||
if (revision.Status != initialStatus)
|
||||
{
|
||||
revision.SetStatus(initialStatus, DateTimeOffset.UtcNow);
|
||||
revision.SetStatus(initialStatus, _timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
return Task.FromResult(revision);
|
||||
@@ -95,9 +101,9 @@ internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bundle);
|
||||
|
||||
var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, DateTimeOffset.UtcNow));
|
||||
var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, _timeProvider.GetUtcNow()));
|
||||
var revision = pack.GetOrAddRevision(version > 0 ? version : pack.GetNextVersion(),
|
||||
v => new PolicyRevisionRecord(v, requiresTwoPerson: false, status: PolicyRevisionStatus.Draft, DateTimeOffset.UtcNow));
|
||||
v => new PolicyRevisionRecord(v, requiresTwoPerson: false, status: PolicyRevisionStatus.Draft, _timeProvider.GetUtcNow()));
|
||||
|
||||
revision.SetBundle(bundle);
|
||||
return Task.FromResult(bundle);
|
||||
|
||||
@@ -13,6 +13,12 @@ namespace StellaOps.Policy.Engine.Storage.InMemory;
|
||||
public sealed class InMemoryExceptionRepository : IExceptionRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<(string Tenant, Guid Id), ExceptionEntity> _exceptions = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryExceptionRepository(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<ExceptionEntity> CreateAsync(ExceptionEntity exception, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -123,7 +129,7 @@ public sealed class InMemoryExceptionRepository : IExceptionRepository
|
||||
_exceptions[key] = Copy(
|
||||
existing,
|
||||
statusOverride: ExceptionStatus.Revoked,
|
||||
revokedAtOverride: DateTimeOffset.UtcNow,
|
||||
revokedAtOverride: _timeProvider.GetUtcNow(),
|
||||
revokedByOverride: revokedBy);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
@@ -133,7 +139,7 @@ public sealed class InMemoryExceptionRepository : IExceptionRepository
|
||||
|
||||
public Task<int> ExpireAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var normalizedTenant = Normalize(tenantId);
|
||||
var expired = 0;
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ public static class ExceptionApprovalEndpoints
|
||||
CreateApprovalRequestDto request,
|
||||
IExceptionApprovalRepository repository,
|
||||
IExceptionApprovalRulesService rulesService,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
ILogger<ExceptionApprovalRequestEntity> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -110,7 +111,7 @@ public static class ExceptionApprovalEndpoints
|
||||
}
|
||||
|
||||
// Generate request ID
|
||||
var requestId = $"EAR-{DateTimeOffset.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
|
||||
var requestId = $"EAR-{timeProvider.GetUtcNow():yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
|
||||
|
||||
// Parse gate level
|
||||
if (!Enum.TryParse<GateLevel>(request.GateLevel, ignoreCase: true, out var gateLevel))
|
||||
@@ -139,7 +140,7 @@ public static class ExceptionApprovalEndpoints
|
||||
});
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var entity = new ExceptionApprovalRequestEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
|
||||
@@ -134,6 +134,7 @@ public static class ExceptionEndpoints
|
||||
CreateExceptionRequest request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (request is null)
|
||||
@@ -146,7 +147,7 @@ public static class ExceptionEndpoints
|
||||
}
|
||||
|
||||
// Validate expiry is in future
|
||||
if (request.ExpiresAt <= DateTimeOffset.UtcNow)
|
||||
if (request.ExpiresAt <= timeProvider.GetUtcNow())
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
@@ -157,7 +158,7 @@ public static class ExceptionEndpoints
|
||||
}
|
||||
|
||||
// Validate expiry is not more than 1 year
|
||||
if (request.ExpiresAt > DateTimeOffset.UtcNow.AddYears(1))
|
||||
if (request.ExpiresAt > timeProvider.GetUtcNow().AddYears(1))
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
@@ -188,8 +189,8 @@ public static class ExceptionEndpoints
|
||||
},
|
||||
OwnerId = request.OwnerId,
|
||||
RequesterId = actorId,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
CreatedAt = timeProvider.GetUtcNow(),
|
||||
UpdatedAt = timeProvider.GetUtcNow(),
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
ReasonCode = ParseReasonRequired(request.ReasonCode),
|
||||
Rationale = request.Rationale,
|
||||
@@ -210,6 +211,7 @@ public static class ExceptionEndpoints
|
||||
UpdateExceptionRequest request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -238,7 +240,7 @@ public static class ExceptionEndpoints
|
||||
var updated = existing with
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = timeProvider.GetUtcNow(),
|
||||
Rationale = request.Rationale ?? existing.Rationale,
|
||||
EvidenceRefs = request.EvidenceRefs?.ToImmutableArray() ?? existing.EvidenceRefs,
|
||||
CompensatingControls = request.CompensatingControls?.ToImmutableArray() ?? existing.CompensatingControls,
|
||||
@@ -258,6 +260,7 @@ public static class ExceptionEndpoints
|
||||
ApproveExceptionRequest? request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -294,8 +297,8 @@ public static class ExceptionEndpoints
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
Status = ExceptionStatus.Approved,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
ApprovedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = timeProvider.GetUtcNow(),
|
||||
ApprovedAt = timeProvider.GetUtcNow(),
|
||||
ApproverIds = existing.ApproverIds.Add(actorId)
|
||||
};
|
||||
|
||||
@@ -310,6 +313,7 @@ public static class ExceptionEndpoints
|
||||
string id,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -335,7 +339,7 @@ public static class ExceptionEndpoints
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
Status = ExceptionStatus.Active,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
UpdatedAt = timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
var result = await repository.UpdateAsync(
|
||||
@@ -350,6 +354,7 @@ public static class ExceptionEndpoints
|
||||
ExtendExceptionRequest request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -384,7 +389,7 @@ public static class ExceptionEndpoints
|
||||
var updated = existing with
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = timeProvider.GetUtcNow(),
|
||||
ExpiresAt = request.NewExpiresAt
|
||||
};
|
||||
|
||||
@@ -400,6 +405,7 @@ public static class ExceptionEndpoints
|
||||
[FromBody] RevokeExceptionRequest? request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -425,7 +431,7 @@ public static class ExceptionEndpoints
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
Status = ExceptionStatus.Revoked,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
UpdatedAt = timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
var result = await repository.UpdateAsync(
|
||||
|
||||
@@ -39,6 +39,7 @@ public static class GateEndpoints
|
||||
IBaselineSelector baselineSelector,
|
||||
IGateBypassAuditor bypassAuditor,
|
||||
IMemoryCache cache,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
ILogger<DriftGateEvaluator> logger,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
@@ -79,12 +80,12 @@ public static class GateEndpoints
|
||||
|
||||
return Results.Ok(new GateEvaluateResponse
|
||||
{
|
||||
DecisionId = $"gate:{DateTimeOffset.UtcNow:yyyyMMddHHmmss}:{Guid.NewGuid():N}",
|
||||
DecisionId = $"gate:{timeProvider.GetUtcNow():yyyyMMddHHmmss}:{Guid.NewGuid():N}",
|
||||
Status = GateStatus.Pass,
|
||||
ExitCode = GateExitCodes.Pass,
|
||||
ImageDigest = request.ImageDigest,
|
||||
BaselineRef = request.BaselineRef,
|
||||
DecidedAt = DateTimeOffset.UtcNow,
|
||||
DecidedAt = timeProvider.GetUtcNow(),
|
||||
Summary = "First build - no baseline for comparison",
|
||||
Advisory = "This appears to be a first build. Future builds will be compared against this baseline."
|
||||
});
|
||||
@@ -224,7 +225,8 @@ public static class GateEndpoints
|
||||
.WithDescription("Retrieve a previous gate evaluation decision by ID");
|
||||
|
||||
// GET /api/v1/policy/gate/health - Health check for gate service
|
||||
gates.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTimeOffset.UtcNow }))
|
||||
gates.MapGet("/health", ([FromServices] TimeProvider timeProvider) =>
|
||||
Results.Ok(new { status = "healthy", timestamp = timeProvider.GetUtcNow() }))
|
||||
.WithName("GateHealth")
|
||||
.WithDescription("Health check for the gate evaluation service");
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ public static class GovernanceEndpoints
|
||||
|
||||
private static Task<IResult> GetSealedModeStatusAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
[FromQuery] string? tenantId)
|
||||
{
|
||||
var tenant = tenantId ?? GetTenantId(httpContext) ?? "default";
|
||||
@@ -118,7 +119,7 @@ public static class GovernanceEndpoints
|
||||
.Select(MapOverrideToResponse)
|
||||
.ToList(),
|
||||
VerificationStatus = "verified",
|
||||
LastVerifiedAt = DateTimeOffset.UtcNow.ToString("O")
|
||||
LastVerifiedAt = timeProvider.GetUtcNow().ToString("O")
|
||||
};
|
||||
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
@@ -140,11 +141,12 @@ public static class GovernanceEndpoints
|
||||
|
||||
private static Task<IResult> ToggleSealedModeAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
SealedModeToggleRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
var state = SealedModeStates.GetOrAdd(tenant, _ => new SealedModeState());
|
||||
|
||||
@@ -173,7 +175,7 @@ public static class GovernanceEndpoints
|
||||
|
||||
// Audit
|
||||
RecordAudit(tenant, actor, "sealed_mode_toggled", "sealed-mode", "system_config",
|
||||
$"{(request.Enable ? "Enabled" : "Disabled")} sealed mode: {request.Reason}");
|
||||
$"{(request.Enable ? "Enabled" : "Disabled")} sealed mode: {request.Reason}", timeProvider);
|
||||
|
||||
var response = new SealedModeStatusResponse
|
||||
{
|
||||
@@ -193,11 +195,12 @@ public static class GovernanceEndpoints
|
||||
|
||||
private static Task<IResult> CreateSealedModeOverrideAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
SealedModeOverrideRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
var overrideId = $"override-{Guid.NewGuid():N}";
|
||||
var entity = new SealedModeOverrideEntity
|
||||
@@ -217,13 +220,14 @@ public static class GovernanceEndpoints
|
||||
Overrides[overrideId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "sealed_mode_override_created", overrideId, "sealed_mode_override",
|
||||
$"Created override for {request.Target}: {request.Reason}");
|
||||
$"Created override for {request.Target}: {request.Reason}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.Ok(MapOverrideToResponse(entity)));
|
||||
}
|
||||
|
||||
private static Task<IResult> RevokeSealedModeOverrideAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
string overrideId,
|
||||
RevokeOverrideRequest request)
|
||||
{
|
||||
@@ -243,7 +247,7 @@ public static class GovernanceEndpoints
|
||||
Overrides[overrideId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "sealed_mode_override_revoked", overrideId, "sealed_mode_override",
|
||||
$"Revoked override: {request.Reason}");
|
||||
$"Revoked override: {request.Reason}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.NoContent());
|
||||
}
|
||||
@@ -289,11 +293,12 @@ public static class GovernanceEndpoints
|
||||
|
||||
private static Task<IResult> CreateRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CreateRiskProfileRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
var profileId = $"profile-{Guid.NewGuid():N}";
|
||||
var entity = new RiskProfileEntity
|
||||
@@ -317,19 +322,20 @@ public static class GovernanceEndpoints
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_created", profileId, "risk_profile",
|
||||
$"Created risk profile: {request.Name}");
|
||||
$"Created risk profile: {request.Name}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.Created($"/api/v1/governance/risk-profiles/{profileId}", MapProfileToResponse(entity)));
|
||||
}
|
||||
|
||||
private static Task<IResult> UpdateRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
string profileId,
|
||||
UpdateRiskProfileRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var existing))
|
||||
{
|
||||
@@ -354,13 +360,14 @@ public static class GovernanceEndpoints
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_updated", profileId, "risk_profile",
|
||||
$"Updated risk profile: {entity.Name}");
|
||||
$"Updated risk profile: {entity.Name}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
|
||||
}
|
||||
|
||||
private static Task<IResult> DeleteRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
string profileId)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
@@ -376,18 +383,19 @@ public static class GovernanceEndpoints
|
||||
}
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_deleted", profileId, "risk_profile",
|
||||
$"Deleted risk profile: {removed.Name}");
|
||||
$"Deleted risk profile: {removed.Name}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.NoContent());
|
||||
}
|
||||
|
||||
private static Task<IResult> ActivateRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
string profileId)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var existing))
|
||||
{
|
||||
@@ -408,19 +416,20 @@ public static class GovernanceEndpoints
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_activated", profileId, "risk_profile",
|
||||
$"Activated risk profile: {entity.Name}");
|
||||
$"Activated risk profile: {entity.Name}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
|
||||
}
|
||||
|
||||
private static Task<IResult> DeprecateRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
string profileId,
|
||||
DeprecateProfileRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var existing))
|
||||
{
|
||||
@@ -442,7 +451,7 @@ public static class GovernanceEndpoints
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_deprecated", profileId, "risk_profile",
|
||||
$"Deprecated risk profile: {entity.Name} - {request.Reason}");
|
||||
$"Deprecated risk profile: {entity.Name} - {request.Reason}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
|
||||
}
|
||||
@@ -582,7 +591,7 @@ public static class GovernanceEndpoints
|
||||
?? httpContext.Request.Headers["X-StellaOps-Actor"].FirstOrDefault();
|
||||
}
|
||||
|
||||
private static void RecordAudit(string tenantId, string actor, string eventType, string targetId, string targetType, string summary)
|
||||
private static void RecordAudit(string tenantId, string actor, string eventType, string targetId, string targetType, string summary, TimeProvider timeProvider)
|
||||
{
|
||||
var id = $"audit-{Guid.NewGuid():N}";
|
||||
AuditEntries[id] = new GovernanceAuditEntry
|
||||
@@ -590,7 +599,7 @@ public static class GovernanceEndpoints
|
||||
Id = id,
|
||||
TenantId = tenantId,
|
||||
Type = eventType,
|
||||
Timestamp = DateTimeOffset.UtcNow.ToString("O"),
|
||||
Timestamp = timeProvider.GetUtcNow().ToString("O"),
|
||||
Actor = actor,
|
||||
ActorType = "user",
|
||||
TargetResource = targetId,
|
||||
|
||||
@@ -50,6 +50,7 @@ internal static class RegistryWebhookEndpoints
|
||||
private static async Task<Results<Accepted<WebhookAcceptedResponse>, ProblemHttpResult>> HandleDockerRegistryWebhook(
|
||||
[FromBody] DockerRegistryNotification notification,
|
||||
IGateEvaluationQueue evaluationQueue,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
ILogger<RegistryWebhookEndpointMarker> logger,
|
||||
CancellationToken ct)
|
||||
{
|
||||
@@ -77,7 +78,7 @@ internal static class RegistryWebhookEndpoints
|
||||
Tag = evt.Target.Tag,
|
||||
RegistryUrl = evt.Request?.Host,
|
||||
Source = "docker-registry",
|
||||
Timestamp = evt.Timestamp ?? DateTimeOffset.UtcNow
|
||||
Timestamp = evt.Timestamp ?? timeProvider.GetUtcNow()
|
||||
}, ct);
|
||||
|
||||
jobs.Add(jobId);
|
||||
@@ -100,6 +101,7 @@ internal static class RegistryWebhookEndpoints
|
||||
private static async Task<Results<Accepted<WebhookAcceptedResponse>, ProblemHttpResult>> HandleHarborWebhook(
|
||||
[FromBody] HarborWebhookEvent notification,
|
||||
IGateEvaluationQueue evaluationQueue,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
ILogger<RegistryWebhookEndpointMarker> logger,
|
||||
CancellationToken ct)
|
||||
{
|
||||
@@ -136,7 +138,7 @@ internal static class RegistryWebhookEndpoints
|
||||
Tag = resource.Tag,
|
||||
RegistryUrl = notification.EventData.Repository?.RepoFullName,
|
||||
Source = "harbor",
|
||||
Timestamp = notification.OccurAt ?? DateTimeOffset.UtcNow
|
||||
Timestamp = notification.OccurAt ?? timeProvider.GetUtcNow()
|
||||
}, ct);
|
||||
|
||||
jobs.Add(jobId);
|
||||
@@ -159,6 +161,7 @@ internal static class RegistryWebhookEndpoints
|
||||
private static async Task<Results<Accepted<WebhookAcceptedResponse>, ProblemHttpResult>> HandleGenericWebhook(
|
||||
[FromBody] GenericRegistryWebhook notification,
|
||||
IGateEvaluationQueue evaluationQueue,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
ILogger<RegistryWebhookEndpointMarker> logger,
|
||||
CancellationToken ct)
|
||||
{
|
||||
@@ -177,7 +180,7 @@ internal static class RegistryWebhookEndpoints
|
||||
RegistryUrl = notification.RegistryUrl,
|
||||
BaselineRef = notification.BaselineRef,
|
||||
Source = notification.Source ?? "generic",
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
Timestamp = timeProvider.GetUtcNow()
|
||||
}, ct);
|
||||
|
||||
logger.LogInformation(
|
||||
|
||||
@@ -21,11 +21,15 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
|
||||
{
|
||||
private readonly Channel<GateEvaluationJob> _channel;
|
||||
private readonly ILogger<InMemoryGateEvaluationQueue> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryGateEvaluationQueue(ILogger<InMemoryGateEvaluationQueue> logger)
|
||||
public InMemoryGateEvaluationQueue(
|
||||
ILogger<InMemoryGateEvaluationQueue> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
|
||||
// Bounded channel to prevent unbounded memory growth
|
||||
_channel = Channel.CreateBounded<GateEvaluationJob>(new BoundedChannelOptions(1000)
|
||||
@@ -46,7 +50,7 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
|
||||
{
|
||||
JobId = jobId,
|
||||
Request = request,
|
||||
QueuedAt = DateTimeOffset.UtcNow
|
||||
QueuedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
await _channel.Writer.WriteAsync(job, cancellationToken).ConfigureAwait(false);
|
||||
@@ -65,10 +69,10 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
|
||||
/// </summary>
|
||||
public ChannelReader<GateEvaluationJob> Reader => _channel.Reader;
|
||||
|
||||
private static string GenerateJobId()
|
||||
private string GenerateJobId()
|
||||
{
|
||||
// Format: gate-{timestamp}-{random}
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
var timestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds();
|
||||
var random = Guid.NewGuid().ToString("N")[..8];
|
||||
return $"gate-{timestamp}-{random}";
|
||||
}
|
||||
|
||||
@@ -9,6 +9,12 @@ namespace StellaOps.Policy.Registry.Storage;
|
||||
public sealed class InMemoryOverrideStore : IOverrideStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid OverrideId), OverrideEntity> _overrides = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryOverrideStore(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<OverrideEntity> CreateAsync(
|
||||
Guid tenantId,
|
||||
@@ -16,7 +22,7 @@ public sealed class InMemoryOverrideStore : IOverrideStore
|
||||
string? createdBy = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var overrideId = Guid.NewGuid();
|
||||
|
||||
var entity = new OverrideEntity
|
||||
@@ -73,7 +79,7 @@ public sealed class InMemoryOverrideStore : IOverrideStore
|
||||
return Task.FromResult<OverrideEntity?>(null);
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var updated = existing with
|
||||
{
|
||||
Status = OverrideStatus.Approved,
|
||||
|
||||
@@ -13,6 +13,12 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid PackId), PolicyPackEntity> _packs = new();
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid PackId), List<PolicyPackHistoryEntry>> _history = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryPolicyPackStore(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<PolicyPackEntity> CreateAsync(
|
||||
Guid tenantId,
|
||||
@@ -20,7 +26,7 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore
|
||||
string? createdBy = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var packId = Guid.NewGuid();
|
||||
|
||||
var entity = new PolicyPackEntity
|
||||
@@ -130,7 +136,7 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore
|
||||
Description = request.Description ?? existing.Description,
|
||||
Rules = request.Rules ?? existing.Rules,
|
||||
Metadata = request.Metadata ?? existing.Metadata,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = _timeProvider.GetUtcNow(),
|
||||
UpdatedBy = updatedBy
|
||||
};
|
||||
|
||||
@@ -178,7 +184,7 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore
|
||||
return Task.FromResult<PolicyPackEntity?>(null);
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var updated = existing with
|
||||
{
|
||||
Status = newStatus,
|
||||
@@ -228,7 +234,7 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore
|
||||
{
|
||||
PackId = packId,
|
||||
Action = action,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Timestamp = _timeProvider.GetUtcNow(),
|
||||
PerformedBy = performedBy,
|
||||
PreviousStatus = previousStatus,
|
||||
NewStatus = newStatus,
|
||||
|
||||
@@ -12,6 +12,12 @@ namespace StellaOps.Policy.Registry.Storage;
|
||||
public sealed class InMemorySnapshotStore : ISnapshotStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid SnapshotId), SnapshotEntity> _snapshots = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemorySnapshotStore(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<SnapshotEntity> CreateAsync(
|
||||
Guid tenantId,
|
||||
@@ -19,7 +25,7 @@ public sealed class InMemorySnapshotStore : ISnapshotStore
|
||||
string? createdBy = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var snapshotId = Guid.NewGuid();
|
||||
|
||||
// Compute digest from pack IDs and timestamp for uniqueness
|
||||
|
||||
@@ -9,6 +9,12 @@ namespace StellaOps.Policy.Registry.Storage;
|
||||
public sealed class InMemoryVerificationPolicyStore : IVerificationPolicyStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, string PolicyId), VerificationPolicyEntity> _policies = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryVerificationPolicyStore(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<VerificationPolicyEntity> CreateAsync(
|
||||
Guid tenantId,
|
||||
@@ -16,7 +22,7 @@ public sealed class InMemoryVerificationPolicyStore : IVerificationPolicyStore
|
||||
string? createdBy = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
var entity = new VerificationPolicyEntity
|
||||
{
|
||||
@@ -102,7 +108,7 @@ public sealed class InMemoryVerificationPolicyStore : IVerificationPolicyStore
|
||||
SignerRequirements = request.SignerRequirements ?? existing.SignerRequirements,
|
||||
ValidityWindow = request.ValidityWindow ?? existing.ValidityWindow,
|
||||
Metadata = request.Metadata ?? existing.Metadata,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = _timeProvider.GetUtcNow(),
|
||||
UpdatedBy = updatedBy
|
||||
};
|
||||
|
||||
|
||||
@@ -9,13 +9,19 @@ namespace StellaOps.Policy.Registry.Storage;
|
||||
public sealed class InMemoryViolationStore : IViolationStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid ViolationId), ViolationEntity> _violations = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryViolationStore(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<ViolationEntity> AppendAsync(
|
||||
Guid tenantId,
|
||||
CreateViolationRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var violationId = Guid.NewGuid();
|
||||
|
||||
var entity = new ViolationEntity
|
||||
@@ -42,7 +48,7 @@ public sealed class InMemoryViolationStore : IViolationStore
|
||||
IReadOnlyList<CreateViolationRequest> requests,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
int created = 0;
|
||||
int failed = 0;
|
||||
var errors = new List<BatchError>();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Policy.Persistence;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Unknowns.Models;
|
||||
@@ -35,7 +36,7 @@ public sealed class UnknownsRepositoryTests : IAsyncLifetime
|
||||
public async Task CreateAndGetById_RoundTripsReasonCodeAndEvidence()
|
||||
{
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(_tenantId.ToString());
|
||||
var repository = new UnknownsRepository(connection);
|
||||
var repository = new UnknownsRepository(connection, TimeProvider.System, SystemGuidProvider.Instance);
|
||||
var now = new DateTimeOffset(2025, 1, 2, 3, 4, 5, TimeSpan.Zero);
|
||||
|
||||
var unknown = CreateUnknown(
|
||||
@@ -65,7 +66,7 @@ public sealed class UnknownsRepositoryTests : IAsyncLifetime
|
||||
public async Task UpdateAsync_PersistsReasonCodeAndAssumptions()
|
||||
{
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(_tenantId.ToString());
|
||||
var repository = new UnknownsRepository(connection);
|
||||
var repository = new UnknownsRepository(connection, TimeProvider.System, SystemGuidProvider.Instance);
|
||||
var now = new DateTimeOffset(2025, 2, 3, 4, 5, 6, TimeSpan.Zero);
|
||||
|
||||
var unknown = CreateUnknown(
|
||||
|
||||
@@ -33,7 +33,7 @@ rules:
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var timeProvider = new FakeTimeProvider();
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
|
||||
await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, null), CancellationToken.None);
|
||||
|
||||
@@ -80,7 +80,7 @@ rules:
|
||||
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
|
||||
|
||||
var findings = ImmutableArray.Create(
|
||||
@@ -111,7 +111,7 @@ rules:
|
||||
{
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
|
||||
|
||||
const string invalid = "version: 1.0";
|
||||
@@ -159,7 +159,7 @@ rules:
|
||||
Assert.False(binding.Document.Rules[0].Metadata.ContainsKey("quiet"));
|
||||
Assert.True(binding.Document.Rules[0].Action.Quiet);
|
||||
|
||||
var store = new PolicySnapshotStore(new InMemoryPolicySnapshotRepository(), new InMemoryPolicyAuditRepository(), TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(new InMemoryPolicySnapshotRepository(), new InMemoryPolicyAuditRepository(), TimeProvider.System, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, "quiet test"), CancellationToken.None);
|
||||
var snapshot = await store.GetLatestAsync();
|
||||
Assert.NotNull(snapshot);
|
||||
|
||||
@@ -25,7 +25,7 @@ rules:
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 10, 0, 0, TimeSpan.Zero));
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
|
||||
var content = new PolicySnapshotContent(BasePolicyYaml, PolicyDocumentFormat.Yaml, "cli", "test", null);
|
||||
|
||||
@@ -56,7 +56,7 @@ rules:
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 10, 0, 0, TimeSpan.Zero));
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
|
||||
var content = new PolicySnapshotContent(BasePolicyYaml, PolicyDocumentFormat.Yaml, "cli", "test", null);
|
||||
var first = await store.SaveAsync(content, CancellationToken.None);
|
||||
@@ -81,7 +81,7 @@ rules:
|
||||
{
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
|
||||
const string invalidYaml = "version: '1.0'\nrules: []";
|
||||
var content = new PolicySnapshotContent(invalidYaml, PolicyDocumentFormat.Yaml, null, null, null);
|
||||
|
||||
@@ -32,6 +32,7 @@ public sealed class ReplayEngineTests
|
||||
_snapshotService,
|
||||
sourceResolver,
|
||||
verdictComparer,
|
||||
null,
|
||||
NullLogger<ReplayEngine>.Instance);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user