Gaps fill up, fixes, ui restructuring
This commit is contained in:
@@ -0,0 +1,452 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Platform.WebService.Constants;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Trust and signing owner mutation endpoints backing Administration A6.
|
||||
/// </summary>
|
||||
public static class AdministrationTrustSigningMutationEndpoints
|
||||
{
|
||||
private const int DefaultLimit = 50;
|
||||
private const int MaxLimit = 200;
|
||||
|
||||
public static IEndpointRouteBuilder MapAdministrationTrustSigningMutationEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("/api/v1/administration/trust-signing")
|
||||
.WithTags("Administration");
|
||||
|
||||
group.MapGet("/keys", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IAdministrationTrustSigningStore store,
|
||||
TimeProvider timeProvider,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] int? offset,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var normalizedLimit = NormalizeLimit(limit);
|
||||
var normalizedOffset = NormalizeOffset(offset);
|
||||
var items = await store.ListKeysAsync(
|
||||
requestContext!.TenantId,
|
||||
normalizedLimit,
|
||||
normalizedOffset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<AdministrationTrustKeySummary>(
|
||||
requestContext.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
items,
|
||||
items.Count,
|
||||
normalizedLimit,
|
||||
normalizedOffset));
|
||||
})
|
||||
.WithName("ListAdministrationTrustKeys")
|
||||
.WithSummary("List trust signing keys")
|
||||
.RequireAuthorization(PlatformPolicies.TrustRead);
|
||||
|
||||
group.MapPost("/keys", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IAdministrationTrustSigningStore store,
|
||||
CreateAdministrationTrustKeyRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var created = await store.CreateKeyAsync(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Created($"/api/v1/administration/trust-signing/keys/{created.KeyId}", created);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return MapStoreError(ex, keyId: null, certificateId: null);
|
||||
}
|
||||
})
|
||||
.WithName("CreateAdministrationTrustKey")
|
||||
.WithSummary("Create trust signing key")
|
||||
.RequireAuthorization(PlatformPolicies.TrustWrite);
|
||||
|
||||
group.MapPost("/keys/{keyId:guid}/rotate", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IAdministrationTrustSigningStore store,
|
||||
Guid keyId,
|
||||
RotateAdministrationTrustKeyRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var updated = await store.RotateKeyAsync(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
keyId,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(updated);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return MapStoreError(ex, keyId, certificateId: null);
|
||||
}
|
||||
})
|
||||
.WithName("RotateAdministrationTrustKey")
|
||||
.WithSummary("Rotate trust signing key")
|
||||
.RequireAuthorization(PlatformPolicies.TrustWrite);
|
||||
|
||||
group.MapPost("/keys/{keyId:guid}/revoke", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IAdministrationTrustSigningStore store,
|
||||
Guid keyId,
|
||||
RevokeAdministrationTrustKeyRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var updated = await store.RevokeKeyAsync(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
keyId,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(updated);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return MapStoreError(ex, keyId, certificateId: null);
|
||||
}
|
||||
})
|
||||
.WithName("RevokeAdministrationTrustKey")
|
||||
.WithSummary("Revoke trust signing key")
|
||||
.RequireAuthorization(PlatformPolicies.TrustAdmin);
|
||||
|
||||
group.MapGet("/issuers", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IAdministrationTrustSigningStore store,
|
||||
TimeProvider timeProvider,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] int? offset,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var normalizedLimit = NormalizeLimit(limit);
|
||||
var normalizedOffset = NormalizeOffset(offset);
|
||||
var items = await store.ListIssuersAsync(
|
||||
requestContext!.TenantId,
|
||||
normalizedLimit,
|
||||
normalizedOffset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<AdministrationTrustIssuerSummary>(
|
||||
requestContext.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
items,
|
||||
items.Count,
|
||||
normalizedLimit,
|
||||
normalizedOffset));
|
||||
})
|
||||
.WithName("ListAdministrationTrustIssuers")
|
||||
.WithSummary("List trust issuers")
|
||||
.RequireAuthorization(PlatformPolicies.TrustRead);
|
||||
|
||||
group.MapPost("/issuers", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IAdministrationTrustSigningStore store,
|
||||
RegisterAdministrationTrustIssuerRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var created = await store.RegisterIssuerAsync(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Created($"/api/v1/administration/trust-signing/issuers/{created.IssuerId}", created);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return MapStoreError(ex, keyId: null, certificateId: null);
|
||||
}
|
||||
})
|
||||
.WithName("RegisterAdministrationTrustIssuer")
|
||||
.WithSummary("Register trust issuer")
|
||||
.RequireAuthorization(PlatformPolicies.TrustWrite);
|
||||
|
||||
group.MapGet("/certificates", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IAdministrationTrustSigningStore store,
|
||||
TimeProvider timeProvider,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] int? offset,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var normalizedLimit = NormalizeLimit(limit);
|
||||
var normalizedOffset = NormalizeOffset(offset);
|
||||
var items = await store.ListCertificatesAsync(
|
||||
requestContext!.TenantId,
|
||||
normalizedLimit,
|
||||
normalizedOffset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<AdministrationTrustCertificateSummary>(
|
||||
requestContext.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
items,
|
||||
items.Count,
|
||||
normalizedLimit,
|
||||
normalizedOffset));
|
||||
})
|
||||
.WithName("ListAdministrationTrustCertificates")
|
||||
.WithSummary("List trust certificates")
|
||||
.RequireAuthorization(PlatformPolicies.TrustRead);
|
||||
|
||||
group.MapPost("/certificates", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IAdministrationTrustSigningStore store,
|
||||
RegisterAdministrationTrustCertificateRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var created = await store.RegisterCertificateAsync(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Created($"/api/v1/administration/trust-signing/certificates/{created.CertificateId}", created);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return MapStoreError(ex, keyId: null, certificateId: null);
|
||||
}
|
||||
})
|
||||
.WithName("RegisterAdministrationTrustCertificate")
|
||||
.WithSummary("Register trust certificate")
|
||||
.RequireAuthorization(PlatformPolicies.TrustWrite);
|
||||
|
||||
group.MapPost("/certificates/{certificateId:guid}/revoke", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IAdministrationTrustSigningStore store,
|
||||
Guid certificateId,
|
||||
RevokeAdministrationTrustCertificateRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var updated = await store.RevokeCertificateAsync(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
certificateId,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(updated);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return MapStoreError(ex, keyId: null, certificateId);
|
||||
}
|
||||
})
|
||||
.WithName("RevokeAdministrationTrustCertificate")
|
||||
.WithSummary("Revoke trust certificate")
|
||||
.RequireAuthorization(PlatformPolicies.TrustAdmin);
|
||||
|
||||
group.MapGet("/transparency-log", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IAdministrationTrustSigningStore store,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var config = await store.GetTransparencyLogConfigAsync(
|
||||
requestContext!.TenantId,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (config is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "transparency_log_not_configured" });
|
||||
}
|
||||
|
||||
return Results.Ok(new PlatformItemResponse<AdministrationTransparencyLogConfig>(
|
||||
requestContext.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
config));
|
||||
})
|
||||
.WithName("GetAdministrationTrustTransparencyLog")
|
||||
.WithSummary("Get trust transparency log configuration")
|
||||
.RequireAuthorization(PlatformPolicies.TrustRead);
|
||||
|
||||
group.MapPut("/transparency-log", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IAdministrationTrustSigningStore store,
|
||||
ConfigureAdministrationTransparencyLogRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await store.ConfigureTransparencyLogAsync(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(config);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return MapStoreError(ex, keyId: null, certificateId: null);
|
||||
}
|
||||
})
|
||||
.WithName("ConfigureAdministrationTrustTransparencyLog")
|
||||
.WithSummary("Configure trust transparency log")
|
||||
.RequireAuthorization(PlatformPolicies.TrustAdmin);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static IResult MapStoreError(InvalidOperationException exception, Guid? keyId, Guid? certificateId)
|
||||
{
|
||||
return exception.Message switch
|
||||
{
|
||||
"request_required" => Results.BadRequest(new { error = "request_required" }),
|
||||
"tenant_required" => Results.BadRequest(new { error = "tenant_required" }),
|
||||
"tenant_id_invalid" => Results.BadRequest(new { error = "tenant_id_invalid" }),
|
||||
"reason_required" => Results.BadRequest(new { error = "reason_required" }),
|
||||
"key_alias_required" => Results.BadRequest(new { error = "key_alias_required" }),
|
||||
"key_algorithm_required" => Results.BadRequest(new { error = "key_algorithm_required" }),
|
||||
"key_alias_exists" => Results.Conflict(new { error = "key_alias_exists" }),
|
||||
"key_not_found" => Results.NotFound(new { error = "key_not_found", keyId }),
|
||||
"key_revoked" => Results.Conflict(new { error = "key_revoked", keyId }),
|
||||
"issuer_name_required" => Results.BadRequest(new { error = "issuer_name_required" }),
|
||||
"issuer_uri_required" => Results.BadRequest(new { error = "issuer_uri_required" }),
|
||||
"issuer_uri_invalid" => Results.BadRequest(new { error = "issuer_uri_invalid" }),
|
||||
"issuer_trust_level_required" => Results.BadRequest(new { error = "issuer_trust_level_required" }),
|
||||
"issuer_uri_exists" => Results.Conflict(new { error = "issuer_uri_exists" }),
|
||||
"issuer_not_found" => Results.NotFound(new { error = "issuer_not_found" }),
|
||||
"certificate_serial_required" => Results.BadRequest(new { error = "certificate_serial_required" }),
|
||||
"certificate_validity_invalid" => Results.BadRequest(new { error = "certificate_validity_invalid" }),
|
||||
"certificate_serial_exists" => Results.Conflict(new { error = "certificate_serial_exists" }),
|
||||
"certificate_not_found" => Results.NotFound(new { error = "certificate_not_found", certificateId }),
|
||||
"transparency_log_url_required" => Results.BadRequest(new { error = "transparency_log_url_required" }),
|
||||
"transparency_log_url_invalid" => Results.BadRequest(new { error = "transparency_log_url_invalid" }),
|
||||
"transparency_witness_url_invalid" => Results.BadRequest(new { error = "transparency_witness_url_invalid" }),
|
||||
_ => Results.BadRequest(new { error = exception.Message })
|
||||
};
|
||||
}
|
||||
|
||||
private static int NormalizeLimit(int? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => DefaultLimit,
|
||||
< 1 => 1,
|
||||
> MaxLimit => MaxLimit,
|
||||
_ => value.Value
|
||||
};
|
||||
}
|
||||
|
||||
private static int NormalizeOffset(int? value) => value is null or < 0 ? 0 : value.Value;
|
||||
|
||||
private static bool TryResolveContext(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
out PlatformRequestContext? requestContext,
|
||||
out IResult? failure)
|
||||
{
|
||||
if (resolver.TryResolve(context, out requestContext, out var error))
|
||||
{
|
||||
failure = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Platform.WebService.Constants;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
using StellaOps.Telemetry.Federation.Bundles;
|
||||
using StellaOps.Telemetry.Federation.Consent;
|
||||
using StellaOps.Telemetry.Federation.Intelligence;
|
||||
using StellaOps.Telemetry.Federation.Privacy;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Endpoints;
|
||||
|
||||
public static class FederationTelemetryEndpoints
|
||||
{
|
||||
// In-memory bundle store for MVP; production would use persistent store
|
||||
private static readonly List<FederatedBundle> _bundles = new();
|
||||
private static readonly object _bundleLock = new();
|
||||
|
||||
public static IEndpointRouteBuilder MapFederationTelemetryEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("/api/v1/telemetry/federation")
|
||||
.WithTags("Federated Telemetry");
|
||||
|
||||
// GET /consent — get consent state
|
||||
group.MapGet("/consent", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IConsentManager consentManager,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
return failure!;
|
||||
|
||||
var state = await consentManager.GetConsentStateAsync(requestContext!.TenantId, ct).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new FederationConsentStateResponse(
|
||||
state.Granted, state.GrantedBy, state.GrantedAt, state.ExpiresAt, state.DsseDigest));
|
||||
})
|
||||
.WithName("GetFederationConsent")
|
||||
.WithSummary("Get federation consent state for current tenant")
|
||||
.RequireAuthorization(PlatformPolicies.FederationRead);
|
||||
|
||||
// POST /consent/grant — grant consent
|
||||
group.MapPost("/consent/grant", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IConsentManager consentManager,
|
||||
FederationGrantConsentRequest request,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
return failure!;
|
||||
|
||||
TimeSpan? ttl = request.TtlHours.HasValue
|
||||
? TimeSpan.FromHours(request.TtlHours.Value)
|
||||
: null;
|
||||
|
||||
var proof = await consentManager.GrantConsentAsync(
|
||||
requestContext!.TenantId, request.GrantedBy, ttl, ct).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new FederationConsentProofResponse(
|
||||
proof.TenantId, proof.GrantedBy, proof.GrantedAt, proof.ExpiresAt, proof.DsseDigest));
|
||||
})
|
||||
.WithName("GrantFederationConsent")
|
||||
.WithSummary("Grant federation telemetry consent")
|
||||
.RequireAuthorization(PlatformPolicies.FederationManage);
|
||||
|
||||
// POST /consent/revoke — revoke consent
|
||||
group.MapPost("/consent/revoke", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IConsentManager consentManager,
|
||||
FederationRevokeConsentRequest request,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
return failure!;
|
||||
|
||||
await consentManager.RevokeConsentAsync(requestContext!.TenantId, request.RevokedBy, ct).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new { revoked = true });
|
||||
})
|
||||
.WithName("RevokeFederationConsent")
|
||||
.WithSummary("Revoke federation telemetry consent")
|
||||
.RequireAuthorization(PlatformPolicies.FederationManage);
|
||||
|
||||
// GET /status — federation status
|
||||
group.MapGet("/status", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IConsentManager consentManager,
|
||||
IPrivacyBudgetTracker budgetTracker,
|
||||
Microsoft.Extensions.Options.IOptions<Telemetry.Federation.FederatedTelemetryOptions> fedOptions,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
return failure!;
|
||||
|
||||
var consent = await consentManager.GetConsentStateAsync(requestContext!.TenantId, ct).ConfigureAwait(false);
|
||||
var snapshot = budgetTracker.GetSnapshot();
|
||||
|
||||
int bundleCount;
|
||||
lock (_bundleLock) { bundleCount = _bundles.Count; }
|
||||
|
||||
return Results.Ok(new FederationStatusResponse(
|
||||
Enabled: !fedOptions.Value.SealedModeEnabled,
|
||||
SealedMode: fedOptions.Value.SealedModeEnabled,
|
||||
SiteId: fedOptions.Value.SiteId,
|
||||
ConsentGranted: consent.Granted,
|
||||
EpsilonRemaining: snapshot.Remaining,
|
||||
EpsilonTotal: snapshot.Total,
|
||||
BudgetExhausted: snapshot.Exhausted,
|
||||
NextBudgetReset: snapshot.NextReset,
|
||||
BundleCount: bundleCount));
|
||||
})
|
||||
.WithName("GetFederationStatus")
|
||||
.WithSummary("Get federation telemetry status")
|
||||
.RequireAuthorization(PlatformPolicies.FederationRead);
|
||||
|
||||
// GET /bundles — list bundles
|
||||
group.MapGet("/bundles", Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out _, out var failure))
|
||||
return Task.FromResult(failure!);
|
||||
|
||||
List<FederationBundleSummary> summaries;
|
||||
lock (_bundleLock)
|
||||
{
|
||||
summaries = _bundles.Select(b => new FederationBundleSummary(
|
||||
b.Id, b.SourceSiteId,
|
||||
b.Aggregation.Buckets.Count,
|
||||
b.Aggregation.SuppressedBuckets,
|
||||
b.Aggregation.EpsilonSpent,
|
||||
Verified: true,
|
||||
b.CreatedAt)).ToList();
|
||||
}
|
||||
|
||||
return Task.FromResult(Results.Ok(summaries));
|
||||
})
|
||||
.WithName("ListFederationBundles")
|
||||
.WithSummary("List federation telemetry bundles")
|
||||
.RequireAuthorization(PlatformPolicies.FederationRead);
|
||||
|
||||
// GET /bundles/{id} — bundle detail
|
||||
group.MapGet("/bundles/{id:guid}", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IFederatedTelemetryBundleBuilder bundleBuilder,
|
||||
Guid id,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out _, out var failure))
|
||||
return failure!;
|
||||
|
||||
FederatedBundle? bundle;
|
||||
lock (_bundleLock) { bundle = _bundles.FirstOrDefault(b => b.Id == id); }
|
||||
|
||||
if (bundle is null)
|
||||
return Results.NotFound(new { error = "bundle_not_found", id });
|
||||
|
||||
var verified = await bundleBuilder.VerifyAsync(bundle, ct).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new FederationBundleDetailResponse(
|
||||
bundle.Id, bundle.SourceSiteId,
|
||||
bundle.Aggregation.TotalFacts,
|
||||
bundle.Aggregation.Buckets.Count,
|
||||
bundle.Aggregation.SuppressedBuckets,
|
||||
bundle.Aggregation.EpsilonSpent,
|
||||
bundle.ConsentDsseDigest,
|
||||
bundle.BundleDsseDigest,
|
||||
verified,
|
||||
bundle.Aggregation.AggregatedAt,
|
||||
bundle.CreatedAt,
|
||||
bundle.Aggregation.Buckets.Select(b => new FederationBucketDetail(
|
||||
b.CveId, b.ObservationCount, b.ArtifactCount, b.NoisyCount, b.Suppressed)).ToList()));
|
||||
})
|
||||
.WithName("GetFederationBundle")
|
||||
.WithSummary("Get federation telemetry bundle detail")
|
||||
.RequireAuthorization(PlatformPolicies.FederationRead);
|
||||
|
||||
// GET /intelligence — exploit corpus
|
||||
group.MapGet("/intelligence", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IExploitIntelligenceMerger intelligenceMerger,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out _, out var failure))
|
||||
return failure!;
|
||||
|
||||
var corpus = await intelligenceMerger.GetCorpusAsync(ct).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new FederationIntelligenceResponse(
|
||||
corpus.Entries.Select(e => new FederationIntelligenceEntry(
|
||||
e.CveId, e.SourceSiteId, e.ObservationCount, e.NoisyCount, e.ArtifactCount, e.ObservedAt)).ToList(),
|
||||
corpus.TotalEntries,
|
||||
corpus.UniqueCves,
|
||||
corpus.ContributingSites,
|
||||
corpus.LastUpdated));
|
||||
})
|
||||
.WithName("GetFederationIntelligence")
|
||||
.WithSummary("Get shared exploit intelligence corpus")
|
||||
.RequireAuthorization(PlatformPolicies.FederationRead);
|
||||
|
||||
// GET /privacy-budget — budget snapshot
|
||||
group.MapGet("/privacy-budget", Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IPrivacyBudgetTracker budgetTracker) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out _, out var failure))
|
||||
return Task.FromResult(failure!);
|
||||
|
||||
var snapshot = budgetTracker.GetSnapshot();
|
||||
|
||||
return Task.FromResult(Results.Ok(new FederationPrivacyBudgetResponse(
|
||||
snapshot.Remaining, snapshot.Total, snapshot.Exhausted,
|
||||
snapshot.PeriodStart, snapshot.NextReset,
|
||||
snapshot.QueriesThisPeriod, snapshot.SuppressedThisPeriod)));
|
||||
})
|
||||
.WithName("GetFederationPrivacyBudget")
|
||||
.WithSummary("Get privacy budget snapshot")
|
||||
.RequireAuthorization(PlatformPolicies.FederationRead);
|
||||
|
||||
// POST /trigger — trigger aggregation
|
||||
group.MapPost("/trigger", Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IPrivacyBudgetTracker budgetTracker) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out _, out var failure))
|
||||
return Task.FromResult(failure!);
|
||||
|
||||
if (budgetTracker.IsBudgetExhausted)
|
||||
{
|
||||
return Task.FromResult(Results.Ok(new FederationTriggerResponse(
|
||||
Triggered: false,
|
||||
Reason: "Privacy budget exhausted")));
|
||||
}
|
||||
|
||||
// Placeholder: actual implementation would trigger sync service
|
||||
return Task.FromResult(Results.Ok(new FederationTriggerResponse(
|
||||
Triggered: true,
|
||||
Reason: null)));
|
||||
})
|
||||
.WithName("TriggerFederationAggregation")
|
||||
.WithSummary("Trigger manual federation aggregation cycle")
|
||||
.RequireAuthorization(PlatformPolicies.FederationManage);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static bool TryResolveContext(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
out PlatformRequestContext? requestContext,
|
||||
out IResult? failure)
|
||||
{
|
||||
if (resolver.TryResolve(context, out requestContext, out var error))
|
||||
{
|
||||
failure = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,859 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Platform.WebService.Constants;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Pack-driven adapter endpoints for Dashboard, Platform Ops, and Administration views.
|
||||
/// </summary>
|
||||
public static class PackAdapterEndpoints
|
||||
{
|
||||
private static readonly DateTimeOffset SnapshotAt = DateTimeOffset.Parse("2026-02-19T03:15:00Z");
|
||||
|
||||
public static IEndpointRouteBuilder MapPackAdapterEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
app.MapGet("/api/v1/dashboard/summary", (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var payload = BuildDashboardSummary();
|
||||
return Results.Ok(new PlatformItemResponse<DashboardSummaryDto>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
SnapshotAt,
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
payload));
|
||||
})
|
||||
.WithTags("Dashboard")
|
||||
.WithName("GetDashboardSummary")
|
||||
.WithSummary("Pack v2 dashboard summary projection.")
|
||||
.RequireAuthorization(PlatformPolicies.HealthRead);
|
||||
|
||||
var platform = app.MapGroup("/api/v1/platform")
|
||||
.WithTags("Platform Ops");
|
||||
|
||||
platform.MapGet("/data-integrity/summary", (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var payload = BuildDataIntegritySummary();
|
||||
return Results.Ok(new PlatformItemResponse<DataIntegritySummaryDto>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
SnapshotAt,
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
payload));
|
||||
})
|
||||
.WithName("GetDataIntegritySummary")
|
||||
.WithSummary("Pack v2 data-integrity card summary.")
|
||||
.RequireAuthorization(PlatformPolicies.HealthRead);
|
||||
|
||||
platform.MapGet("/data-integrity/report", (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var payload = BuildDataIntegrityReport();
|
||||
return Results.Ok(new PlatformItemResponse<DataIntegrityReportDto>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
SnapshotAt,
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
payload));
|
||||
})
|
||||
.WithName("GetDataIntegrityReport")
|
||||
.WithSummary("Pack v2 nightly data-integrity report projection.")
|
||||
.RequireAuthorization(PlatformPolicies.HealthRead);
|
||||
|
||||
platform.MapGet("/feeds/freshness", (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var feeds = BuildFeedFreshness();
|
||||
return Results.Ok(new PlatformListResponse<FeedFreshnessDto>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
SnapshotAt,
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
feeds,
|
||||
feeds.Count));
|
||||
})
|
||||
.WithName("GetFeedsFreshness")
|
||||
.WithSummary("Pack v2 advisory/feed freshness projection.")
|
||||
.RequireAuthorization(PlatformPolicies.HealthRead);
|
||||
|
||||
platform.MapGet("/scan-pipeline/health", (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var payload = BuildScanPipelineHealth();
|
||||
return Results.Ok(new PlatformItemResponse<ScanPipelineHealthDto>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
SnapshotAt,
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
payload));
|
||||
})
|
||||
.WithName("GetScanPipelineHealth")
|
||||
.WithSummary("Pack v2 scan-pipeline health projection.")
|
||||
.RequireAuthorization(PlatformPolicies.HealthRead);
|
||||
|
||||
platform.MapGet("/reachability/ingest-health", (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var payload = BuildReachabilityIngestHealth();
|
||||
return Results.Ok(new PlatformItemResponse<ReachabilityIngestHealthDto>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
SnapshotAt,
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
payload));
|
||||
})
|
||||
.WithName("GetReachabilityIngestHealth")
|
||||
.WithSummary("Pack v2 reachability ingest health projection.")
|
||||
.RequireAuthorization(PlatformPolicies.HealthRead);
|
||||
|
||||
var administration = app.MapGroup("/api/v1/administration")
|
||||
.WithTags("Administration");
|
||||
|
||||
administration.MapGet("/summary", (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
{
|
||||
return BuildAdministrationItem(context, resolver, BuildAdministrationSummary);
|
||||
})
|
||||
.WithName("GetAdministrationSummary")
|
||||
.WithSummary("Pack v2 administration overview cards.")
|
||||
.RequireAuthorization(PlatformPolicies.SetupRead);
|
||||
|
||||
administration.MapGet("/identity-access", (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
{
|
||||
return BuildAdministrationItem(context, resolver, BuildAdministrationIdentityAccess);
|
||||
})
|
||||
.WithName("GetAdministrationIdentityAccess")
|
||||
.WithSummary("Pack v2 administration A1 identity and access projection.")
|
||||
.RequireAuthorization(PlatformPolicies.SetupRead);
|
||||
|
||||
administration.MapGet("/tenant-branding", (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
{
|
||||
return BuildAdministrationItem(context, resolver, BuildAdministrationTenantBranding);
|
||||
})
|
||||
.WithName("GetAdministrationTenantBranding")
|
||||
.WithSummary("Pack v2 administration A2 tenant and branding projection.")
|
||||
.RequireAuthorization(PlatformPolicies.SetupRead);
|
||||
|
||||
administration.MapGet("/notifications", (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
{
|
||||
return BuildAdministrationItem(context, resolver, BuildAdministrationNotifications);
|
||||
})
|
||||
.WithName("GetAdministrationNotifications")
|
||||
.WithSummary("Pack v2 administration A3 notifications projection.")
|
||||
.RequireAuthorization(PlatformPolicies.SetupRead);
|
||||
|
||||
administration.MapGet("/usage-limits", (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
{
|
||||
return BuildAdministrationItem(context, resolver, BuildAdministrationUsageLimits);
|
||||
})
|
||||
.WithName("GetAdministrationUsageLimits")
|
||||
.WithSummary("Pack v2 administration A4 usage and limits projection.")
|
||||
.RequireAuthorization(PlatformPolicies.SetupRead);
|
||||
|
||||
administration.MapGet("/policy-governance", (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
{
|
||||
return BuildAdministrationItem(context, resolver, BuildAdministrationPolicyGovernance);
|
||||
})
|
||||
.WithName("GetAdministrationPolicyGovernance")
|
||||
.WithSummary("Pack v2 administration A5 policy governance projection.")
|
||||
.RequireAuthorization(PlatformPolicies.SetupRead);
|
||||
|
||||
administration.MapGet("/trust-signing", (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
{
|
||||
return BuildAdministrationItem(context, resolver, BuildAdministrationTrustSigning);
|
||||
})
|
||||
.WithName("GetAdministrationTrustSigning")
|
||||
.WithSummary("Pack v2 administration A6 trust and signing projection.")
|
||||
.RequireAuthorization(PlatformPolicies.TrustRead);
|
||||
|
||||
administration.MapGet("/system", (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
{
|
||||
return BuildAdministrationItem(context, resolver, BuildAdministrationSystem);
|
||||
})
|
||||
.WithName("GetAdministrationSystem")
|
||||
.WithSummary("Pack v2 administration A7 system projection.")
|
||||
.RequireAuthorization(PlatformPolicies.SetupRead);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static DashboardSummaryDto BuildDashboardSummary()
|
||||
{
|
||||
var confidence = BuildConfidenceBadge();
|
||||
var environments = new[]
|
||||
{
|
||||
new EnvironmentRiskSnapshotDto("apac-prod", CriticalReachable: 0, HighReachable: 0, SbomState: "fresh"),
|
||||
new EnvironmentRiskSnapshotDto("eu-prod", CriticalReachable: 0, HighReachable: 1, SbomState: "fresh"),
|
||||
new EnvironmentRiskSnapshotDto("us-prod", CriticalReachable: 2, HighReachable: 1, SbomState: "stale"),
|
||||
new EnvironmentRiskSnapshotDto("us-uat", CriticalReachable: 1, HighReachable: 2, SbomState: "stale"),
|
||||
}.OrderBy(item => item.Environment, StringComparer.Ordinal).ToList();
|
||||
|
||||
var topDrivers = new[]
|
||||
{
|
||||
new DashboardDriverDto("CVE-2026-1234", "user-service", "critical", "reachable"),
|
||||
new DashboardDriverDto("CVE-2026-2222", "billing-worker", "critical", "reachable"),
|
||||
new DashboardDriverDto("CVE-2026-9001", "api-gateway", "high", "not_reachable"),
|
||||
};
|
||||
|
||||
return new DashboardSummaryDto(
|
||||
DataConfidence: confidence,
|
||||
EnvironmentsWithCriticalReachable: 2,
|
||||
TotalCriticalReachable: 3,
|
||||
SbomCoveragePercent: 98.0m,
|
||||
VexCoveragePercent: 62.0m,
|
||||
BlockedApprovals: 2,
|
||||
ExceptionsExpiringSoon: 4,
|
||||
EnvironmentRisk: environments,
|
||||
TopDrivers: topDrivers);
|
||||
}
|
||||
|
||||
private static DataIntegritySummaryDto BuildDataIntegritySummary()
|
||||
{
|
||||
var cards = new[]
|
||||
{
|
||||
new DataIntegritySignalDto("feeds", "Advisory feeds", "warning", "NVD mirror stale by 3h.", "/api/v1/platform/feeds/freshness"),
|
||||
new DataIntegritySignalDto("reachability", "Reachability ingest", "warning", "Runtime ingest lag exceeds policy threshold.", "/api/v1/platform/reachability/ingest-health"),
|
||||
new DataIntegritySignalDto("scan-pipeline", "Scan pipeline", "warning", "Pending SBOM rescans create stale risk windows.", "/api/v1/platform/scan-pipeline/health"),
|
||||
new DataIntegritySignalDto("sbom", "SBOM coverage", "warning", "12 digests missing a fresh scan snapshot.", "/api/v1/platform/data-integrity/report"),
|
||||
}.OrderBy(card => card.Id, StringComparer.Ordinal).ToList();
|
||||
|
||||
return new DataIntegritySummaryDto(
|
||||
Confidence: BuildConfidenceBadge(),
|
||||
Signals: cards);
|
||||
}
|
||||
|
||||
private static DataIntegrityReportDto BuildDataIntegrityReport()
|
||||
{
|
||||
var sections = new[]
|
||||
{
|
||||
new DataIntegrityReportSectionDto(
|
||||
"advisory-feeds",
|
||||
"warning",
|
||||
"NVD and vendor feed freshness lag detected.",
|
||||
["nvd stale by 3h", "vendor feed retry budget exceeded once"]),
|
||||
new DataIntegrityReportSectionDto(
|
||||
"scan-pipeline",
|
||||
"warning",
|
||||
"Scan backlog increased due to transient worker degradation.",
|
||||
["pending sbom rescans: 12", "oldest pending digest age: 26h"]),
|
||||
new DataIntegrityReportSectionDto(
|
||||
"reachability",
|
||||
"warning",
|
||||
"Runtime attestations delayed in one region.",
|
||||
["us-east runtime agents degraded", "eu-west ingest healthy"]),
|
||||
}.OrderBy(section => section.SectionId, StringComparer.Ordinal).ToList();
|
||||
|
||||
return new DataIntegrityReportDto(
|
||||
ReportId: "ops-nightly-2026-02-19",
|
||||
GeneratedAt: SnapshotAt,
|
||||
Window: "2026-02-18T03:15:00Z/2026-02-19T03:15:00Z",
|
||||
Sections: sections,
|
||||
RecommendedActions:
|
||||
[
|
||||
"Prioritize runtime ingest queue drain in us-east.",
|
||||
"Force-feed refresh for NVD source before next approval window.",
|
||||
"Trigger high-risk SBOM rescan profile for stale production digests.",
|
||||
]);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<FeedFreshnessDto> BuildFeedFreshness()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new FeedFreshnessDto("NVD", "warning", LastSyncedAt: "2026-02-19T00:12:00Z", FreshnessHours: 3, SlaHours: 1),
|
||||
new FeedFreshnessDto("OSV", "healthy", LastSyncedAt: "2026-02-19T03:02:00Z", FreshnessHours: 0, SlaHours: 1),
|
||||
new FeedFreshnessDto("Vendor advisories", "healthy", LastSyncedAt: "2026-02-19T02:48:00Z", FreshnessHours: 0, SlaHours: 2),
|
||||
}.OrderBy(feed => feed.Source, StringComparer.Ordinal).ToList();
|
||||
}
|
||||
|
||||
private static ScanPipelineHealthDto BuildScanPipelineHealth()
|
||||
{
|
||||
var stages = new[]
|
||||
{
|
||||
new PipelineStageHealthDto("ingest", "healthy", QueueDepth: 12, OldestAgeMinutes: 8),
|
||||
new PipelineStageHealthDto("normalize", "healthy", QueueDepth: 3, OldestAgeMinutes: 4),
|
||||
new PipelineStageHealthDto("rescan", "warning", QueueDepth: 12, OldestAgeMinutes: 1570),
|
||||
}.OrderBy(stage => stage.Stage, StringComparer.Ordinal).ToList();
|
||||
|
||||
return new ScanPipelineHealthDto(
|
||||
Status: "warning",
|
||||
PendingDigests: 12,
|
||||
FailedJobs24h: 3,
|
||||
Stages: stages);
|
||||
}
|
||||
|
||||
private static ReachabilityIngestHealthDto BuildReachabilityIngestHealth()
|
||||
{
|
||||
var regions = new[]
|
||||
{
|
||||
new RegionIngestHealthDto("apac", "healthy", Backlog: 7, FreshnessMinutes: 6),
|
||||
new RegionIngestHealthDto("eu-west", "healthy", Backlog: 11, FreshnessMinutes: 7),
|
||||
new RegionIngestHealthDto("us-east", "warning", Backlog: 1230, FreshnessMinutes: 42),
|
||||
}.OrderBy(region => region.Region, StringComparer.Ordinal).ToList();
|
||||
|
||||
return new ReachabilityIngestHealthDto(
|
||||
Status: "warning",
|
||||
RuntimeCoveragePercent: 41,
|
||||
Regions: regions);
|
||||
}
|
||||
|
||||
private static AdministrationSummaryDto BuildAdministrationSummary()
|
||||
{
|
||||
var domains = new[]
|
||||
{
|
||||
new AdministrationDomainCardDto("identity", "Identity & Access", "healthy", "Role assignments and API tokens are within policy.", "/administration/identity-access"),
|
||||
new AdministrationDomainCardDto("notifications", "Notifications", "healthy", "All configured notification providers are operational.", "/administration/notifications"),
|
||||
new AdministrationDomainCardDto("policy", "Policy Governance", "warning", "One policy bundle update is pending review.", "/administration/policy-governance"),
|
||||
new AdministrationDomainCardDto("system", "System", "healthy", "Control plane services report healthy heartbeat.", "/administration/system"),
|
||||
new AdministrationDomainCardDto("tenant", "Tenant & Branding", "healthy", "Tenant branding and domain mappings are current.", "/administration/tenant-branding"),
|
||||
new AdministrationDomainCardDto("trust", "Trust & Signing", "warning", "One certificate expires within 10 days.", "/administration/trust-signing"),
|
||||
new AdministrationDomainCardDto("usage", "Usage & Limits", "warning", "Scanner quota at 65% with upward trend.", "/administration/usage"),
|
||||
}.OrderBy(domain => domain.DomainId, StringComparer.Ordinal).ToList();
|
||||
|
||||
return new AdministrationSummaryDto(
|
||||
Domains: domains,
|
||||
ActiveIncidents:
|
||||
[
|
||||
"trust/certificate-expiry-warning",
|
||||
"usage/scanner-quota-warning",
|
||||
]);
|
||||
}
|
||||
|
||||
private static AdministrationIdentityAccessDto BuildAdministrationIdentityAccess()
|
||||
{
|
||||
var tabs = new[]
|
||||
{
|
||||
new AdministrationFacetTabDto("api-tokens", "API Tokens", Count: 12, Status: "warning", ActionPath: "/administration/identity-access/tokens"),
|
||||
new AdministrationFacetTabDto("oauth-clients", "OAuth/SSO Clients", Count: 6, Status: "healthy", ActionPath: "/administration/identity-access/clients"),
|
||||
new AdministrationFacetTabDto("roles", "Roles", Count: 18, Status: "healthy", ActionPath: "/administration/identity-access/roles"),
|
||||
new AdministrationFacetTabDto("tenants", "Tenants", Count: 4, Status: "healthy", ActionPath: "/administration/identity-access/tenants"),
|
||||
new AdministrationFacetTabDto("users", "Users", Count: 146, Status: "healthy", ActionPath: "/administration/identity-access/users"),
|
||||
}.OrderBy(tab => tab.TabId, StringComparer.Ordinal).ToList();
|
||||
|
||||
var actors = new[]
|
||||
{
|
||||
new IdentityAccessActorDto("alice@core.example", "release-approver", "active", "2026-02-19T01:22:00Z"),
|
||||
new IdentityAccessActorDto("jenkins-bot", "ci-bot", "active", "2026-02-19T02:17:00Z"),
|
||||
new IdentityAccessActorDto("security-admin@core.example", "security-admin", "active", "2026-02-19T02:59:00Z"),
|
||||
}.OrderBy(actor => actor.Actor, StringComparer.Ordinal).ToList();
|
||||
|
||||
var aliases = BuildAdministrationAliases(
|
||||
[
|
||||
new AdministrationRouteAliasDto("/settings/admin/clients", "/administration/identity-access/clients", "redirect"),
|
||||
new AdministrationRouteAliasDto("/settings/admin/roles", "/administration/identity-access/roles", "redirect"),
|
||||
new AdministrationRouteAliasDto("/settings/admin/tenants", "/administration/identity-access/tenants", "redirect"),
|
||||
new AdministrationRouteAliasDto("/settings/admin/tokens", "/administration/identity-access/tokens", "redirect"),
|
||||
new AdministrationRouteAliasDto("/settings/admin/users", "/administration/identity-access/users", "redirect"),
|
||||
]);
|
||||
|
||||
return new AdministrationIdentityAccessDto(
|
||||
Tabs: tabs,
|
||||
RecentActors: actors,
|
||||
LegacyAliases: aliases,
|
||||
AuditLogPath: "/evidence-audit/audit");
|
||||
}
|
||||
|
||||
private static AdministrationTenantBrandingDto BuildAdministrationTenantBranding()
|
||||
{
|
||||
var tenants = new[]
|
||||
{
|
||||
new AdministrationTenantDto("apac-core", "Core APAC", "apac.core.example", "core-pack-v7", "active"),
|
||||
new AdministrationTenantDto("eu-core", "Core EU", "eu.core.example", "core-pack-v7", "active"),
|
||||
new AdministrationTenantDto("us-core", "Core US", "us.core.example", "core-pack-v7", "active"),
|
||||
}.OrderBy(tenant => tenant.TenantId, StringComparer.Ordinal).ToList();
|
||||
|
||||
var aliases = BuildAdministrationAliases(
|
||||
[
|
||||
new AdministrationRouteAliasDto("/settings/admin/branding", "/administration/tenant-branding", "redirect"),
|
||||
]);
|
||||
|
||||
return new AdministrationTenantBrandingDto(
|
||||
Tenants: tenants,
|
||||
BrandingDefaults: new TenantBrandingDefaultsDto(
|
||||
Theme: "light",
|
||||
SupportUrl: "https://support.core.example",
|
||||
LegalFooterVersion: "2026.02"),
|
||||
LegacyAliases: aliases);
|
||||
}
|
||||
|
||||
private static AdministrationNotificationsDto BuildAdministrationNotifications()
|
||||
{
|
||||
var rules = new[]
|
||||
{
|
||||
new AdministrationNotificationRuleDto("critical-reachable", "Critical reachable finding", "high", "active"),
|
||||
new AdministrationNotificationRuleDto("gate-blocked", "Gate blocked release", "high", "active"),
|
||||
new AdministrationNotificationRuleDto("quota-warning", "Quota warning", "medium", "active"),
|
||||
}.OrderBy(rule => rule.RuleId, StringComparer.Ordinal).ToList();
|
||||
|
||||
var channels = new[]
|
||||
{
|
||||
new AdministrationNotificationChannelDto("email", "healthy", "2026-02-19T02:40:00Z"),
|
||||
new AdministrationNotificationChannelDto("slack", "healthy", "2026-02-19T02:41:00Z"),
|
||||
new AdministrationNotificationChannelDto("webhook", "warning", "2026-02-19T01:58:00Z"),
|
||||
}.OrderBy(channel => channel.ChannelId, StringComparer.Ordinal).ToList();
|
||||
|
||||
var aliases = BuildAdministrationAliases(
|
||||
[
|
||||
new AdministrationRouteAliasDto("/admin/notifications", "/administration/notifications", "redirect"),
|
||||
new AdministrationRouteAliasDto("/operations/notifications", "/administration/notifications", "redirect"),
|
||||
new AdministrationRouteAliasDto("/settings/notifications/*", "/administration/notifications/*", "redirect"),
|
||||
]);
|
||||
|
||||
return new AdministrationNotificationsDto(
|
||||
Rules: rules,
|
||||
Channels: channels,
|
||||
ChannelManagementPath: "/integrations/notifications",
|
||||
LegacyAliases: aliases);
|
||||
}
|
||||
|
||||
private static AdministrationUsageLimitsDto BuildAdministrationUsageLimits()
|
||||
{
|
||||
var meters = new[]
|
||||
{
|
||||
new AdministrationUsageMeterDto("api-calls", "API calls", Used: 15000, Limit: 100000, Unit: "calls"),
|
||||
new AdministrationUsageMeterDto("evidence-packets", "Evidence packets", Used: 2800, Limit: 10000, Unit: "packets"),
|
||||
new AdministrationUsageMeterDto("scanner-runs", "Scanner runs", Used: 6500, Limit: 10000, Unit: "runs"),
|
||||
new AdministrationUsageMeterDto("storage", "Storage", Used: 42, Limit: 100, Unit: "GB"),
|
||||
}.OrderBy(meter => meter.MeterId, StringComparer.Ordinal).ToList();
|
||||
|
||||
var policies = new[]
|
||||
{
|
||||
new AdministrationUsagePolicyDto("api-burst", "API burst throttle", "enabled"),
|
||||
new AdministrationUsagePolicyDto("integration-cap", "Per-integration cap", "enabled"),
|
||||
new AdministrationUsagePolicyDto("scanner-quota", "Scanner daily quota", "warning"),
|
||||
}.OrderBy(policy => policy.PolicyId, StringComparer.Ordinal).ToList();
|
||||
|
||||
var aliases = BuildAdministrationAliases(
|
||||
[
|
||||
new AdministrationRouteAliasDto("/settings/admin/:page", "/administration/:page", "redirect"),
|
||||
]);
|
||||
|
||||
return new AdministrationUsageLimitsDto(
|
||||
Meters: meters,
|
||||
Policies: policies,
|
||||
OperationsDrilldownPath: "/platform-ops/quotas",
|
||||
LegacyAliases: aliases);
|
||||
}
|
||||
|
||||
private static AdministrationPolicyGovernanceDto BuildAdministrationPolicyGovernance()
|
||||
{
|
||||
var baselines = new[]
|
||||
{
|
||||
new AdministrationPolicyBaselineDto("dev", "core-pack-v7", "active"),
|
||||
new AdministrationPolicyBaselineDto("prod", "core-pack-v7", "active"),
|
||||
new AdministrationPolicyBaselineDto("stage", "core-pack-v7", "active"),
|
||||
}.OrderBy(baseline => baseline.Environment, StringComparer.Ordinal).ToList();
|
||||
|
||||
var signals = new[]
|
||||
{
|
||||
new AdministrationPolicySignalDto("exception-workflow", "warning", "2 pending exception approvals"),
|
||||
new AdministrationPolicySignalDto("governance-rules", "healthy", "Reachable-critical gate enforced"),
|
||||
new AdministrationPolicySignalDto("simulation", "healthy", "Last what-if simulation completed successfully"),
|
||||
}.OrderBy(signal => signal.SignalId, StringComparer.Ordinal).ToList();
|
||||
|
||||
var aliases = BuildAdministrationAliases(
|
||||
[
|
||||
new AdministrationRouteAliasDto("/admin/policy/governance", "/administration/policy-governance", "redirect"),
|
||||
new AdministrationRouteAliasDto("/admin/policy/simulation", "/administration/policy-governance/simulation", "redirect"),
|
||||
new AdministrationRouteAliasDto("/policy/exceptions/*", "/administration/policy-governance/exceptions/*", "redirect"),
|
||||
new AdministrationRouteAliasDto("/policy/governance", "/administration/policy-governance", "redirect"),
|
||||
new AdministrationRouteAliasDto("/policy/packs/*", "/administration/policy-governance/packs/*", "redirect"),
|
||||
]);
|
||||
|
||||
return new AdministrationPolicyGovernanceDto(
|
||||
Baselines: baselines,
|
||||
Signals: signals,
|
||||
LegacyAliases: aliases,
|
||||
CrossLinks:
|
||||
[
|
||||
"/release-control/approvals",
|
||||
"/administration/policy/exceptions",
|
||||
]);
|
||||
}
|
||||
|
||||
private static AdministrationTrustSigningDto BuildAdministrationTrustSigning()
|
||||
{
|
||||
var signals = new[]
|
||||
{
|
||||
new AdministrationTrustSignalDto("audit-log", "healthy", "Audit log ingestion is current."),
|
||||
new AdministrationTrustSignalDto("certificate-expiry", "warning", "1 certificate expires within 10 days."),
|
||||
new AdministrationTrustSignalDto("transparency-log", "healthy", "Rekor witness is reachable."),
|
||||
new AdministrationTrustSignalDto("trust-scoring", "healthy", "Issuer trust score recalculation completed."),
|
||||
}.OrderBy(signal => signal.SignalId, StringComparer.Ordinal).ToList();
|
||||
|
||||
var aliases = BuildAdministrationAliases(
|
||||
[
|
||||
new AdministrationRouteAliasDto("/admin/issuers", "/administration/trust-signing/issuers", "redirect"),
|
||||
new AdministrationRouteAliasDto("/admin/trust/*", "/administration/trust-signing/*", "redirect"),
|
||||
new AdministrationRouteAliasDto("/settings/trust/*", "/administration/trust-signing/*", "redirect"),
|
||||
]);
|
||||
|
||||
return new AdministrationTrustSigningDto(
|
||||
Inventory: new AdministrationTrustInventoryDto(Keys: 14, Issuers: 7, Certificates: 23),
|
||||
Signals: signals,
|
||||
LegacyAliases: aliases,
|
||||
EvidenceConsumerPath: "/evidence-audit/proofs");
|
||||
}
|
||||
|
||||
private static AdministrationSystemDto BuildAdministrationSystem()
|
||||
{
|
||||
var controls = new[]
|
||||
{
|
||||
new AdministrationSystemControlDto("background-jobs", "warning", "1 paused job family awaiting manual resume."),
|
||||
new AdministrationSystemControlDto("doctor", "healthy", "Last diagnostics run passed."),
|
||||
new AdministrationSystemControlDto("health-check", "healthy", "All core control-plane services are healthy."),
|
||||
new AdministrationSystemControlDto("slo-config", "healthy", "SLO thresholds are synchronized."),
|
||||
}.OrderBy(control => control.ControlId, StringComparer.Ordinal).ToList();
|
||||
|
||||
var aliases = BuildAdministrationAliases(
|
||||
[
|
||||
new AdministrationRouteAliasDto("/operations/status", "/administration/system/status", "redirect"),
|
||||
new AdministrationRouteAliasDto("/settings/configuration-pane", "/administration/system/configuration", "redirect"),
|
||||
new AdministrationRouteAliasDto("/settings/workflows/*", "/administration/system/workflows", "redirect"),
|
||||
]);
|
||||
|
||||
return new AdministrationSystemDto(
|
||||
OverallStatus: "healthy",
|
||||
Controls: controls,
|
||||
LegacyAliases: aliases,
|
||||
Drilldowns:
|
||||
[
|
||||
"/platform-ops/health",
|
||||
"/platform-ops/orchestrator/jobs",
|
||||
"/platform-ops/data-integrity",
|
||||
]);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AdministrationRouteAliasDto> BuildAdministrationAliases(
|
||||
AdministrationRouteAliasDto[] aliases)
|
||||
{
|
||||
return aliases
|
||||
.OrderBy(alias => alias.LegacyPath, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static DataConfidenceBadgeDto BuildConfidenceBadge()
|
||||
{
|
||||
return new DataConfidenceBadgeDto(
|
||||
Status: "warning",
|
||||
Summary: "NVD freshness and runtime ingest lag reduce confidence.",
|
||||
NvdStalenessHours: 3,
|
||||
StaleSbomDigests: 12,
|
||||
RuntimeDlqDepth: 1230);
|
||||
}
|
||||
|
||||
private static IResult BuildAdministrationItem<T>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
Func<T> payloadFactory)
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var payload = payloadFactory();
|
||||
return Results.Ok(new PlatformItemResponse<T>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
SnapshotAt,
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
payload));
|
||||
}
|
||||
|
||||
private static bool TryResolveContext(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
out PlatformRequestContext? requestContext,
|
||||
out IResult? failure)
|
||||
{
|
||||
if (resolver.TryResolve(context, out requestContext, out var error))
|
||||
{
|
||||
failure = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record DashboardSummaryDto(
|
||||
DataConfidenceBadgeDto DataConfidence,
|
||||
int EnvironmentsWithCriticalReachable,
|
||||
int TotalCriticalReachable,
|
||||
decimal SbomCoveragePercent,
|
||||
decimal VexCoveragePercent,
|
||||
int BlockedApprovals,
|
||||
int ExceptionsExpiringSoon,
|
||||
IReadOnlyList<EnvironmentRiskSnapshotDto> EnvironmentRisk,
|
||||
IReadOnlyList<DashboardDriverDto> TopDrivers);
|
||||
|
||||
public sealed record EnvironmentRiskSnapshotDto(
|
||||
string Environment,
|
||||
int CriticalReachable,
|
||||
int HighReachable,
|
||||
string SbomState);
|
||||
|
||||
public sealed record DashboardDriverDto(
|
||||
string Cve,
|
||||
string Component,
|
||||
string Severity,
|
||||
string Reachability);
|
||||
|
||||
public sealed record DataIntegritySummaryDto(
|
||||
DataConfidenceBadgeDto Confidence,
|
||||
IReadOnlyList<DataIntegritySignalDto> Signals);
|
||||
|
||||
public sealed record DataIntegritySignalDto(
|
||||
string Id,
|
||||
string Label,
|
||||
string Status,
|
||||
string Summary,
|
||||
string ActionPath);
|
||||
|
||||
public sealed record DataIntegrityReportDto(
|
||||
string ReportId,
|
||||
DateTimeOffset GeneratedAt,
|
||||
string Window,
|
||||
IReadOnlyList<DataIntegrityReportSectionDto> Sections,
|
||||
IReadOnlyList<string> RecommendedActions);
|
||||
|
||||
public sealed record DataIntegrityReportSectionDto(
|
||||
string SectionId,
|
||||
string Status,
|
||||
string Summary,
|
||||
IReadOnlyList<string> Highlights);
|
||||
|
||||
public sealed record FeedFreshnessDto(
|
||||
string Source,
|
||||
string Status,
|
||||
string LastSyncedAt,
|
||||
int FreshnessHours,
|
||||
int SlaHours);
|
||||
|
||||
public sealed record ScanPipelineHealthDto(
|
||||
string Status,
|
||||
int PendingDigests,
|
||||
int FailedJobs24h,
|
||||
IReadOnlyList<PipelineStageHealthDto> Stages);
|
||||
|
||||
public sealed record PipelineStageHealthDto(
|
||||
string Stage,
|
||||
string Status,
|
||||
int QueueDepth,
|
||||
int OldestAgeMinutes);
|
||||
|
||||
public sealed record ReachabilityIngestHealthDto(
|
||||
string Status,
|
||||
int RuntimeCoveragePercent,
|
||||
IReadOnlyList<RegionIngestHealthDto> Regions);
|
||||
|
||||
public sealed record RegionIngestHealthDto(
|
||||
string Region,
|
||||
string Status,
|
||||
int Backlog,
|
||||
int FreshnessMinutes);
|
||||
|
||||
public sealed record AdministrationSummaryDto(
|
||||
IReadOnlyList<AdministrationDomainCardDto> Domains,
|
||||
IReadOnlyList<string> ActiveIncidents);
|
||||
|
||||
public sealed record AdministrationDomainCardDto(
|
||||
string DomainId,
|
||||
string Label,
|
||||
string Status,
|
||||
string Summary,
|
||||
string ActionPath);
|
||||
|
||||
public sealed record AdministrationIdentityAccessDto(
|
||||
IReadOnlyList<AdministrationFacetTabDto> Tabs,
|
||||
IReadOnlyList<IdentityAccessActorDto> RecentActors,
|
||||
IReadOnlyList<AdministrationRouteAliasDto> LegacyAliases,
|
||||
string AuditLogPath);
|
||||
|
||||
public sealed record AdministrationFacetTabDto(
|
||||
string TabId,
|
||||
string Label,
|
||||
int Count,
|
||||
string Status,
|
||||
string ActionPath);
|
||||
|
||||
public sealed record IdentityAccessActorDto(
|
||||
string Actor,
|
||||
string Role,
|
||||
string Status,
|
||||
string LastSeenAt);
|
||||
|
||||
public sealed record AdministrationTenantBrandingDto(
|
||||
IReadOnlyList<AdministrationTenantDto> Tenants,
|
||||
TenantBrandingDefaultsDto BrandingDefaults,
|
||||
IReadOnlyList<AdministrationRouteAliasDto> LegacyAliases);
|
||||
|
||||
public sealed record AdministrationTenantDto(
|
||||
string TenantId,
|
||||
string DisplayName,
|
||||
string PrimaryDomain,
|
||||
string DefaultPolicyPack,
|
||||
string Status);
|
||||
|
||||
public sealed record TenantBrandingDefaultsDto(
|
||||
string Theme,
|
||||
string SupportUrl,
|
||||
string LegalFooterVersion);
|
||||
|
||||
public sealed record AdministrationNotificationsDto(
|
||||
IReadOnlyList<AdministrationNotificationRuleDto> Rules,
|
||||
IReadOnlyList<AdministrationNotificationChannelDto> Channels,
|
||||
string ChannelManagementPath,
|
||||
IReadOnlyList<AdministrationRouteAliasDto> LegacyAliases);
|
||||
|
||||
public sealed record AdministrationNotificationRuleDto(
|
||||
string RuleId,
|
||||
string Label,
|
||||
string Severity,
|
||||
string Status);
|
||||
|
||||
public sealed record AdministrationNotificationChannelDto(
|
||||
string ChannelId,
|
||||
string Status,
|
||||
string LastDeliveredAt);
|
||||
|
||||
public sealed record AdministrationUsageLimitsDto(
|
||||
IReadOnlyList<AdministrationUsageMeterDto> Meters,
|
||||
IReadOnlyList<AdministrationUsagePolicyDto> Policies,
|
||||
string OperationsDrilldownPath,
|
||||
IReadOnlyList<AdministrationRouteAliasDto> LegacyAliases);
|
||||
|
||||
public sealed record AdministrationUsageMeterDto(
|
||||
string MeterId,
|
||||
string Label,
|
||||
int Used,
|
||||
int Limit,
|
||||
string Unit);
|
||||
|
||||
public sealed record AdministrationUsagePolicyDto(
|
||||
string PolicyId,
|
||||
string Label,
|
||||
string Status);
|
||||
|
||||
public sealed record AdministrationPolicyGovernanceDto(
|
||||
IReadOnlyList<AdministrationPolicyBaselineDto> Baselines,
|
||||
IReadOnlyList<AdministrationPolicySignalDto> Signals,
|
||||
IReadOnlyList<AdministrationRouteAliasDto> LegacyAliases,
|
||||
IReadOnlyList<string> CrossLinks);
|
||||
|
||||
public sealed record AdministrationPolicyBaselineDto(
|
||||
string Environment,
|
||||
string PolicyPack,
|
||||
string Status);
|
||||
|
||||
public sealed record AdministrationPolicySignalDto(
|
||||
string SignalId,
|
||||
string Status,
|
||||
string Summary);
|
||||
|
||||
public sealed record AdministrationTrustSigningDto(
|
||||
AdministrationTrustInventoryDto Inventory,
|
||||
IReadOnlyList<AdministrationTrustSignalDto> Signals,
|
||||
IReadOnlyList<AdministrationRouteAliasDto> LegacyAliases,
|
||||
string EvidenceConsumerPath);
|
||||
|
||||
public sealed record AdministrationTrustInventoryDto(
|
||||
int Keys,
|
||||
int Issuers,
|
||||
int Certificates);
|
||||
|
||||
public sealed record AdministrationTrustSignalDto(
|
||||
string SignalId,
|
||||
string Status,
|
||||
string Summary);
|
||||
|
||||
public sealed record AdministrationSystemDto(
|
||||
string OverallStatus,
|
||||
IReadOnlyList<AdministrationSystemControlDto> Controls,
|
||||
IReadOnlyList<AdministrationRouteAliasDto> LegacyAliases,
|
||||
IReadOnlyList<string> Drilldowns);
|
||||
|
||||
public sealed record AdministrationSystemControlDto(
|
||||
string ControlId,
|
||||
string Status,
|
||||
string Summary);
|
||||
|
||||
public sealed record AdministrationRouteAliasDto(
|
||||
string LegacyPath,
|
||||
string CanonicalPath,
|
||||
string Action);
|
||||
|
||||
public sealed record DataConfidenceBadgeDto(
|
||||
string Status,
|
||||
string Summary,
|
||||
int NvdStalenessHours,
|
||||
int StaleSbomDigests,
|
||||
int RuntimeDlqDepth);
|
||||
@@ -0,0 +1,330 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Platform.WebService.Constants;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Release Control bundle lifecycle endpoints consumed by UI v2 shell.
|
||||
/// </summary>
|
||||
public static class ReleaseControlEndpoints
|
||||
{
|
||||
private const int DefaultLimit = 50;
|
||||
private const int MaxLimit = 200;
|
||||
|
||||
public static IEndpointRouteBuilder MapReleaseControlEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var bundles = app.MapGroup("/api/v1/release-control/bundles")
|
||||
.WithTags("Release Control");
|
||||
|
||||
bundles.MapGet(string.Empty, async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IReleaseControlBundleStore store,
|
||||
TimeProvider timeProvider,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] int? offset,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var normalizedLimit = NormalizeLimit(limit);
|
||||
var normalizedOffset = NormalizeOffset(offset);
|
||||
|
||||
var items = await store.ListBundlesAsync(
|
||||
requestContext!.TenantId,
|
||||
normalizedLimit,
|
||||
normalizedOffset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<ReleaseControlBundleSummary>(
|
||||
requestContext.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
items,
|
||||
items.Count,
|
||||
normalizedLimit,
|
||||
normalizedOffset));
|
||||
})
|
||||
.WithName("ListReleaseControlBundles")
|
||||
.WithSummary("List release control bundles")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
bundles.MapGet("/{bundleId:guid}", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IReleaseControlBundleStore store,
|
||||
TimeProvider timeProvider,
|
||||
Guid bundleId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await store.GetBundleAsync(
|
||||
requestContext!.TenantId,
|
||||
bundleId,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "bundle_not_found", bundleId });
|
||||
}
|
||||
|
||||
return Results.Ok(new PlatformItemResponse<ReleaseControlBundleDetail>(
|
||||
requestContext.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
item));
|
||||
})
|
||||
.WithName("GetReleaseControlBundle")
|
||||
.WithSummary("Get release control bundle by id")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
bundles.MapPost(string.Empty, async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IReleaseControlBundleStore store,
|
||||
CreateReleaseControlBundleRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var created = await store.CreateBundleAsync(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var location = $"/api/v1/release-control/bundles/{created.Id}";
|
||||
return Results.Created(location, created);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return MapStoreError(ex, bundleId: null, versionId: null);
|
||||
}
|
||||
})
|
||||
.WithName("CreateReleaseControlBundle")
|
||||
.WithSummary("Create release control bundle")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlOperate);
|
||||
|
||||
bundles.MapGet("/{bundleId:guid}/versions", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IReleaseControlBundleStore store,
|
||||
TimeProvider timeProvider,
|
||||
Guid bundleId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] int? offset,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var normalizedLimit = NormalizeLimit(limit);
|
||||
var normalizedOffset = NormalizeOffset(offset);
|
||||
|
||||
try
|
||||
{
|
||||
var items = await store.ListVersionsAsync(
|
||||
requestContext!.TenantId,
|
||||
bundleId,
|
||||
normalizedLimit,
|
||||
normalizedOffset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<ReleaseControlBundleVersionSummary>(
|
||||
requestContext.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
items,
|
||||
items.Count,
|
||||
normalizedLimit,
|
||||
normalizedOffset));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return MapStoreError(ex, bundleId, versionId: null);
|
||||
}
|
||||
})
|
||||
.WithName("ListReleaseControlBundleVersions")
|
||||
.WithSummary("List bundle versions")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
bundles.MapGet("/{bundleId:guid}/versions/{versionId:guid}", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IReleaseControlBundleStore store,
|
||||
TimeProvider timeProvider,
|
||||
Guid bundleId,
|
||||
Guid versionId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var version = await store.GetVersionAsync(
|
||||
requestContext!.TenantId,
|
||||
bundleId,
|
||||
versionId,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (version is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "bundle_version_not_found", bundleId, versionId });
|
||||
}
|
||||
|
||||
return Results.Ok(new PlatformItemResponse<ReleaseControlBundleVersionDetail>(
|
||||
requestContext.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
version));
|
||||
})
|
||||
.WithName("GetReleaseControlBundleVersion")
|
||||
.WithSummary("Get bundle version")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
bundles.MapPost("/{bundleId:guid}/versions", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IReleaseControlBundleStore store,
|
||||
Guid bundleId,
|
||||
PublishReleaseControlBundleVersionRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var created = await store.PublishVersionAsync(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
bundleId,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var location = $"/api/v1/release-control/bundles/{bundleId}/versions/{created.Id}";
|
||||
return Results.Created(location, created);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return MapStoreError(ex, bundleId, versionId: null);
|
||||
}
|
||||
})
|
||||
.WithName("PublishReleaseControlBundleVersion")
|
||||
.WithSummary("Publish immutable bundle version")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlOperate);
|
||||
|
||||
bundles.MapPost("/{bundleId:guid}/versions/{versionId:guid}/materialize", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IReleaseControlBundleStore store,
|
||||
Guid bundleId,
|
||||
Guid versionId,
|
||||
MaterializeReleaseControlBundleVersionRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var run = await store.MaterializeVersionAsync(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
bundleId,
|
||||
versionId,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var location = $"/api/v1/release-control/bundles/{bundleId}/versions/{versionId}/materialize/{run.RunId}";
|
||||
return Results.Accepted(location, run);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return MapStoreError(ex, bundleId, versionId);
|
||||
}
|
||||
})
|
||||
.WithName("MaterializeReleaseControlBundleVersion")
|
||||
.WithSummary("Materialize bundle version")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlOperate);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static int NormalizeLimit(int? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => DefaultLimit,
|
||||
< 1 => 1,
|
||||
> MaxLimit => MaxLimit,
|
||||
_ => value.Value
|
||||
};
|
||||
}
|
||||
|
||||
private static int NormalizeOffset(int? value) => value is null or < 0 ? 0 : value.Value;
|
||||
|
||||
private static IResult MapStoreError(InvalidOperationException exception, Guid? bundleId, Guid? versionId)
|
||||
{
|
||||
return exception.Message switch
|
||||
{
|
||||
"bundle_not_found" => Results.NotFound(new { error = "bundle_not_found", bundleId }),
|
||||
"bundle_version_not_found" => Results.NotFound(new { error = "bundle_version_not_found", bundleId, versionId }),
|
||||
"bundle_slug_exists" => Results.Conflict(new { error = "bundle_slug_exists" }),
|
||||
"bundle_slug_required" => Results.BadRequest(new { error = "bundle_slug_required" }),
|
||||
"bundle_name_required" => Results.BadRequest(new { error = "bundle_name_required" }),
|
||||
"request_required" => Results.BadRequest(new { error = "request_required" }),
|
||||
"tenant_required" => Results.BadRequest(new { error = "tenant_required" }),
|
||||
"tenant_id_invalid" => Results.BadRequest(new { error = "tenant_id_invalid" }),
|
||||
_ => Results.BadRequest(new { error = exception.Message })
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryResolveContext(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
out PlatformRequestContext? requestContext,
|
||||
out IResult? failure)
|
||||
{
|
||||
if (resolver.TryResolve(context, out requestContext, out var error))
|
||||
{
|
||||
failure = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user