Files
git.stella-ops.org/src/Platform/StellaOps.Platform.WebService/Endpoints/FederationTelemetryEndpoints.cs
2026-02-23 23:44:50 +02:00

275 lines
11 KiB
C#

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.Auth.ServerIntegration.Tenancy;
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")
.RequireAuthorization(PlatformPolicies.FederationRead)
.RequireTenant();
// 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;
}
}