Merge remote changes (theirs)
This commit is contained in:
@@ -2,12 +2,10 @@
|
||||
// Sprint: SPRINT_20251226_003_BE_exception_approval
|
||||
// Task: EXCEPT-05, EXCEPT-06, EXCEPT-07 - Exception approval API endpoints
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
@@ -91,8 +89,7 @@ public static class ExceptionApprovalEndpoints
|
||||
CreateApprovalRequestDto request,
|
||||
IExceptionApprovalRepository repository,
|
||||
IExceptionApprovalRulesService rulesService,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
ILogger<ExceptionApprovalRequestEntity> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -114,8 +111,7 @@ public static class ExceptionApprovalEndpoints
|
||||
}
|
||||
|
||||
// Generate request ID
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var requestId = $"EAR-{now.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{guidProvider.NewGuid().ToString("N", CultureInfo.InvariantCulture)[..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))
|
||||
@@ -144,9 +140,10 @@ public static class ExceptionApprovalEndpoints
|
||||
});
|
||||
}
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var entity = new ExceptionApprovalRequestEntity
|
||||
{
|
||||
Id = guidProvider.NewGuid(),
|
||||
Id = Guid.NewGuid(),
|
||||
RequestId = requestId,
|
||||
TenantId = tenantId,
|
||||
ExceptionId = request.ExceptionId,
|
||||
@@ -208,7 +205,7 @@ public static class ExceptionApprovalEndpoints
|
||||
// Record audit entry
|
||||
await repository.RecordAuditAsync(new ExceptionApprovalAuditEntity
|
||||
{
|
||||
Id = guidProvider.NewGuid(),
|
||||
Id = Guid.NewGuid(),
|
||||
RequestId = requestId,
|
||||
TenantId = tenantId,
|
||||
SequenceNumber = 1,
|
||||
|
||||
@@ -8,7 +8,6 @@ using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Exceptions.Models;
|
||||
using StellaOps.Policy.Exceptions.Repositories;
|
||||
using StellaOps.Policy.Gateway.Contracts;
|
||||
@@ -135,8 +134,7 @@ public static class ExceptionEndpoints
|
||||
CreateExceptionRequest request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (request is null)
|
||||
@@ -148,10 +146,8 @@ public static class ExceptionEndpoints
|
||||
});
|
||||
}
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
// Validate expiry is in future
|
||||
if (request.ExpiresAt <= now)
|
||||
if (request.ExpiresAt <= timeProvider.GetUtcNow())
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
@@ -162,7 +158,7 @@ public static class ExceptionEndpoints
|
||||
}
|
||||
|
||||
// Validate expiry is not more than 1 year
|
||||
if (request.ExpiresAt > now.AddYears(1))
|
||||
if (request.ExpiresAt > timeProvider.GetUtcNow().AddYears(1))
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
@@ -175,7 +171,7 @@ public static class ExceptionEndpoints
|
||||
var actorId = GetActorId(context);
|
||||
var clientInfo = GetClientInfo(context);
|
||||
|
||||
var exceptionId = $"EXC-{guidProvider.NewGuid():N}"[..20];
|
||||
var exceptionId = $"EXC-{Guid.NewGuid():N}"[..20];
|
||||
|
||||
var exception = new ExceptionObject
|
||||
{
|
||||
@@ -193,8 +189,8 @@ public static class ExceptionEndpoints
|
||||
},
|
||||
OwnerId = request.OwnerId,
|
||||
RequesterId = actorId,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
CreatedAt = timeProvider.GetUtcNow(),
|
||||
UpdatedAt = timeProvider.GetUtcNow(),
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
ReasonCode = ParseReasonRequired(request.ReasonCode),
|
||||
Rationale = request.Rationale,
|
||||
@@ -215,7 +211,7 @@ public static class ExceptionEndpoints
|
||||
UpdateExceptionRequest request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -264,7 +260,7 @@ public static class ExceptionEndpoints
|
||||
ApproveExceptionRequest? request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -297,13 +293,12 @@ public static class ExceptionEndpoints
|
||||
});
|
||||
}
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var updated = existing with
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
Status = ExceptionStatus.Approved,
|
||||
UpdatedAt = now,
|
||||
ApprovedAt = now,
|
||||
UpdatedAt = timeProvider.GetUtcNow(),
|
||||
ApprovedAt = timeProvider.GetUtcNow(),
|
||||
ApproverIds = existing.ApproverIds.Add(actorId)
|
||||
};
|
||||
|
||||
@@ -318,7 +313,7 @@ public static class ExceptionEndpoints
|
||||
string id,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -359,7 +354,7 @@ public static class ExceptionEndpoints
|
||||
ExtendExceptionRequest request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -410,7 +405,7 @@ public static class ExceptionEndpoints
|
||||
[FromBody] RevokeExceptionRequest? request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
// Sprint: SPRINT_20251226_001_BE_cicd_gate_integration
|
||||
// Task: CICD-GATE-01 - Create POST /api/v1/policy/gate/evaluate endpoint
|
||||
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Audit;
|
||||
using StellaOps.Policy.Deltas;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
@@ -41,8 +39,7 @@ public static class GateEndpoints
|
||||
IBaselineSelector baselineSelector,
|
||||
IGateBypassAuditor bypassAuditor,
|
||||
IMemoryCache cache,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
ILogger<DriftGateEvaluator> logger,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
@@ -83,7 +80,7 @@ public static class GateEndpoints
|
||||
|
||||
return Results.Ok(new GateEvaluateResponse
|
||||
{
|
||||
DecisionId = $"gate:{timeProvider.GetUtcNow().ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture)}:{guidProvider.NewGuid():N}",
|
||||
DecisionId = $"gate:{timeProvider.GetUtcNow():yyyyMMddHHmmss}:{Guid.NewGuid():N}",
|
||||
Status = GateStatus.Pass,
|
||||
ExitCode = GateExitCodes.Pass,
|
||||
ImageDigest = request.ImageDigest,
|
||||
@@ -228,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", (TimeProvider timeProvider) => Results.Ok(new { status = "healthy", timestamp = timeProvider.GetUtcNow() }))
|
||||
gates.MapGet("/health", ([FromServices] TimeProvider timeProvider) =>
|
||||
Results.Ok(new { status = "healthy", timestamp = timeProvider.GetUtcNow() }))
|
||||
.WithName("GateHealth")
|
||||
.WithDescription("Health check for the gate evaluation service");
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
// Task: GOV-018 - Sealed mode overrides and risk profile events endpoints
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
|
||||
namespace StellaOps.Policy.Gateway.Endpoints;
|
||||
|
||||
@@ -102,11 +101,11 @@ public static class GovernanceEndpoints
|
||||
|
||||
private static Task<IResult> GetSealedModeStatusAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
[FromQuery] string? tenantId)
|
||||
{
|
||||
var tenant = tenantId ?? GetTenantId(httpContext) ?? "default";
|
||||
var state = SealedModeStates.GetOrAdd(tenant, _ => new SealedModeState());
|
||||
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
|
||||
var response = new SealedModeStatusResponse
|
||||
{
|
||||
@@ -121,7 +120,7 @@ public static class GovernanceEndpoints
|
||||
.Select(MapOverrideToResponse)
|
||||
.ToList(),
|
||||
VerificationStatus = "verified",
|
||||
LastVerifiedAt = timeProvider.GetUtcNow().ToString("O")
|
||||
LastVerifiedAt = timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
@@ -143,20 +142,21 @@ 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 timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
var state = SealedModeStates.GetOrAdd(tenant, _ => new SealedModeState());
|
||||
|
||||
if (request.Enable)
|
||||
{
|
||||
state = new SealedModeState
|
||||
{
|
||||
IsSealed = true,
|
||||
SealedAt = now.ToString("O"),
|
||||
SealedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
SealedBy = actor,
|
||||
Reason = request.Reason,
|
||||
TrustRoots = request.TrustRoots ?? [],
|
||||
@@ -168,7 +168,7 @@ public static class GovernanceEndpoints
|
||||
state = new SealedModeState
|
||||
{
|
||||
IsSealed = false,
|
||||
LastUnsealedAt = now.ToString("O")
|
||||
LastUnsealedAt = now.ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ public static class GovernanceEndpoints
|
||||
|
||||
// Audit
|
||||
RecordAudit(tenant, actor, "sealed_mode_toggled", "sealed-mode", "system_config",
|
||||
$"{(request.Enable ? "Enabled" : "Disabled")} sealed mode: {request.Reason}", timeProvider, guidProvider);
|
||||
$"{(request.Enable ? "Enabled" : "Disabled")} sealed mode: {request.Reason}", timeProvider);
|
||||
|
||||
var response = new SealedModeStatusResponse
|
||||
{
|
||||
@@ -188,7 +188,7 @@ public static class GovernanceEndpoints
|
||||
AllowedSources = state.AllowedSources,
|
||||
Overrides = [],
|
||||
VerificationStatus = "verified",
|
||||
LastVerifiedAt = now.ToString("O")
|
||||
LastVerifiedAt = now.ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
@@ -196,15 +196,14 @@ 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 timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
var overrideId = $"override-{guidProvider.NewGuid():N}";
|
||||
var overrideId = $"override-{Guid.NewGuid():N}";
|
||||
var entity = new SealedModeOverrideEntity
|
||||
{
|
||||
Id = overrideId,
|
||||
@@ -212,30 +211,29 @@ public static class GovernanceEndpoints
|
||||
Type = request.Type,
|
||||
Target = request.Target,
|
||||
Reason = request.Reason,
|
||||
ApprovalId = $"approval-{guidProvider.NewGuid():N}",
|
||||
ApprovalId = $"approval-{Guid.NewGuid():N}",
|
||||
ApprovedBy = [actor],
|
||||
ExpiresAt = now.AddHours(request.DurationHours).ToString("O"),
|
||||
CreatedAt = now.ToString("O"),
|
||||
ExpiresAt = now.AddHours(request.DurationHours).ToString("O", CultureInfo.InvariantCulture),
|
||||
CreatedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
Active = true
|
||||
};
|
||||
|
||||
Overrides[overrideId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "sealed_mode_override_created", overrideId, "sealed_mode_override",
|
||||
$"Created override for {request.Target}: {request.Reason}", timeProvider, guidProvider);
|
||||
$"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)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
|
||||
if (!Overrides.TryGetValue(overrideId, out var entity) || entity.TenantId != tenant)
|
||||
{
|
||||
@@ -250,7 +248,7 @@ public static class GovernanceEndpoints
|
||||
Overrides[overrideId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "sealed_mode_override_revoked", overrideId, "sealed_mode_override",
|
||||
$"Revoked override: {request.Reason}", timeProvider, guidProvider);
|
||||
$"Revoked override: {request.Reason}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.NoContent());
|
||||
}
|
||||
@@ -296,15 +294,14 @@ 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 timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
var profileId = $"profile-{guidProvider.NewGuid():N}";
|
||||
var profileId = $"profile-{Guid.NewGuid():N}";
|
||||
var entity = new RiskProfileEntity
|
||||
{
|
||||
Id = profileId,
|
||||
@@ -317,8 +314,8 @@ public static class GovernanceEndpoints
|
||||
Signals = request.Signals ?? [],
|
||||
SeverityOverrides = request.SeverityOverrides ?? [],
|
||||
ActionOverrides = request.ActionOverrides ?? [],
|
||||
CreatedAt = now.ToString("O"),
|
||||
ModifiedAt = now.ToString("O"),
|
||||
CreatedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
ModifiedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
CreatedBy = actor,
|
||||
ModifiedBy = actor
|
||||
};
|
||||
@@ -326,20 +323,19 @@ public static class GovernanceEndpoints
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_created", profileId, "risk_profile",
|
||||
$"Created risk profile: {request.Name}", timeProvider, guidProvider);
|
||||
$"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 timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var existing))
|
||||
@@ -358,26 +354,25 @@ public static class GovernanceEndpoints
|
||||
Signals = request.Signals ?? existing.Signals,
|
||||
SeverityOverrides = request.SeverityOverrides ?? existing.SeverityOverrides,
|
||||
ActionOverrides = request.ActionOverrides ?? existing.ActionOverrides,
|
||||
ModifiedAt = now.ToString("O"),
|
||||
ModifiedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
ModifiedBy = actor
|
||||
};
|
||||
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_updated", profileId, "risk_profile",
|
||||
$"Updated risk profile: {entity.Name}", timeProvider, guidProvider);
|
||||
$"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";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
|
||||
if (!RiskProfiles.TryRemove(profileId, out var removed))
|
||||
{
|
||||
@@ -389,19 +384,18 @@ public static class GovernanceEndpoints
|
||||
}
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_deleted", profileId, "risk_profile",
|
||||
$"Deleted risk profile: {removed.Name}", timeProvider, guidProvider);
|
||||
$"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 timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var existing))
|
||||
@@ -416,27 +410,26 @@ public static class GovernanceEndpoints
|
||||
var entity = existing with
|
||||
{
|
||||
Status = "active",
|
||||
ModifiedAt = now.ToString("O"),
|
||||
ModifiedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
ModifiedBy = actor
|
||||
};
|
||||
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_activated", profileId, "risk_profile",
|
||||
$"Activated risk profile: {entity.Name}", timeProvider, guidProvider);
|
||||
$"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 timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var existing))
|
||||
@@ -451,7 +444,7 @@ public static class GovernanceEndpoints
|
||||
var entity = existing with
|
||||
{
|
||||
Status = "deprecated",
|
||||
ModifiedAt = now.ToString("O"),
|
||||
ModifiedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
ModifiedBy = actor,
|
||||
DeprecationReason = request.Reason
|
||||
};
|
||||
@@ -459,7 +452,7 @@ public static class GovernanceEndpoints
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_deprecated", profileId, "risk_profile",
|
||||
$"Deprecated risk profile: {entity.Name} - {request.Reason}", timeProvider, guidProvider);
|
||||
$"Deprecated risk profile: {entity.Name} - {request.Reason}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
|
||||
}
|
||||
@@ -559,7 +552,7 @@ public static class GovernanceEndpoints
|
||||
{
|
||||
if (RiskProfiles.IsEmpty)
|
||||
{
|
||||
var now = TimeProvider.System.GetUtcNow().ToString("O", System.Globalization.CultureInfo.InvariantCulture);
|
||||
var now = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture);
|
||||
RiskProfiles["profile-default"] = new RiskProfileEntity
|
||||
{
|
||||
Id = "profile-default",
|
||||
@@ -599,15 +592,15 @@ 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, TimeProvider timeProvider, IGuidProvider guidProvider)
|
||||
private static void RecordAudit(string tenantId, string actor, string eventType, string targetId, string targetType, string summary, TimeProvider timeProvider)
|
||||
{
|
||||
var id = $"audit-{guidProvider.NewGuid():N}";
|
||||
var id = $"audit-{Guid.NewGuid():N}";
|
||||
AuditEntries[id] = new GovernanceAuditEntry
|
||||
{
|
||||
Id = id,
|
||||
TenantId = tenantId,
|
||||
Type = eventType,
|
||||
Timestamp = timeProvider.GetUtcNow().ToString("O", System.Globalization.CultureInfo.InvariantCulture),
|
||||
Timestamp = timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture),
|
||||
Actor = actor,
|
||||
ActorType = "user",
|
||||
TargetResource = targetId,
|
||||
|
||||
@@ -50,7 +50,7 @@ internal static class RegistryWebhookEndpoints
|
||||
private static async Task<Results<Accepted<WebhookAcceptedResponse>, ProblemHttpResult>> HandleDockerRegistryWebhook(
|
||||
[FromBody] DockerRegistryNotification notification,
|
||||
IGateEvaluationQueue evaluationQueue,
|
||||
TimeProvider timeProvider,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
ILogger<RegistryWebhookEndpointMarker> logger,
|
||||
CancellationToken ct)
|
||||
{
|
||||
@@ -101,7 +101,7 @@ internal static class RegistryWebhookEndpoints
|
||||
private static async Task<Results<Accepted<WebhookAcceptedResponse>, ProblemHttpResult>> HandleHarborWebhook(
|
||||
[FromBody] HarborWebhookEvent notification,
|
||||
IGateEvaluationQueue evaluationQueue,
|
||||
TimeProvider timeProvider,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
ILogger<RegistryWebhookEndpointMarker> logger,
|
||||
CancellationToken ct)
|
||||
{
|
||||
@@ -161,7 +161,7 @@ internal static class RegistryWebhookEndpoints
|
||||
private static async Task<Results<Accepted<WebhookAcceptedResponse>, ProblemHttpResult>> HandleGenericWebhook(
|
||||
[FromBody] GenericRegistryWebhook notification,
|
||||
IGateEvaluationQueue evaluationQueue,
|
||||
TimeProvider timeProvider,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
ILogger<RegistryWebhookEndpointMarker> logger,
|
||||
CancellationToken ct)
|
||||
{
|
||||
|
||||
@@ -5,11 +5,9 @@
|
||||
// Description: In-memory queue for gate evaluation jobs with background processing
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Globalization;
|
||||
using System.Threading.Channels;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Gateway.Endpoints;
|
||||
|
||||
@@ -24,14 +22,14 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
|
||||
private readonly Channel<GateEvaluationJob> _channel;
|
||||
private readonly ILogger<InMemoryGateEvaluationQueue> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public InMemoryGateEvaluationQueue(ILogger<InMemoryGateEvaluationQueue> logger, TimeProvider timeProvider, IGuidProvider guidProvider)
|
||||
public InMemoryGateEvaluationQueue(
|
||||
ILogger<InMemoryGateEvaluationQueue> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider;
|
||||
_guidProvider = guidProvider;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
|
||||
// Bounded channel to prevent unbounded memory growth
|
||||
_channel = Channel.CreateBounded<GateEvaluationJob>(new BoundedChannelOptions(1000)
|
||||
@@ -74,8 +72,8 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
|
||||
private string GenerateJobId()
|
||||
{
|
||||
// Format: gate-{timestamp}-{random}
|
||||
var timestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture);
|
||||
var random = _guidProvider.NewGuid().ToString("N", CultureInfo.InvariantCulture)[..8];
|
||||
var timestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds();
|
||||
var random = Guid.NewGuid().ToString("N")[..8];
|
||||
return $"gate-{timestamp}-{random}";
|
||||
}
|
||||
}
|
||||
@@ -92,7 +90,7 @@ public sealed record GateEvaluationJob
|
||||
|
||||
/// <summary>
|
||||
/// Background service that processes gate evaluation jobs from the queue.
|
||||
/// Orchestrates: image analysis → drift delta computation → gate evaluation.
|
||||
/// Orchestrates: image analysis -> drift delta computation -> gate evaluation.
|
||||
/// </summary>
|
||||
public sealed class GateEvaluationWorker : BackgroundService
|
||||
{
|
||||
|
||||
@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0445-M | DONE | Maintainability audit for StellaOps.Policy.Gateway. |
|
||||
| AUDIT-0445-T | DONE | Test coverage audit for StellaOps.Policy.Gateway. |
|
||||
| AUDIT-0445-A | TODO | APPLY pending approval for StellaOps.Policy.Gateway. |
|
||||
| AUDIT-0445-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Policy.Gateway. |
|
||||
| AUDIT-0445-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Policy.Gateway. |
|
||||
| AUDIT-0445-A | TODO | Revalidated 2026-01-07 (open findings). |
|
||||
|
||||
Reference in New Issue
Block a user