docs re-org, audit fixes, build fixes

This commit is contained in:
StellaOps Bot
2026-01-05 09:35:33 +02:00
parent eca4e964d3
commit dfab8a29c3
173 changed files with 1276 additions and 560 deletions

View File

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

View File

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

View File

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

View File

@@ -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(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,6 +32,7 @@ public sealed class ReplayEngineTests
_snapshotService,
sourceResolver,
verdictComparer,
null,
NullLogger<ReplayEngine>.Instance);
}