Gaps fill up, fixes, ui restructuring

This commit is contained in:
master
2026-02-19 22:10:54 +02:00
parent b5829dce5c
commit 04cacdca8a
331 changed files with 42859 additions and 2174 deletions

View File

@@ -0,0 +1,29 @@
namespace StellaOps.Telemetry.Federation.Aggregation;
public interface ITelemetryAggregator
{
Task<AggregationResult> AggregateAsync(
IReadOnlyList<TelemetryFact> facts,
CancellationToken ct = default);
}
public sealed record TelemetryFact(
string ArtifactDigest,
string CveId,
string SymbolPath,
bool Exploited,
DateTimeOffset ObservedAt);
public sealed record AggregationBucket(
string CveId,
int ObservationCount,
int ArtifactCount,
double NoisyCount,
bool Suppressed);
public sealed record AggregationResult(
IReadOnlyList<AggregationBucket> Buckets,
int TotalFacts,
int SuppressedBuckets,
double EpsilonSpent,
DateTimeOffset AggregatedAt);

View File

@@ -0,0 +1,101 @@
using Microsoft.Extensions.Options;
using StellaOps.Telemetry.Federation.Privacy;
namespace StellaOps.Telemetry.Federation.Aggregation;
public sealed class TelemetryAggregator : ITelemetryAggregator
{
private readonly FederatedTelemetryOptions _options;
private readonly IPrivacyBudgetTracker _budgetTracker;
private readonly TimeProvider _timeProvider;
private readonly Random _rng;
public TelemetryAggregator(
IOptions<FederatedTelemetryOptions> options,
IPrivacyBudgetTracker budgetTracker,
TimeProvider? timeProvider = null,
Random? rng = null)
{
_options = options.Value;
_budgetTracker = budgetTracker;
_timeProvider = timeProvider ?? TimeProvider.System;
_rng = rng ?? Random.Shared;
}
public Task<AggregationResult> AggregateAsync(
IReadOnlyList<TelemetryFact> facts,
CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
// Group facts by CVE ID
var groups = facts
.GroupBy(f => f.CveId)
.Select(g => new
{
CveId = g.Key,
Observations = g.ToList(),
DistinctArtifacts = g.Select(f => f.ArtifactDigest).Distinct().Count()
})
.ToList();
var buckets = new List<AggregationBucket>();
var suppressedCount = 0;
var epsilonPerBucket = _options.EpsilonBudget / Math.Max(1, groups.Count);
var totalEpsilonSpent = 0.0;
foreach (var group in groups)
{
// K-anonymity: suppress buckets with fewer than k distinct artifacts
if (group.DistinctArtifacts < _options.KAnonymityThreshold)
{
buckets.Add(new AggregationBucket(
CveId: group.CveId,
ObservationCount: group.Observations.Count,
ArtifactCount: group.DistinctArtifacts,
NoisyCount: 0,
Suppressed: true));
suppressedCount++;
continue;
}
// Try to spend epsilon for this bucket
if (!_budgetTracker.TrySpend(epsilonPerBucket))
{
buckets.Add(new AggregationBucket(
CveId: group.CveId,
ObservationCount: group.Observations.Count,
ArtifactCount: group.DistinctArtifacts,
NoisyCount: 0,
Suppressed: true));
suppressedCount++;
continue;
}
// Add Laplacian noise to the observation count
var noise = PrivacyBudgetTracker.LaplacianNoise(
sensitivity: 1.0,
epsilon: epsilonPerBucket,
rng: _rng);
var noisyCount = Math.Max(0, group.Observations.Count + noise);
totalEpsilonSpent += epsilonPerBucket;
buckets.Add(new AggregationBucket(
CveId: group.CveId,
ObservationCount: group.Observations.Count,
ArtifactCount: group.DistinctArtifacts,
NoisyCount: noisyCount,
Suppressed: false));
}
var result = new AggregationResult(
Buckets: buckets,
TotalFacts: facts.Count,
SuppressedBuckets: suppressedCount,
EpsilonSpent: totalEpsilonSpent,
AggregatedAt: _timeProvider.GetUtcNow());
return Task.FromResult(result);
}
}

View File

