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,132 @@
using StellaOps.Telemetry.Federation.Consent;
namespace StellaOps.Telemetry.Federation.Tests;
public sealed class ConsentManagerTests
{
[Fact]
public async Task Default_consent_state_is_not_granted()
{
var manager = new ConsentManager();
var state = await manager.GetConsentStateAsync("tenant-1");
Assert.False(state.Granted);
Assert.Null(state.GrantedBy);
Assert.Null(state.GrantedAt);
Assert.Null(state.ExpiresAt);
Assert.Null(state.DsseDigest);
}
[Fact]
public async Task Grant_consent_sets_granted_state()
{
var manager = new ConsentManager();
var proof = await manager.GrantConsentAsync("tenant-1", "admin@example.com");
Assert.Equal("tenant-1", proof.TenantId);
Assert.Equal("admin@example.com", proof.GrantedBy);
Assert.NotNull(proof.DsseDigest);
Assert.StartsWith("sha256:", proof.DsseDigest);
Assert.NotEmpty(proof.Envelope);
var state = await manager.GetConsentStateAsync("tenant-1");
Assert.True(state.Granted);
Assert.Equal("admin@example.com", state.GrantedBy);
}
[Fact]
public async Task Revoke_consent_clears_state()
{
var manager = new ConsentManager();
await manager.GrantConsentAsync("tenant-1", "admin@example.com");
await manager.RevokeConsentAsync("tenant-1", "admin@example.com");
var state = await manager.GetConsentStateAsync("tenant-1");
Assert.False(state.Granted);
}
[Fact]
public async Task TTL_expiry_revokes_consent()
{
var fakeTime = new FakeTimeProvider(DateTimeOffset.UtcNow);
var manager = new ConsentManager(fakeTime);
await manager.GrantConsentAsync("tenant-1", "admin@example.com", ttl: TimeSpan.FromHours(1));
var stateBefore = await manager.GetConsentStateAsync("tenant-1");
Assert.True(stateBefore.Granted);
// Advance time past TTL
fakeTime.Advance(TimeSpan.FromHours(2));
var stateAfter = await manager.GetConsentStateAsync("tenant-1");
Assert.False(stateAfter.Granted);
}
[Fact]
public async Task Grant_without_TTL_has_no_expiry()
{
var manager = new ConsentManager();
var proof = await manager.GrantConsentAsync("tenant-1", "admin@example.com");
Assert.Null(proof.ExpiresAt);
var state = await manager.GetConsentStateAsync("tenant-1");
Assert.True(state.Granted);
Assert.Null(state.ExpiresAt);
}
[Fact]
public async Task Multiple_tenants_independent()
{
var manager = new ConsentManager();
await manager.GrantConsentAsync("tenant-1", "admin1@example.com");
await manager.GrantConsentAsync("tenant-2", "admin2@example.com");
await manager.RevokeConsentAsync("tenant-1", "admin1@example.com");
var state1 = await manager.GetConsentStateAsync("tenant-1");
var state2 = await manager.GetConsentStateAsync("tenant-2");
Assert.False(state1.Granted);
Assert.True(state2.Granted);
}
[Fact]
public async Task Grant_overwrites_previous_consent()
{
var manager = new ConsentManager();
var proof1 = await manager.GrantConsentAsync("tenant-1", "admin@example.com");
var proof2 = await manager.GrantConsentAsync("tenant-1", "newadmin@example.com");
Assert.NotEqual(proof1.DsseDigest, proof2.DsseDigest);
var state = await manager.GetConsentStateAsync("tenant-1");
Assert.Equal("newadmin@example.com", state.GrantedBy);
}
}
/// <summary>
/// Simple fake TimeProvider for testing time-dependent behavior.
/// </summary>
internal sealed class FakeTimeProvider : TimeProvider
{
private DateTimeOffset _utcNow;
public FakeTimeProvider(DateTimeOffset start)
{
_utcNow = start;
}
public override DateTimeOffset GetUtcNow() => _utcNow;
public void Advance(TimeSpan duration) => _utcNow += duration;
public void SetUtcNow(DateTimeOffset value) => _utcNow = value;
}

View File

