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

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