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 _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( 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( 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( 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( HttpContext context, PlatformRequestContextResolver resolver, IConsentManager consentManager, IPrivacyBudgetTracker budgetTracker, Microsoft.Extensions.Options.IOptions 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( HttpContext context, PlatformRequestContextResolver resolver) => { if (!TryResolveContext(context, resolver, out _, out var failure)) return Task.FromResult(failure!); List 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( 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( 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( 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( 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; } }