@@ -0,0 +1,132 @@
using Microsoft.Extensions.Options;
using StellaOps.Telemetry.Federation.Aggregation;
using StellaOps.Telemetry.Federation.Bundles;
using StellaOps.Telemetry.Federation.Consent;
namespace StellaOps.Telemetry.Federation.Tests;
public sealed class FederatedTelemetryBundleBuilderTests
{
private static FederatedTelemetryBundleBuilder CreateBuilder(string siteId = "test-site")
{
var options = Options.Create(new FederatedTelemetryOptions
{
SiteId = siteId,
BundlePredicateType = "stella.ops/federatedTelemetry@v1"
});
return new FederatedTelemetryBundleBuilder(options);
}
private static AggregationResult CreateAggregation()
{
return new AggregationResult(
Buckets:
[
new AggregationBucket("CVE-2024-0001", 10, 5, 10.3, false),
new AggregationBucket("CVE-2024-0002", 3, 2, 0, true),
],
TotalFacts: 13,
SuppressedBuckets: 1,
EpsilonSpent: 0.5,
AggregatedAt: DateTimeOffset.UtcNow);
}
private static ConsentProof CreateConsentProof()
{
return new ConsentProof(
TenantId: "tenant-1",
GrantedBy: "admin@example.com",
GrantedAt: DateTimeOffset.UtcNow,
ExpiresAt: null,
DsseDigest: "sha256:abc123",
Envelope: new byte[] { 1, 2, 3 });
}
[Fact]
public async Task Build_creates_bundle_with_correct_site_id()
{
var builder = CreateBuilder("my-site");
var aggregation = CreateAggregation();
var consent = CreateConsentProof();
var bundle = await builder.BuildAsync(aggregation, consent);
Assert.Equal("my-site", bundle.SourceSiteId);
Assert.NotEqual(Guid.Empty, bundle.Id);
}
[Fact]
public async Task Build_includes_consent_digest()
{
var builder = CreateBuilder();
var consent = CreateConsentProof();
var bundle = await builder.BuildAsync(CreateAggregation(), consent);
Assert.Equal(consent.DsseDigest, bundle.ConsentDsseDigest);
}
[Fact]
public async Task Build_produces_valid_dsse_digest()
{
var builder = CreateBuilder();
var bundle = await builder.BuildAsync(CreateAggregation(), CreateConsentProof());
Assert.StartsWith("sha256:", bundle.BundleDsseDigest);
Assert.NotEmpty(bundle.Envelope);
}
[Fact]
public async Task Verify_succeeds_for_unmodified_bundle()
{
var builder = CreateBuilder();
var bundle = await builder.BuildAsync(CreateAggregation(), CreateConsentProof());
var isValid = await builder.VerifyAsync(bundle);
Assert.True(isValid);
}
[Fact]
public async Task Verify_fails_for_tampered_bundle()
{
var builder = CreateBuilder();
var bundle = await builder.BuildAsync(CreateAggregation(), CreateConsentProof());
// Tamper with the envelope
var tampered = bundle with
{
Envelope = new byte[] { 0xFF, 0xFE, 0xFD }
};
var isValid = await builder.VerifyAsync(tampered);
Assert.False(isValid);
}
[Fact]
public async Task Build_includes_aggregation_data()
{
var builder = CreateBuilder();
var aggregation = CreateAggregation();
var bundle = await builder.BuildAsync(aggregation, CreateConsentProof());
Assert.Same(aggregation, bundle.Aggregation);
}
[Fact]
public async Task Build_sets_creation_timestamp()
{
var builder = CreateBuilder();
var before = DateTimeOffset.UtcNow;
var bundle = await builder.BuildAsync(CreateAggregation(), CreateConsentProof());
var after = DateTimeOffset.UtcNow;
Assert.InRange(bundle.CreatedAt, before, after);
}
}

View File

