// // SPDX-License-Identifier: BUSL-1.1 // Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-004) // using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using StellaOps.Policy.Determinization; using System.Security.Claims; namespace StellaOps.Policy.Engine.Endpoints; /// /// API endpoints for determinization configuration. /// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-004) /// public static class DeterminizationConfigEndpoints { /// /// Maps determinization config endpoints. /// public static IEndpointRouteBuilder MapDeterminizationConfigEndpoints(this IEndpointRouteBuilder endpoints) { var group = endpoints.MapGroup("/api/v1/policy/config/determinization") .WithTags("Determinization Configuration"); // Read endpoints (policy viewer access) group.MapGet("", GetEffectiveConfig) .WithName("GetEffectiveDeterminizationConfig") .WithSummary("Get effective determinization configuration for the current tenant") .Produces(StatusCodes.Status200OK) .RequireAuthorization("PolicyViewer"); group.MapGet("/defaults", GetDefaultConfig) .WithName("GetDefaultDeterminizationConfig") .WithSummary("Get default determinization configuration") .Produces(StatusCodes.Status200OK) .RequireAuthorization("PolicyViewer"); group.MapGet("/audit", GetAuditHistory) .WithName("GetDeterminizationConfigAuditHistory") .WithSummary("Get audit history for determinization configuration changes") .Produces(StatusCodes.Status200OK) .RequireAuthorization("PolicyViewer"); // Write endpoints (policy admin access) group.MapPut("", UpdateConfig) .WithName("UpdateDeterminizationConfig") .WithSummary("Update determinization configuration for the current tenant") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) .RequireAuthorization("PolicyAdmin"); group.MapPost("/validate", ValidateConfig) .WithName("ValidateDeterminizationConfig") .WithSummary("Validate determinization configuration without saving") .Produces(StatusCodes.Status200OK) .RequireAuthorization("PolicyViewer"); return endpoints; } private static async Task GetEffectiveConfig( HttpContext context, IDeterminizationConfigStore configStore, ILogger logger, CancellationToken ct) { var tenantId = GetTenantId(context); logger.LogDebug("Getting effective determinization config for tenant {TenantId}", tenantId); var config = await configStore.GetEffectiveConfigAsync(tenantId, ct); return Results.Ok(new EffectiveConfigResponse { Config = config.Config, IsDefault = config.IsDefault, TenantId = config.TenantId, LastUpdatedAt = config.LastUpdatedAt, LastUpdatedBy = config.LastUpdatedBy, Version = config.Version }); } private static IResult GetDefaultConfig( ILogger logger) { logger.LogDebug("Getting default determinization config"); return Results.Ok(new DeterminizationOptions()); } private static async Task GetAuditHistory( HttpContext context, IDeterminizationConfigStore configStore, ILogger logger, int limit = 50, CancellationToken ct = default) { var tenantId = GetTenantId(context); logger.LogDebug("Getting audit history for tenant {TenantId}", tenantId); var entries = await configStore.GetAuditHistoryAsync(tenantId, limit, ct); return Results.Ok(new AuditHistoryResponse { Entries = entries.Select(e => new AuditEntryDto { Id = e.Id, ChangedAt = e.ChangedAt, Actor = e.Actor, Reason = e.Reason, Source = e.Source, Summary = e.Summary }).ToList() }); } private static async Task UpdateConfig( HttpContext context, IDeterminizationConfigStore configStore, ILogger logger, UpdateConfigRequest request, CancellationToken ct) { var tenantId = GetTenantId(context); var actor = GetActorId(context); logger.LogInformation( "Updating determinization config for tenant {TenantId} by {Actor}: {Reason}", tenantId, actor, request.Reason); // Validate config var validation = ValidateConfigInternal(request.Config); if (!validation.IsValid) { return Results.BadRequest(new { errors = validation.Errors }); } // Save with audit await configStore.SaveConfigAsync( tenantId, request.Config, new ConfigAuditInfo { Actor = actor, Reason = request.Reason, Source = "API", CorrelationId = context.TraceIdentifier }, ct); // Return updated config var updated = await configStore.GetEffectiveConfigAsync(tenantId, ct); return Results.Ok(new EffectiveConfigResponse { Config = updated.Config, IsDefault = updated.IsDefault, TenantId = updated.TenantId, LastUpdatedAt = updated.LastUpdatedAt, LastUpdatedBy = updated.LastUpdatedBy, Version = updated.Version }); } private static IResult ValidateConfig( ValidateConfigRequest request, ILogger logger) { logger.LogDebug("Validating determinization config"); var validation = ValidateConfigInternal(request.Config); return Results.Ok(new ValidationResponse { IsValid = validation.IsValid, Errors = validation.Errors, Warnings = validation.Warnings }); } private static (bool IsValid, List Errors, List Warnings) ValidateConfigInternal( DeterminizationOptions config) { var errors = new List(); var warnings = new List(); // Validate trigger config if (config.Triggers.EpssDeltaThreshold < 0 || config.Triggers.EpssDeltaThreshold > 1) { errors.Add("EpssDeltaThreshold must be between 0 and 1"); } if (config.Triggers.EpssDeltaThreshold < 0.1) { warnings.Add("EpssDeltaThreshold below 0.1 may cause excessive reanalysis"); } // Validate conflict policy if (config.ConflictPolicy.EscalationSeverityThreshold < 0 || config.ConflictPolicy.EscalationSeverityThreshold > 1) { errors.Add("EscalationSeverityThreshold must be between 0 and 1"); } if (config.ConflictPolicy.ConflictTtlHours < 1) { errors.Add("ConflictTtlHours must be at least 1"); } // Validate environment thresholds ValidateThresholds(config.EnvironmentThresholds.Development, "Development", errors, warnings); ValidateThresholds(config.EnvironmentThresholds.Staging, "Staging", errors, warnings); ValidateThresholds(config.EnvironmentThresholds.Production, "Production", errors, warnings); return (errors.Count == 0, errors, warnings); } private static void ValidateThresholds( EnvironmentThresholdValues threshold, string envName, List errors, List warnings) { if (threshold.MaxPassEntropy < 0 || threshold.MaxPassEntropy > 1) { errors.Add($"{envName}.MaxPassEntropy must be between 0 and 1"); } if (threshold.MinEvidenceCount < 0) { errors.Add($"{envName}.MinEvidenceCount must be >= 0"); } if (threshold.MaxPassEntropy > 0.8) { warnings.Add($"{envName}.MaxPassEntropy above 0.8 may reduce confidence controls"); } } private static string GetTenantId(HttpContext context) { return context.User.FindFirstValue("tenant_id") ?? "default"; } private static string GetActorId(HttpContext context) { return context.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? context.User.FindFirstValue("sub") ?? "system"; } } // DTOs /// Effective config response. public sealed record EffectiveConfigResponse { public required DeterminizationOptions Config { get; init; } public required bool IsDefault { get; init; } public string? TenantId { get; init; } public DateTimeOffset? LastUpdatedAt { get; init; } public string? LastUpdatedBy { get; init; } public int Version { get; init; } } /// Update config request. public sealed record UpdateConfigRequest { public required DeterminizationOptions Config { get; init; } public required string Reason { get; init; } } /// Validate config request. public sealed record ValidateConfigRequest { public required DeterminizationOptions Config { get; init; } } /// Validation response. public sealed record ValidationResponse { public required bool IsValid { get; init; } public required List Errors { get; init; } public required List Warnings { get; init; } } /// Audit history response. public sealed record AuditHistoryResponse { public required List Entries { get; init; } } /// Audit entry DTO. public sealed record AuditEntryDto { public required Guid Id { get; init; } public required DateTimeOffset ChangedAt { get; init; } public required string Actor { get; init; } public required string Reason { get; init; } public string? Source { get; init; } public string? Summary { get; init; } }