275 lines
11 KiB
C#
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;
|
|
}
|
|
}
|