@@ -0,0 +1,357 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
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.Tests;
public sealed class FederationSyncAndIntelligenceTests
{
[Fact]
public void IntelligenceNormalizer_NormalizesCveSiteAndTimestamp()
{
var normalizer = new FederatedIntelligenceNormalizer();
var localObservedAt = new DateTimeOffset(2026, 2, 20, 8, 30, 0, TimeSpan.FromHours(+3));
var normalized = normalizer.Normalize(new ExploitIntelligenceEntry(
CveId: " cve-2026-12345 ",
SourceSiteId: " Site-East ",
ObservationCount: 7,
NoisyCount: 7.2,
ArtifactCount: 3,
ObservedAt: localObservedAt));
Assert.Equal("CVE-2026-12345", normalized.CveId);
Assert.Equal("site-east", normalized.SourceSiteId);
Assert.Equal(localObservedAt.ToUniversalTime(), normalized.ObservedAt);
}
[Fact]
public async Task ExploitIntelligenceMerger_DeduplicatesByCveAndSite_KeepingLatestObservation()
{
var now = new DateTimeOffset(2026, 2, 20, 10, 0, 0, TimeSpan.Zero);
var merger = new ExploitIntelligenceMerger(
new FederatedIntelligenceNormalizer(),
new DeterministicTimeProvider(now));
var older = new ExploitIntelligenceEntry(
CveId: "CVE-2026-1000",
SourceSiteId: "site-a",
ObservationCount: 4,
NoisyCount: 4.1,
ArtifactCount: 2,
ObservedAt: now.AddMinutes(-30));
var newer = older with
{
ObservationCount = 9,
NoisyCount = 9.3,
ArtifactCount = 5,
ObservedAt = now
};
var corpus = await merger.MergeAsync(new[] { older, newer });
Assert.Equal(1, corpus.TotalEntries);
Assert.Equal(1, corpus.UniqueCves);
Assert.Equal(1, corpus.ContributingSites);
Assert.Equal(now, corpus.LastUpdated);
Assert.Equal(9, corpus.Entries[0].ObservationCount);
Assert.Equal(5, corpus.Entries[0].ArtifactCount);
}
[Fact]
public async Task EgressPolicyIntegration_AllowsByDefault()
{
var egress = new EgressPolicyIntegration(NullLogger<EgressPolicyIntegration>.Instance);
var result = await egress.CheckEgressAsync("mesh-eu", 512);
Assert.True(result.Allowed);
Assert.Null(result.Reason);
}
[Fact]
public async Task EgressPolicyIntegration_ThrowsWhenCancellationRequested()
{
var egress = new EgressPolicyIntegration(NullLogger<EgressPolicyIntegration>.Instance);
using var cts = new CancellationTokenSource();
cts.Cancel();
await Assert.ThrowsAsync<OperationCanceledException>(() => egress.CheckEgressAsync("mesh-eu", 128, cts.Token));
}
[Fact]
public async Task SyncCycle_SkipsWhenBudgetExhausted()
{
var budget = new StubBudgetTracker(isBudgetExhausted: true);
var aggregator = new RecordingAggregator();
var consent = new StubConsentManager(granted: true);
var bundles = new RecordingBundleBuilder();
var egress = new StubEgressPolicy(allowed: true);
var service = CreateSyncService(budget, aggregator, consent, bundles, egress);
service.EnqueueFact(CreateFact("CVE-2026-2001"));
await service.RunSyncCycleAsync(CancellationToken.None);
Assert.Equal(0, aggregator.CallCount);
Assert.Equal(0, bundles.BuildCallCount);
Assert.Equal(0, egress.CallCount);
}
[Fact]
public async Task SyncCycle_SkipsWhenConsentMissing()
{
var budget = new StubBudgetTracker(isBudgetExhausted: false);
var aggregator = new RecordingAggregator();
var consent = new StubConsentManager(granted: false);
var bundles = new RecordingBundleBuilder();
var egress = new StubEgressPolicy(allowed: true);
var service = CreateSyncService(budget, aggregator, consent, bundles, egress);
service.EnqueueFact(CreateFact("CVE-2026-2002"));
await service.RunSyncCycleAsync(CancellationToken.None);
Assert.Equal(0, aggregator.CallCount);
Assert.Equal(0, bundles.BuildCallCount);
Assert.Equal(0, egress.CallCount);
}
[Fact]
public async Task SyncCycle_AggregatesBuildsAndChecksEgress_WhenEligible()
{
var budget = new StubBudgetTracker(isBudgetExhausted: false);
var aggregator = new RecordingAggregator();
var consent = new StubConsentManager(granted: true);
var bundles = new RecordingBundleBuilder();
var egress = new StubEgressPolicy(allowed: true);
var service = CreateSyncService(budget, aggregator, consent, bundles, egress);
service.EnqueueFact(CreateFact("CVE-2026-2003"));
await service.RunSyncCycleAsync(CancellationToken.None);
Assert.Equal(1, aggregator.CallCount);
Assert.Equal(1, bundles.BuildCallCount);
Assert.Equal(1, egress.CallCount);
Assert.Equal(1, consent.GrantCallCount);
Assert.True(egress.LastPayloadSizeBytes > 0);
}
[Fact]
public async Task SyncCycle_WhenEgressBlocked_StillRunsAggregationAndBuild()
{
var budget = new StubBudgetTracker(isBudgetExhausted: false);
var aggregator = new RecordingAggregator();
var consent = new StubConsentManager(granted: true);
var bundles = new RecordingBundleBuilder();
var egress = new StubEgressPolicy(allowed: false);
var service = CreateSyncService(budget, aggregator, consent, bundles, egress);
service.EnqueueFact(CreateFact("CVE-2026-2004"));
await service.RunSyncCycleAsync(CancellationToken.None);
Assert.Equal(1, aggregator.CallCount);
Assert.Equal(1, bundles.BuildCallCount);
Assert.Equal(1, egress.CallCount);
}
private static FederatedTelemetrySyncService CreateSyncService(
IPrivacyBudgetTracker budget,
ITelemetryAggregator aggregator,
IConsentManager consent,
IFederatedTelemetryBundleBuilder bundles,
IEgressPolicyIntegration egress)
{
var options = Options.Create(new FederatedTelemetryOptions
{
SiteId = "site-test",
SealedModeEnabled = false,
AggregationInterval = TimeSpan.FromMilliseconds(25)
});
return new FederatedTelemetrySyncService(
options,
budget,
aggregator,
consent,
bundles,
egress,
NullLogger<FederatedTelemetrySyncService>.Instance);
}
private static TelemetryFact CreateFact(string cveId)
{
return new TelemetryFact(
ArtifactDigest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
CveId: cveId,
SymbolPath: "/usr/lib/libcrypto.so",
Exploited: true,
ObservedAt: new DateTimeOffset(2026, 2, 20, 10, 0, 0, TimeSpan.Zero));
}
private sealed class DeterministicTimeProvider : TimeProvider
{
private readonly DateTimeOffset _now;
public DeterministicTimeProvider(DateTimeOffset now)
{
_now = now;
}
public override DateTimeOffset GetUtcNow() => _now;
}
private sealed class StubBudgetTracker : IPrivacyBudgetTracker
{
public StubBudgetTracker(bool isBudgetExhausted)
{
IsBudgetExhausted = isBudgetExhausted;
}
public double RemainingEpsilon => IsBudgetExhausted ? 0 : 1;
public double TotalBudget => 1;
public bool IsBudgetExhausted { get; }
public DateTimeOffset CurrentPeriodStart => new(2026, 2, 20, 0, 0, 0, TimeSpan.Zero);
public DateTimeOffset NextReset => CurrentPeriodStart.AddHours(24);
public bool TrySpend(double epsilon) => !IsBudgetExhausted && epsilon > 0;
public void Reset() { }
public PrivacyBudgetSnapshot GetSnapshot()
{
return new PrivacyBudgetSnapshot(
Remaining: RemainingEpsilon,
Total: TotalBudget,
Exhausted: IsBudgetExhausted,
PeriodStart: CurrentPeriodStart,
NextReset: NextReset,
QueriesThisPeriod: 0,
SuppressedThisPeriod: IsBudgetExhausted ? 1 : 0);
}
}
private sealed class RecordingAggregator : ITelemetryAggregator
{
public int CallCount { get; private set; }
public Task<AggregationResult> AggregateAsync(IReadOnlyList<TelemetryFact> facts, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
CallCount++;
var bucket = new AggregationBucket(
CveId: facts[0].CveId,
ObservationCount: facts.Count,
ArtifactCount: facts.Select(static fact => fact.ArtifactDigest).Distinct().Count(),
NoisyCount: facts.Count,
Suppressed: false);
return Task.FromResult(new AggregationResult(
Buckets: new[] { bucket },
TotalFacts: facts.Count,
SuppressedBuckets: 0,
EpsilonSpent: 0.1,
AggregatedAt: new DateTimeOffset(2026, 2, 20, 10, 5, 0, TimeSpan.Zero)));
}
}
private sealed class StubConsentManager : IConsentManager
{
private bool _granted;
public StubConsentManager(bool granted)
{
_granted = granted;
}
public int GrantCallCount { get; private set; }
public Task<ConsentState> GetConsentStateAsync(string tenantId, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
return Task.FromResult(new ConsentState(
Granted: _granted,
GrantedBy: _granted ? "tester" : null,
GrantedAt: _granted ? new DateTimeOffset(2026, 2, 20, 9, 0, 0, TimeSpan.Zero) : null,
ExpiresAt: null,
DsseDigest: _granted ? "sha256:consent" : null));
}
public Task<ConsentProof> GrantConsentAsync(string tenantId, string grantedBy, TimeSpan? ttl = null, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
GrantCallCount++;
_granted = true;
return Task.FromResult(new ConsentProof(
TenantId: tenantId,
GrantedBy: grantedBy,
GrantedAt: new DateTimeOffset(2026, 2, 20, 9, 0, 0, TimeSpan.Zero),
ExpiresAt: ttl.HasValue ? new DateTimeOffset(2026, 2, 20, 9, 0, 0, TimeSpan.Zero).Add(ttl.Value) : null,
DsseDigest: "sha256:consent-proof",
Envelope: [0x10, 0x11, 0x12]));
}
public Task RevokeConsentAsync(string tenantId, string revokedBy, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
_granted = false;
return Task.CompletedTask;
}
}
private sealed class RecordingBundleBuilder : IFederatedTelemetryBundleBuilder
{
public int BuildCallCount { get; private set; }
public Task<FederatedBundle> BuildAsync(AggregationResult aggregation, ConsentProof consent, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
BuildCallCount++;
return Task.FromResult(new FederatedBundle(
Id: Guid.Parse("2f4ad049-5b29-4f51-b022-ec0735630f38"),
SourceSiteId: "site-test",
Aggregation: aggregation,
ConsentDsseDigest: consent.DsseDigest,
BundleDsseDigest: "sha256:bundle-proof",
Envelope: [0x01, 0x02, 0x03, 0x04],
CreatedAt: new DateTimeOffset(2026, 2, 20, 10, 6, 0, TimeSpan.Zero)));
}
public Task<bool> VerifyAsync(FederatedBundle bundle, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
return Task.FromResult(true);
}
}
private sealed class StubEgressPolicy : IEgressPolicyIntegration
{
private readonly bool _allowed;
public StubEgressPolicy(bool allowed)
{
_allowed = allowed;
}
public int CallCount { get; private set; }
public int LastPayloadSizeBytes { get; private set; }
public Task<EgressCheckResult> CheckEgressAsync(
string destinationSiteId,
int payloadSizeBytes,
CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
CallCount++;
LastPayloadSizeBytes = payloadSizeBytes;
return Task.FromResult(new EgressCheckResult(
Allowed: _allowed,
Reason: _allowed ? null : "egress_policy_blocked"));
}
}
}