@@ -0,0 +1,82 @@
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.Extensions.Options;
using StellaOps.Telemetry.Federation.Aggregation;
using StellaOps.Telemetry.Federation.Consent;
namespace StellaOps.Telemetry.Federation.Bundles;
public sealed class FederatedTelemetryBundleBuilder : IFederatedTelemetryBundleBuilder
{
private readonly FederatedTelemetryOptions _options;
private readonly TimeProvider _timeProvider;
public FederatedTelemetryBundleBuilder(
IOptions<FederatedTelemetryOptions> options,
TimeProvider? timeProvider = null)
{
_options = options.Value;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public Task<FederatedBundle> BuildAsync(
AggregationResult aggregation,
ConsentProof consent,
CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var bundleId = Guid.NewGuid();
var now = _timeProvider.GetUtcNow();
var payload = JsonSerializer.SerializeToUtf8Bytes(new
{
id = bundleId,
siteId = _options.SiteId,
predicateType = _options.BundlePredicateType,
aggregatedAt = aggregation.AggregatedAt,
totalFacts = aggregation.TotalFacts,
suppressedBuckets = aggregation.SuppressedBuckets,
epsilonSpent = aggregation.EpsilonSpent,
buckets = aggregation.Buckets.Where(b => !b.Suppressed).Select(b => new
{
cveId = b.CveId,
noisyCount = b.NoisyCount,
artifactCount = b.ArtifactCount
}),
consentDigest = consent.DsseDigest,
createdAt = now
});
var digest = ComputeDigest(payload);
var envelope = payload; // Placeholder: real DSSE envelope wraps with signature
var bundle = new FederatedBundle(
Id: bundleId,
SourceSiteId: _options.SiteId,
Aggregation: aggregation,
ConsentDsseDigest: consent.DsseDigest,
BundleDsseDigest: digest,
Envelope: envelope,
CreatedAt: now);
return Task.FromResult(bundle);
}
public Task<bool> VerifyAsync(FederatedBundle bundle, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
// Verify the bundle digest matches the envelope content
var recomputedDigest = ComputeDigest(bundle.Envelope);
var isValid = string.Equals(recomputedDigest, bundle.BundleDsseDigest, StringComparison.Ordinal);
return Task.FromResult(isValid);
}
private static string ComputeDigest(byte[] payload)
{
var hash = SHA256.HashData(payload);
return $"sha256:{Convert.ToHexStringLower(hash)}";
}
}

View File

@@ -0,0 +1,25 @@
using StellaOps.Telemetry.Federation.Aggregation;
using StellaOps.Telemetry.Federation.Consent;
namespace StellaOps.Telemetry.Federation.Bundles;
public interface IFederatedTelemetryBundleBuilder
{
Task<FederatedBundle> BuildAsync(
AggregationResult aggregation,
ConsentProof consent,
CancellationToken ct = default);
Task<bool> VerifyAsync(
FederatedBundle bundle,
CancellationToken ct = default);
}
public sealed record FederatedBundle(
Guid Id,
string SourceSiteId,
AggregationResult Aggregation,
string ConsentDsseDigest,
string BundleDsseDigest,
byte[] Envelope,
DateTimeOffset CreatedAt);

View File

@@ -0,0 +1,106 @@
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace StellaOps.Telemetry.Federation.Consent;
public sealed class ConsentManager : IConsentManager
{
private readonly ConcurrentDictionary<string, ConsentEntry> _consents = new();
private readonly TimeProvider _timeProvider;
public ConsentManager(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
public Task<ConsentState> GetConsentStateAsync(string tenantId, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
if (!_consents.TryGetValue(tenantId, out var entry))
{
return Task.FromResult(new ConsentState(
Granted: false,
GrantedBy: null,
GrantedAt: null,
ExpiresAt: null,
DsseDigest: null));
}
var now = _timeProvider.GetUtcNow();
if (entry.ExpiresAt.HasValue && now >= entry.ExpiresAt.Value)
{
_consents.TryRemove(tenantId, out _);
return Task.FromResult(new ConsentState(
Granted: false,
GrantedBy: null,
GrantedAt: null,
ExpiresAt: null,
DsseDigest: null));
}
return Task.FromResult(new ConsentState(
Granted: true,
GrantedBy: entry.GrantedBy,
GrantedAt: entry.GrantedAt,
ExpiresAt: entry.ExpiresAt,
DsseDigest: entry.DsseDigest));
}
public Task<ConsentProof> GrantConsentAsync(
string tenantId,
string grantedBy,
TimeSpan? ttl = null,
CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var now = _timeProvider.GetUtcNow();
var expiresAt = ttl.HasValue ? now + ttl.Value : (DateTimeOffset?)null;
var payload = JsonSerializer.SerializeToUtf8Bytes(new
{
tenantId,
grantedBy,
grantedAt = now,
expiresAt,
type = "stella.ops/federatedConsent@v1"
});
var digest = ComputeDigest(payload);
var envelope = payload; // Placeholder: real DSSE envelope wraps with signature
var entry = new ConsentEntry(tenantId, grantedBy, now, expiresAt, digest);
_consents[tenantId] = entry;
return Task.FromResult(new ConsentProof(
TenantId: tenantId,
GrantedBy: grantedBy,
GrantedAt: now,
ExpiresAt: expiresAt,
DsseDigest: digest,
Envelope: envelope));
}
public Task RevokeConsentAsync(string tenantId, string revokedBy, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
_consents.TryRemove(tenantId, out _);
return Task.CompletedTask;
}
private static string ComputeDigest(byte[] payload)
{
var hash = SHA256.HashData(payload);
return $"sha256:{Convert.ToHexStringLower(hash)}";
}
private sealed record ConsentEntry(
string TenantId,
string GrantedBy,
DateTimeOffset GrantedAt,
DateTimeOffset? ExpiresAt,
string DsseDigest);
}

View File

@@ -0,0 +1,23 @@
namespace StellaOps.Telemetry.Federation.Consent;
public interface IConsentManager
{
Task<ConsentState> GetConsentStateAsync(string tenantId, CancellationToken ct = default);
Task<ConsentProof> GrantConsentAsync(string tenantId, string grantedBy, TimeSpan? ttl = null, CancellationToken ct = default);
Task RevokeConsentAsync(string tenantId, string revokedBy, CancellationToken ct = default);
}
public sealed record ConsentState(
bool Granted,
string? GrantedBy,
DateTimeOffset? GrantedAt,
DateTimeOffset? ExpiresAt,
string? DsseDigest);
public sealed record ConsentProof(
string TenantId,
string GrantedBy,
DateTimeOffset GrantedAt,
DateTimeOffset? ExpiresAt,
string DsseDigest,
byte[] Envelope);

View File

@@ -0,0 +1,46 @@
namespace StellaOps.Telemetry.Federation;
public sealed class FederatedTelemetryOptions
{
public const string SectionName = "FederatedTelemetry";
/// <summary>
/// Minimum number of distinct artifacts per CVE bucket to avoid suppression.
/// </summary>
public int KAnonymityThreshold { get; set; } = 5;
/// <summary>
/// Total differential privacy epsilon budget per reset period.
/// </summary>
public double EpsilonBudget { get; set; } = 1.0;
/// <summary>
/// How often the privacy budget resets.
/// </summary>
public TimeSpan BudgetResetPeriod { get; set; } = TimeSpan.FromHours(24);
/// <summary>
/// Interval between automated aggregation cycles.
/// </summary>
public TimeSpan AggregationInterval { get; set; } = TimeSpan.FromMinutes(15);
/// <summary>
/// When true, federation operates in sealed mode (no outbound traffic).
/// </summary>
public bool SealedModeEnabled { get; set; }
/// <summary>
/// DSSE predicate type for consent attestation.
/// </summary>
public string ConsentPredicateType { get; set; } = "stella.ops/federatedConsent@v1";
/// <summary>
/// DSSE predicate type for telemetry bundle attestation.
/// </summary>
public string BundlePredicateType { get; set; } = "stella.ops/federatedTelemetry@v1";
/// <summary>
/// Identifier for this site in the federation mesh.
/// </summary>
public string SiteId { get; set; } = "default";
}

View File

@@ -0,0 +1,43 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Telemetry.Federation.Aggregation;
using StellaOps.Telemetry.Federation.Bundles;
using StellaOps.Telemetry.Federation.Consent;
using StellaOps.Telemetry.Federation.Intelligence;
using StellaOps.Telemetry.Federation.Privacy;
using StellaOps.Telemetry.Federation.Sync;
namespace StellaOps.Telemetry.Federation;
public static class FederationServiceCollectionExtensions
{
public static IServiceCollection AddFederatedTelemetry(
this IServiceCollection services,
Action<FederatedTelemetryOptions>? configureOptions = null)
{
ArgumentNullException.ThrowIfNull(services);
services.AddOptions<FederatedTelemetryOptions>()
.Configure(options => configureOptions?.Invoke(options));
services.TryAddSingleton<IPrivacyBudgetTracker, PrivacyBudgetTracker>();
services.TryAddSingleton<ITelemetryAggregator, TelemetryAggregator>();
services.TryAddSingleton<IConsentManager, ConsentManager>();
services.TryAddSingleton<IFederatedTelemetryBundleBuilder, FederatedTelemetryBundleBuilder>();
services.TryAddSingleton<IExploitIntelligenceMerger, ExploitIntelligenceMerger>();
services.TryAddSingleton<FederatedIntelligenceNormalizer>();
services.TryAddSingleton<IEgressPolicyIntegration, EgressPolicyIntegration>();
return services;
}
public static IServiceCollection AddFederatedTelemetrySync(
this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
services.AddHostedService<FederatedTelemetrySyncService>();
return services;
}
}

View File

@@ -0,0 +1,53 @@
using System.Collections.Concurrent;
namespace StellaOps.Telemetry.Federation.Intelligence;
public sealed class ExploitIntelligenceMerger : IExploitIntelligenceMerger
{
private readonly ConcurrentDictionary<string, ExploitIntelligenceEntry> _corpus = new();
private readonly FederatedIntelligenceNormalizer _normalizer;
private readonly TimeProvider _timeProvider;
public ExploitIntelligenceMerger(
FederatedIntelligenceNormalizer normalizer,
TimeProvider? timeProvider = null)
{
_normalizer = normalizer;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public Task<ExploitCorpus> MergeAsync(
IReadOnlyList<ExploitIntelligenceEntry> incoming,
CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
foreach (var entry in incoming)
{
var normalized = _normalizer.Normalize(entry);
var key = $"{normalized.CveId}:{normalized.SourceSiteId}";
_corpus.AddOrUpdate(
key,
normalized,
(_, existing) => normalized.ObservedAt > existing.ObservedAt ? normalized : existing);
}
return GetCorpusAsync(ct);
}
public Task<ExploitCorpus> GetCorpusAsync(CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var entries = _corpus.Values.ToList();
var corpus = new ExploitCorpus(
Entries: entries,
TotalEntries: entries.Count,
UniqueCves: entries.Select(e => e.CveId).Distinct().Count(),
ContributingSites: entries.Select(e => e.SourceSiteId).Distinct().Count(),
LastUpdated: _timeProvider.GetUtcNow());
return Task.FromResult(corpus);
}
}

View File

@@ -0,0 +1,33 @@
using System.Text.RegularExpressions;
namespace StellaOps.Telemetry.Federation.Intelligence;
public sealed partial class FederatedIntelligenceNormalizer
{
[GeneratedRegex(@"^CVE-\d{4}-\d{4,}$", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
private static partial Regex CvePattern();
public ExploitIntelligenceEntry Normalize(ExploitIntelligenceEntry entry)
{
return entry with
{
CveId = NormalizeCveId(entry.CveId),
SourceSiteId = entry.SourceSiteId.Trim().ToLowerInvariant(),
ObservedAt = entry.ObservedAt.ToUniversalTime()
};
}
private static string NormalizeCveId(string cveId)
{
var trimmed = cveId.Trim();
// Ensure uppercase CVE prefix
if (CvePattern().IsMatch(trimmed))
{
return trimmed.ToUpperInvariant();
}
// If it doesn't match the pattern, return as-is (may be a non-CVE identifier)
return trimmed;
}
}

View File

@@ -0,0 +1,25 @@
namespace StellaOps.Telemetry.Federation.Intelligence;
public interface IExploitIntelligenceMerger
{
Task<ExploitCorpus> MergeAsync(
IReadOnlyList<ExploitIntelligenceEntry> incoming,
CancellationToken ct = default);
Task<ExploitCorpus> GetCorpusAsync(CancellationToken ct = default);
}
public sealed record ExploitIntelligenceEntry(
string CveId,
string SourceSiteId,
int ObservationCount,
double NoisyCount,
int ArtifactCount,
DateTimeOffset ObservedAt);
public sealed record ExploitCorpus(
IReadOnlyList<ExploitIntelligenceEntry> Entries,
int TotalEntries,
int UniqueCves,
int ContributingSites,
DateTimeOffset LastUpdated);

View File

@@ -0,0 +1,22 @@
namespace StellaOps.Telemetry.Federation.Privacy;
public interface IPrivacyBudgetTracker
{
double RemainingEpsilon { get; }
double TotalBudget { get; }
bool IsBudgetExhausted { get; }
DateTimeOffset CurrentPeriodStart { get; }
DateTimeOffset NextReset { get; }
bool TrySpend(double epsilon);
void Reset();
PrivacyBudgetSnapshot GetSnapshot();
}
public sealed record PrivacyBudgetSnapshot(
double Remaining,
double Total,
bool Exhausted,
DateTimeOffset PeriodStart,
DateTimeOffset NextReset,
int QueriesThisPeriod,
int SuppressedThisPeriod);

View File

@@ -0,0 +1,137 @@
using Microsoft.Extensions.Options;
namespace StellaOps.Telemetry.Federation.Privacy;
public sealed class PrivacyBudgetTracker : IPrivacyBudgetTracker
{
private readonly FederatedTelemetryOptions _options;
private readonly TimeProvider _timeProvider;
private readonly object _lock = new();
private double _spent;
private int _queriesThisPeriod;
private int _suppressedThisPeriod;
private DateTimeOffset _periodStart;
public PrivacyBudgetTracker(IOptions<FederatedTelemetryOptions> options, TimeProvider? timeProvider = null)
{
_options = options.Value;
_timeProvider = timeProvider ?? TimeProvider.System;
_periodStart = _timeProvider.GetUtcNow();
}
public double RemainingEpsilon
{
get
{
lock (_lock)
{
MaybeResetPeriod();
return Math.Max(0, _options.EpsilonBudget - _spent);
}
}
}
public double TotalBudget => _options.EpsilonBudget;
public bool IsBudgetExhausted
{
get
{
lock (_lock)
{
MaybeResetPeriod();
return _spent >= _options.EpsilonBudget;
}
}
}
public DateTimeOffset CurrentPeriodStart
{
get
{
lock (_lock)
{
MaybeResetPeriod();
return _periodStart;
}
}
}
public DateTimeOffset NextReset
{
get
{
lock (_lock)
{
MaybeResetPeriod();
return _periodStart + _options.BudgetResetPeriod;
}
}
}
public bool TrySpend(double epsilon)
{
if (epsilon <= 0) return false;
lock (_lock)
{
MaybeResetPeriod();
if (_spent + epsilon > _options.EpsilonBudget)
{
_suppressedThisPeriod++;
return false;
}
_spent += epsilon;
_queriesThisPeriod++;
return true;
}
}
public void Reset()
{
lock (_lock)
{
_spent = 0;
_queriesThisPeriod = 0;
_suppressedThisPeriod = 0;
_periodStart = _timeProvider.GetUtcNow();
}
}
public PrivacyBudgetSnapshot GetSnapshot()
{
lock (_lock)
{
MaybeResetPeriod();
return new PrivacyBudgetSnapshot(
Remaining: Math.Max(0, _options.EpsilonBudget - _spent),
Total: _options.EpsilonBudget,
Exhausted: _spent >= _options.EpsilonBudget,
PeriodStart: _periodStart,
NextReset: _periodStart + _options.BudgetResetPeriod,
QueriesThisPeriod: _queriesThisPeriod,
SuppressedThisPeriod: _suppressedThisPeriod);
}
}
private void MaybeResetPeriod()
{
var now = _timeProvider.GetUtcNow();
if (now >= _periodStart + _options.BudgetResetPeriod)
{
_spent = 0;
_queriesThisPeriod = 0;
_suppressedThisPeriod = 0;
_periodStart = now;
}
}
internal static double LaplacianNoise(double sensitivity, double epsilon, Random rng)
{
double u = rng.NextDouble() - 0.5;
return -(sensitivity / epsilon) * Math.Sign(u) * Math.Log(1 - 2 * Math.Abs(u));
}
}

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,44 @@
using Microsoft.Extensions.Logging;
namespace StellaOps.Telemetry.Federation.Sync;
public interface IEgressPolicyIntegration
{
Task<EgressCheckResult> CheckEgressAsync(
string destinationSiteId,
int payloadSizeBytes,
CancellationToken ct = default);
}
public sealed class EgressPolicyIntegration : IEgressPolicyIntegration
{
private readonly ILogger<EgressPolicyIntegration> _logger;
public EgressPolicyIntegration(ILogger<EgressPolicyIntegration> logger)
{
_logger = logger;
}
/// <summary>
/// Checks whether outbound federation traffic is permitted by the platform egress policy.
/// </summary>
public Task<EgressCheckResult> CheckEgressAsync(
string destinationSiteId,
int payloadSizeBytes,
CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
// Placeholder: integrate with IEgressPolicy from AirGap module when available.
// For now, all egress is allowed unless sealed mode is active.
_logger.LogDebug(
"Egress check for destination {DestinationSiteId}, payload {PayloadSize} bytes",
destinationSiteId, payloadSizeBytes);
return Task.FromResult(new EgressCheckResult(
Allowed: true,
Reason: null));
}
}
public sealed record EgressCheckResult(bool Allowed, string? Reason);

View File

@@ -0,0 +1,135 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Telemetry.Federation.Aggregation;
using StellaOps.Telemetry.Federation.Bundles;
using StellaOps.Telemetry.Federation.Consent;
using StellaOps.Telemetry.Federation.Privacy;
namespace StellaOps.Telemetry.Federation.Sync;
public sealed class FederatedTelemetrySyncService : BackgroundService
{
private readonly FederatedTelemetryOptions _options;
private readonly IPrivacyBudgetTracker _budgetTracker;
private readonly ITelemetryAggregator _aggregator;
private readonly IConsentManager _consentManager;
private readonly IFederatedTelemetryBundleBuilder _bundleBuilder;
private readonly IEgressPolicyIntegration _egressPolicy;
private readonly ILogger<FederatedTelemetrySyncService> _logger;
// In-memory fact buffer; production implementation would read from persistent store
private readonly List<TelemetryFact> _factBuffer = new();
private readonly object _bufferLock = new();
public FederatedTelemetrySyncService(
IOptions<FederatedTelemetryOptions> options,
IPrivacyBudgetTracker budgetTracker,
ITelemetryAggregator aggregator,
IConsentManager consentManager,
IFederatedTelemetryBundleBuilder bundleBuilder,
IEgressPolicyIntegration egressPolicy,
ILogger<FederatedTelemetrySyncService> logger)
{
_options = options.Value;
_budgetTracker = budgetTracker;
_aggregator = aggregator;
_consentManager = consentManager;
_bundleBuilder = bundleBuilder;
_egressPolicy = egressPolicy;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Federation sync service started. Interval: {Interval}, SealedMode: {SealedMode}",
_options.AggregationInterval, _options.SealedModeEnabled);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await Task.Delay(_options.AggregationInterval, stoppingToken).ConfigureAwait(false);
await RunSyncCycleAsync(stoppingToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Federation sync cycle failed");
}
}
_logger.LogInformation("Federation sync service stopped");
}
public async Task RunSyncCycleAsync(CancellationToken ct)
{
if (_options.SealedModeEnabled)
{
_logger.LogDebug("Sealed mode active; skipping federation sync cycle");
return;
}
if (_budgetTracker.IsBudgetExhausted)
{
_logger.LogDebug("Privacy budget exhausted; skipping federation sync cycle");
return;
}
// Check consent for the default tenant (placeholder: real implementation iterates tenants)
var consent = await _consentManager.GetConsentStateAsync("default", ct).ConfigureAwait(false);
if (!consent.Granted)
{
_logger.LogDebug("No consent granted; skipping federation sync cycle");
return;
}
// Drain fact buffer
List<TelemetryFact> facts;
lock (_bufferLock)
{
facts = new List<TelemetryFact>(_factBuffer);
_factBuffer.Clear();
}
if (facts.Count == 0)
{
_logger.LogDebug("No telemetry facts to aggregate");
return;
}
// Aggregate
var aggregation = await _aggregator.AggregateAsync(facts, ct).ConfigureAwait(false);
// Build bundle
var consentProof = await _consentManager.GrantConsentAsync("default", "sync-service", null, ct).ConfigureAwait(false);
var bundle = await _bundleBuilder.BuildAsync(aggregation, consentProof, ct).ConfigureAwait(false);
// Check egress policy
var egressCheck = await _egressPolicy.CheckEgressAsync("federation-mesh", bundle.Envelope.Length, ct).ConfigureAwait(false);
if (!egressCheck.Allowed)
{
_logger.LogWarning("Egress blocked: {Reason}", egressCheck.Reason);
return;
}
_logger.LogInformation(
"Federation sync cycle complete. Bundle {BundleId}: {BucketCount} buckets, {Suppressed} suppressed, epsilon spent: {EpsilonSpent:F4}",
bundle.Id, aggregation.Buckets.Count, aggregation.SuppressedBuckets, aggregation.EpsilonSpent);
}
/// <summary>
/// Enqueue a telemetry fact for the next aggregation cycle.
/// </summary>
public void EnqueueFact(TelemetryFact fact)
{
lock (_bufferLock)
{
_factBuffer.Add(fact);
}
}
}