using System.Collections.Concurrent; using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Policy.Gateway.Endpoints; public static class GovernanceCompatibilityEndpoints { private static readonly ConcurrentDictionary TrustWeightStates = new(StringComparer.OrdinalIgnoreCase); private static readonly ConcurrentDictionary StalenessStates = new(StringComparer.OrdinalIgnoreCase); public static void MapGovernanceCompatibilityEndpoints(this WebApplication app) { var governance = app.MapGroup("/api/v1/governance") .WithTags("Governance Compatibility") .RequireTenant(); governance.MapGet("/trust-weights", ( HttpContext context, [FromQuery] string? projectId, TimeProvider timeProvider) => { var scope = ResolveScope(context, projectId); var state = TrustWeightStates.GetOrAdd(scope.Key, _ => CreateDefaultTrustWeightState(scope.TenantId, scope.ProjectId, timeProvider)); return Results.Ok(state.ToResponse()); }).RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead)); governance.MapPut("/trust-weights/{weightId}", ( HttpContext context, string weightId, [FromBody] TrustWeightWriteModel request, [FromQuery] string? projectId, TimeProvider timeProvider) => { var scope = ResolveScope(context, projectId); var state = TrustWeightStates.GetOrAdd(scope.Key, _ => CreateDefaultTrustWeightState(scope.TenantId, scope.ProjectId, timeProvider)); var updated = state.Upsert(weightId, request, timeProvider, StellaOpsTenantResolver.ResolveActor(context)); return Results.Ok(updated); }).RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor)); governance.MapDelete("/trust-weights/{weightId}", ( HttpContext context, string weightId, [FromQuery] string? projectId, TimeProvider timeProvider) => { var scope = ResolveScope(context, projectId); var state = TrustWeightStates.GetOrAdd(scope.Key, _ => CreateDefaultTrustWeightState(scope.TenantId, scope.ProjectId, timeProvider)); state.Delete(weightId, timeProvider); return Results.NoContent(); }).RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor)); governance.MapPost("/trust-weights/preview-impact", ( [FromBody] TrustWeightPreviewRequest request) => { var weights = request.Weights ?? []; var severityChanges = weights.Count(static weight => weight.Weight >= 1.2m); var decisionChanges = weights.Count(static weight => weight.Active != false); var payload = new { affectedVulnerabilities = Math.Max(3, weights.Count * 9), severityChanges, decisionChanges, sampleAffected = weights.Take(3).Select((weight, index) => new { findingId = $"finding-{index + 1:000}", componentPurl = $"pkg:oci/{weight.IssuerId ?? "stellaops"}/runtime-{index + 1}@sha256:{(index + 1).ToString("D4")}", advisoryId = $"CVE-2026-{1400 + index}", currentSeverity = index == 0 ? "high" : "medium", projectedSeverity = weight.Weight >= 1.2m ? "critical" : "high", currentDecision = "warn", projectedDecision = weight.Active != false ? "deny" : "warn" }).ToArray(), severityTransitions = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["medium->high"] = Math.Max(1, severityChanges), ["high->critical"] = Math.Max(1, severityChanges / 2) } }; return Results.Ok(payload); }).RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor)); governance.MapGet("/staleness/config", ( HttpContext context, [FromQuery] string? projectId, TimeProvider timeProvider) => { var scope = ResolveScope(context, projectId); var state = StalenessStates.GetOrAdd(scope.Key, _ => CreateDefaultStalenessState(scope.TenantId, scope.ProjectId, timeProvider)); return Results.Ok(state.ToResponse()); }).RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead)); governance.MapPut("/staleness/config/{dataType}", ( HttpContext context, string dataType, [FromBody] StalenessConfigWriteModel request, [FromQuery] string? projectId, TimeProvider timeProvider) => { var scope = ResolveScope(context, projectId); var state = StalenessStates.GetOrAdd(scope.Key, _ => CreateDefaultStalenessState(scope.TenantId, scope.ProjectId, timeProvider)); var updated = state.Upsert(dataType, request, timeProvider); return Results.Ok(updated); }).RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor)); governance.MapGet("/staleness/status", ( HttpContext context, [FromQuery] string? projectId, TimeProvider timeProvider) => { var scope = ResolveScope(context, projectId); var state = StalenessStates.GetOrAdd(scope.Key, _ => CreateDefaultStalenessState(scope.TenantId, scope.ProjectId, timeProvider)); return Results.Ok(state.BuildStatus()); }).RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead)); } private static GovernanceScope ResolveScope(HttpContext context, string? projectId) { if (!StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error)) { throw new InvalidOperationException($"Tenant resolution failed: {error ?? "tenant_missing"}"); } var scopedProject = string.IsNullOrWhiteSpace(projectId) ? StellaOpsTenantResolver.ResolveProject(context) : projectId.Trim(); return new GovernanceScope( tenantId, string.IsNullOrWhiteSpace(scopedProject) ? null : scopedProject, string.IsNullOrWhiteSpace(scopedProject) ? tenantId : $"{tenantId}:{scopedProject}"); } private static TrustWeightConfigState CreateDefaultTrustWeightState(string tenantId, string? projectId, TimeProvider timeProvider) { var now = timeProvider.GetUtcNow().ToString("O"); return new TrustWeightConfigState( tenantId, projectId, now, "\"trust-weights-v1\"", [ new TrustWeightRecord("tw-001", "cisa", "CISA", "cisa", 1.50m, 1, true, "Government authoritative source", now, "system"), new TrustWeightRecord("tw-002", "nist", "NIST NVD", "nist", 1.30m, 2, true, "Primary CVE source", now, "system"), new TrustWeightRecord("tw-003", "vendor-redhat", "Red Hat", "vendor", 1.20m, 3, true, "Trusted vendor feed", now, "system") ]); } private static StalenessConfigState CreateDefaultStalenessState(string tenantId, string? projectId, TimeProvider timeProvider) { var now = timeProvider.GetUtcNow().ToString("O"); return new StalenessConfigState( tenantId, projectId, now, "\"staleness-v1\"", [ new StalenessConfigRecord("sbom", BuildThresholds(7, 14, 30, 45), true, 12), new StalenessConfigRecord("vulnerability_data", BuildThresholds(1, 3, 7, 14), true, 6), new StalenessConfigRecord("vex_statements", BuildThresholds(3, 7, 14, 21), true, 12), new StalenessConfigRecord("policy", BuildThresholds(14, 30, 45, 60), false, 24), new StalenessConfigRecord("attestation", BuildThresholds(7, 14, 21, 30), true, 8), new StalenessConfigRecord("scan_result", BuildThresholds(1, 2, 5, 10), true, 4) ]); } private static List BuildThresholds(int fresh, int aging, int stale, int expired) => [ new("fresh", fresh, "low", [new StalenessActionRecord("warn", "Still within freshness SLA.")]), new("aging", aging, "medium", [new StalenessActionRecord("notify", "Approaching review window.")]), new("stale", stale, "high", [new StalenessActionRecord("flag_review", "Operator review required.")]), new("expired", expired, "critical", [new StalenessActionRecord("block", "Fresh data required before continue.")]) ]; private sealed record GovernanceScope(string TenantId, string? ProjectId, string Key); private sealed class TrustWeightConfigState( string tenantId, string? projectId, string modifiedAt, string etag, List weights) { public string TenantId { get; private set; } = tenantId; public string? ProjectId { get; private set; } = projectId; public string ModifiedAt { get; private set; } = modifiedAt; public string Etag { get; private set; } = etag; public List Weights { get; } = weights; public object ToResponse() => new { tenantId = TenantId, projectId = ProjectId, weights = Weights.OrderBy(static weight => weight.Priority).ThenBy(static weight => weight.IssuerName, StringComparer.OrdinalIgnoreCase), defaultWeight = 1.0m, modifiedAt = ModifiedAt, etag = Etag }; public object Upsert(string routeWeightId, TrustWeightWriteModel request, TimeProvider timeProvider, string actor) { var effectiveId = string.IsNullOrWhiteSpace(request.Id) ? routeWeightId : request.Id.Trim(); var index = Weights.FindIndex(weight => string.Equals(weight.Id, effectiveId, StringComparison.OrdinalIgnoreCase)); var existing = index >= 0 ? Weights[index] : null; var now = timeProvider.GetUtcNow().ToString("O"); var updated = new TrustWeightRecord( effectiveId, request.IssuerId?.Trim() ?? existing?.IssuerId ?? effectiveId, request.IssuerName?.Trim() ?? existing?.IssuerName ?? effectiveId, NormalizeSource(request.Source ?? existing?.Source), request.Weight ?? existing?.Weight ?? 1.0m, request.Priority ?? existing?.Priority ?? Weights.Count + 1, request.Active ?? existing?.Active ?? true, request.Reason?.Trim() ?? existing?.Reason, now, actor); if (index >= 0) { Weights[index] = updated; } else { Weights.Add(updated); } ModifiedAt = now; Etag = $"\"trust-weights-{Weights.Count}-{now}\""; return updated; } public void Delete(string weightId, TimeProvider timeProvider) { Weights.RemoveAll(weight => string.Equals(weight.Id, weightId, StringComparison.OrdinalIgnoreCase)); ModifiedAt = timeProvider.GetUtcNow().ToString("O"); Etag = $"\"trust-weights-{Weights.Count}-{ModifiedAt}\""; } } private sealed class StalenessConfigState( string tenantId, string? projectId, string modifiedAt, string etag, List configs) { public string TenantId { get; private set; } = tenantId; public string? ProjectId { get; private set; } = projectId; public string ModifiedAt { get; private set; } = modifiedAt; public string Etag { get; private set; } = etag; public List Configs { get; } = configs; public object ToResponse() => new { tenantId = TenantId, projectId = ProjectId, configs = Configs.OrderBy(static config => config.DataType, StringComparer.OrdinalIgnoreCase), modifiedAt = ModifiedAt, etag = Etag }; public object Upsert(string dataType, StalenessConfigWriteModel request, TimeProvider timeProvider) { var effectiveType = string.IsNullOrWhiteSpace(dataType) ? request.DataType?.Trim() ?? "sbom" : dataType.Trim(); var thresholds = request.Thresholds?.Count > 0 ? request.Thresholds.Select(threshold => new StalenessThresholdRecord( NormalizeLevel(threshold.Level), threshold.AgeDays, NormalizeSeverity(threshold.Severity), threshold.Actions?.Select(action => new StalenessActionRecord(NormalizeActionType(action.Type), action.Message, action.Channels)).ToList() ?? [])).ToList() : BuildThresholds(7, 14, 30, 45); var updated = new StalenessConfigRecord( effectiveType, thresholds, request.Enabled ?? true, request.GracePeriodHours ?? 12); var index = Configs.FindIndex(config => string.Equals(config.DataType, effectiveType, StringComparison.OrdinalIgnoreCase)); if (index >= 0) { Configs[index] = updated; } else { Configs.Add(updated); } ModifiedAt = timeProvider.GetUtcNow().ToString("O"); Etag = $"\"staleness-{Configs.Count}-{ModifiedAt}\""; return updated; } public object[] BuildStatus() => Configs.Select((config, index) => new { dataType = config.DataType, itemId = $"{config.DataType}-asset-{index + 1}", itemName = $"{config.DataType.Replace('_', ' ')} snapshot {index + 1}", lastUpdatedAt = DateTimeOffset.Parse(ModifiedAt).AddDays(-(index + 1) * 3).ToString("O"), ageDays = (index + 1) * 3, level = index == 0 ? "fresh" : index == 1 ? "aging" : index == 2 ? "stale" : "expired", blocked = index >= 2 && config.Enabled }).ToArray(); } private static string NormalizeSource(string? source) => string.IsNullOrWhiteSpace(source) ? "custom" : source.Trim().ToLowerInvariant(); private static string NormalizeSeverity(string? severity) => string.IsNullOrWhiteSpace(severity) ? "medium" : severity.Trim().ToLowerInvariant(); private static string NormalizeLevel(string? level) => string.IsNullOrWhiteSpace(level) ? "fresh" : level.Trim().ToLowerInvariant(); private static string NormalizeActionType(string? actionType) => string.IsNullOrWhiteSpace(actionType) ? "warn" : actionType.Trim().ToLowerInvariant(); private sealed record TrustWeightRecord( string Id, string IssuerId, string IssuerName, string Source, decimal Weight, int Priority, bool Active, string? Reason, string ModifiedAt, string ModifiedBy); private sealed record StalenessConfigRecord( string DataType, List Thresholds, bool Enabled, int GracePeriodHours); private sealed record StalenessThresholdRecord( string Level, int AgeDays, string Severity, List Actions); private sealed record StalenessActionRecord( string Type, string? Message = null, string[]? Channels = null); } public sealed record TrustWeightWriteModel { public string? Id { get; init; } public string? IssuerId { get; init; } public string? IssuerName { get; init; } public string? Source { get; init; } public decimal? Weight { get; init; } public int? Priority { get; init; } public bool? Active { get; init; } public string? Reason { get; init; } } public sealed record TrustWeightPreviewRequest(IReadOnlyList? Weights); public sealed record StalenessConfigWriteModel { public string? DataType { get; init; } public IReadOnlyList? Thresholds { get; init; } public bool? Enabled { get; init; } public int? GracePeriodHours { get; init; } } public sealed record StalenessThresholdWriteModel { public string? Level { get; init; } public int AgeDays { get; init; } public string? Severity { get; init; } public IReadOnlyList? Actions { get; init; } } public sealed record StalenessActionWriteModel { public string? Type { get; init; } public string? Message { get; init; } public string[]? Channels { get; init; } }