View File

@@ -0,0 +1,120 @@
using Microsoft.Extensions.Options;
using StellaOps.Telemetry.Federation.Privacy;
namespace StellaOps.Telemetry.Federation.Tests;
public sealed class PrivacyBudgetTrackerTests
{
private static IOptions<FederatedTelemetryOptions> DefaultOptions(
double epsilon = 1.0,
TimeSpan? resetPeriod = null)
{
return Options.Create(new FederatedTelemetryOptions
{
EpsilonBudget = epsilon,
BudgetResetPeriod = resetPeriod ?? TimeSpan.FromHours(24)
});
}
[Fact]
public void Initial_budget_equals_total()
{
var tracker = new PrivacyBudgetTracker(DefaultOptions(epsilon: 2.0));
Assert.Equal(2.0, tracker.TotalBudget);
Assert.Equal(2.0, tracker.RemainingEpsilon);
Assert.False(tracker.IsBudgetExhausted);
}
[Fact]
public void TrySpend_reduces_remaining_epsilon()
{
var tracker = new PrivacyBudgetTracker(DefaultOptions(epsilon: 1.0));
Assert.True(tracker.TrySpend(0.3));
Assert.Equal(0.7, tracker.RemainingEpsilon, precision: 10);
}
[Fact]
public void TrySpend_rejects_when_budget_exhausted()
{
var tracker = new PrivacyBudgetTracker(DefaultOptions(epsilon: 0.5));
Assert.True(tracker.TrySpend(0.5));
Assert.False(tracker.TrySpend(0.1));
Assert.True(tracker.IsBudgetExhausted);
}
[Fact]
public void TrySpend_rejects_negative_or_zero_epsilon()
{
var tracker = new PrivacyBudgetTracker(DefaultOptions());
Assert.False(tracker.TrySpend(0));
Assert.False(tracker.TrySpend(-0.5));
}
[Fact]
public void Reset_restores_full_budget()
{
var tracker = new PrivacyBudgetTracker(DefaultOptions(epsilon: 1.0));
tracker.TrySpend(0.8);
Assert.Equal(0.2, tracker.RemainingEpsilon, precision: 10);
tracker.Reset();
Assert.Equal(1.0, tracker.RemainingEpsilon);
Assert.False(tracker.IsBudgetExhausted);
}
[Fact]
public void Snapshot_tracks_queries_and_suppressed_counts()
{
var tracker = new PrivacyBudgetTracker(DefaultOptions(epsilon: 0.5));
tracker.TrySpend(0.2); // success
tracker.TrySpend(0.2); // success
tracker.TrySpend(0.2); // fails — over budget
var snapshot = tracker.GetSnapshot();
Assert.Equal(2, snapshot.QueriesThisPeriod);
Assert.Equal(1, snapshot.SuppressedThisPeriod);
Assert.True(snapshot.Exhausted);
}
[Fact]
public void LaplacianNoise_produces_finite_values()
{
var rng = new Random(42);
for (int i = 0; i < 1000; i++)
{
var noise = PrivacyBudgetTracker.LaplacianNoise(1.0, 0.5, rng);
Assert.True(double.IsFinite(noise));
}
}
[Fact]
public void LaplacianNoise_is_deterministic_with_fixed_seed()
{
var rng1 = new Random(12345);
var rng2 = new Random(12345);
var noise1 = PrivacyBudgetTracker.LaplacianNoise(1.0, 1.0, rng1);
var noise2 = PrivacyBudgetTracker.LaplacianNoise(1.0, 1.0, rng2);
Assert.Equal(noise1, noise2);
}
[Fact]
public void LaplacianNoise_scales_with_sensitivity_and_epsilon()
{
var rng = new Random(42);
var samples = Enumerable.Range(0, 10000)
.Select(_ => PrivacyBudgetTracker.LaplacianNoise(1.0, 1.0, rng))
.ToList();
// Mean should be approximately 0 for large sample
var mean = samples.Average();
Assert.True(Math.Abs(mean) < 0.1, $"Mean {mean} too far from 0");
}
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
<ConcelierTestingPath></ConcelierTestingPath>
<ConcelierSharedTestsPath></ConcelierSharedTestsPath>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Remove="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Remove="Microsoft.Extensions.TimeProvider.Testing" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Telemetry.Federation\StellaOps.Telemetry.Federation.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="StellaOps.TestKit" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,150 @@
using Microsoft.Extensions.Options;
using StellaOps.Telemetry.Federation.Aggregation;
using StellaOps.Telemetry.Federation.Privacy;
namespace StellaOps.Telemetry.Federation.Tests;
public sealed class TelemetryAggregatorTests
{
private static (TelemetryAggregator aggregator, PrivacyBudgetTracker budget) CreateAggregator(
int kThreshold = 3,
double epsilon = 10.0,
int seed = 42)
{
var options = Options.Create(new FederatedTelemetryOptions
{
KAnonymityThreshold = kThreshold,
EpsilonBudget = epsilon
});
var budget = new PrivacyBudgetTracker(options);
var rng = new Random(seed);
var aggregator = new TelemetryAggregator(options, budget, rng: rng);
return (aggregator, budget);
}
private static List<TelemetryFact> CreateFacts(string cveId, int distinctArtifacts, int observationsPerArtifact = 1)
{
var facts = new List<TelemetryFact>();
for (int a = 0; a < distinctArtifacts; a++)
{
for (int o = 0; o < observationsPerArtifact; o++)
{
facts.Add(new TelemetryFact(
ArtifactDigest: $"sha256:artifact{a:D4}",
CveId: cveId,
SymbolPath: $"lib/module{a}.dll",
Exploited: o % 2 == 0,
ObservedAt: DateTimeOffset.UtcNow));
}
}
return facts;
}
[Fact]
public async Task Suppresses_buckets_below_k_anonymity_threshold()
{
var (aggregator, _) = CreateAggregator(kThreshold: 5);
// CVE-2024-0001 has only 3 distinct artifacts (below k=5)
var facts = CreateFacts("CVE-2024-0001", distinctArtifacts: 3, observationsPerArtifact: 2);
var result = await aggregator.AggregateAsync(facts);
Assert.Single(result.Buckets);
Assert.True(result.Buckets[0].Suppressed);
Assert.Equal(1, result.SuppressedBuckets);
Assert.Equal(0, result.Buckets[0].NoisyCount);
}
[Fact]
public async Task Passes_buckets_meeting_k_anonymity_threshold()
{
var (aggregator, _) = CreateAggregator(kThreshold: 3);
// CVE-2024-0002 has 5 distinct artifacts (above k=3)
var facts = CreateFacts("CVE-2024-0002", distinctArtifacts: 5);
var result = await aggregator.AggregateAsync(facts);
Assert.Single(result.Buckets);
Assert.False(result.Buckets[0].Suppressed);
Assert.True(result.Buckets[0].NoisyCount > 0);
}
[Fact]
public async Task Noisy_count_differs_from_true_count()
{
var (aggregator, _) = CreateAggregator(kThreshold: 2, epsilon: 0.5);
var facts = CreateFacts("CVE-2024-0003", distinctArtifacts: 10, observationsPerArtifact: 3);
var result = await aggregator.AggregateAsync(facts);
var bucket = result.Buckets.Single(b => !b.Suppressed);
// With low epsilon, noise is large; noisy count should differ from true count (30)
Assert.NotEqual(30.0, bucket.NoisyCount);
}
[Fact]
public async Task Deterministic_output_with_fixed_seed()
{
var (agg1, _) = CreateAggregator(seed: 99);
var (agg2, _) = CreateAggregator(seed: 99);
var facts = CreateFacts("CVE-2024-0004", distinctArtifacts: 5);
var result1 = await agg1.AggregateAsync(facts);
var result2 = await agg2.AggregateAsync(facts);
Assert.Equal(
result1.Buckets[0].NoisyCount,
result2.Buckets[0].NoisyCount);
}
[Fact]
public async Task Spends_epsilon_from_budget()
{
var (aggregator, budget) = CreateAggregator(kThreshold: 2, epsilon: 1.0);
var facts = CreateFacts("CVE-2024-0005", distinctArtifacts: 5);
var before = budget.RemainingEpsilon;
await aggregator.AggregateAsync(facts);
var after = budget.RemainingEpsilon;
Assert.True(after < before, "Budget should have been reduced after aggregation");
}
[Fact]
public async Task Budget_exhaustion_suppresses_remaining_buckets()
{
var (aggregator, budget) = CreateAggregator(kThreshold: 1, epsilon: 0.01);
// Create many CVE groups to exhaust the tiny budget
var facts = new List<TelemetryFact>();
for (int i = 0; i < 100; i++)
{
facts.AddRange(CreateFacts($"CVE-2024-{i:D4}", distinctArtifacts: 2));
}
var result = await aggregator.AggregateAsync(facts);
Assert.True(result.SuppressedBuckets > 0, "Some buckets should be suppressed due to budget exhaustion");
}
[Fact]
public async Task Multiple_cve_groups_aggregated_separately()
{
var (aggregator, _) = CreateAggregator(kThreshold: 2);
var facts = new List<TelemetryFact>();
facts.AddRange(CreateFacts("CVE-2024-1000", distinctArtifacts: 5));
facts.AddRange(CreateFacts("CVE-2024-2000", distinctArtifacts: 5));
var result = await aggregator.AggregateAsync(facts);
Assert.Equal(2, result.Buckets.Count);
Assert.Equal(10, result.TotalFacts);
}
}