doctor enhancements, setup, enhancements, ui functionality and design consolidation and , test projects fixes , product advisory attestation/rekor and delta verfications enhancements

This commit is contained in:
master
2026-01-19 09:02:59 +02:00
parent 8c4bf54aed
commit 17419ba7c4
809 changed files with 170738 additions and 12244 deletions

View File

@@ -0,0 +1,625 @@
// -----------------------------------------------------------------------------
// TetragonPerformanceBenchmarks.cs
// Sprint: SPRINT_20260118_019_Infra_tetragon_integration
// Task: TASK-019-009 - Create performance benchmarks
// Description: Performance benchmarks for Tetragon runtime instrumentation
// -----------------------------------------------------------------------------
using System.Diagnostics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Loggers;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace StellaOps.RuntimeInstrumentation.Tetragon.Tests.Benchmarks;
/// <summary>
/// Performance benchmarks for Tetragon runtime instrumentation.
///
/// Target KPIs (from runtime-agents-architecture.md):
/// - CPU overhead on monitored workloads: &lt;5%
/// - Memory overhead of agent: &lt;100MB
/// - Capture latency (P95): &lt;100ms
/// - Throughput: &gt;10,000 events/second
/// </summary>
[Config(typeof(TetragonBenchmarkConfig))]
[MemoryDiagnoser]
[RankColumn]
public class TetragonPerformanceBenchmarks
{
private TetragonPrivacyFilter _privacyFilter = null!;
private TetragonHotSymbolBridge _hotSymbolBridge = null!;
private TetragonFrameCanonicalizer _frameCanonicalizer = null!;
private Mock<IHotSymbolRepository> _mockRepository = null!;
private Mock<ISymbolResolver> _mockSymbolResolver = null!;
private Mock<IBuildIdResolver> _mockBuildIdResolver = null!;
private List<TetragonEvent> _testEvents = null!;
private List<RuntimeCallEvent> _testRuntimeEvents = null!;
private List<TetragonStackFrame> _testFrames = null!;
[GlobalSetup]
public void Setup()
{
_mockRepository = new Mock<IHotSymbolRepository>();
_mockRepository.Setup(r => r.IngestBatchAsync(It.IsAny<IEnumerable<SymbolObservation>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(0);
_mockSymbolResolver = new Mock<ISymbolResolver>();
_mockSymbolResolver.Setup(r => r.ResolveAsync(It.IsAny<ulong>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ResolvedSymbol { Name = "test_function", Confidence = 0.9 });
_mockBuildIdResolver = new Mock<IBuildIdResolver>();
_mockBuildIdResolver.Setup(r => r.ResolveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync("abc123def456");
var privacyOptions = Options.Create(new TetragonPrivacyOptions
{
RedactArguments = true,
UseDefaultRedactionPatterns = true,
SymbolIdOnlyMode = false
});
_privacyFilter = new TetragonPrivacyFilter(privacyOptions, Mock.Of<ILogger<TetragonPrivacyFilter>>());
var bridgeOptions = Options.Create(new TetragonHotSymbolBridgeOptions
{
AggregationWindowSeconds = 60,
MinConfidenceThreshold = 0.2
});
_hotSymbolBridge = new TetragonHotSymbolBridge(_mockRepository.Object, bridgeOptions, Mock.Of<ILogger<TetragonHotSymbolBridge>>());
var canonicalizerOptions = Options.Create(new TetragonCanonicalizerOptions());
_frameCanonicalizer = new TetragonFrameCanonicalizer(
_mockSymbolResolver.Object,
_mockBuildIdResolver.Object,
canonicalizerOptions,
Mock.Of<ILogger<TetragonFrameCanonicalizer>>());
// Generate test data
_testEvents = GenerateTestEvents(10000);
_testRuntimeEvents = GenerateRuntimeCallEvents(10000);
_testFrames = GenerateTestFrames(1000);
}
/// <summary>
/// Measures privacy filter throughput.
/// Target: &gt;10,000 events/second
/// </summary>
[Benchmark(Baseline = true)]
public int PrivacyFilter_SingleEvent()
{
var count = 0;
foreach (var evt in _testEvents)
{
var filtered = _privacyFilter.Filter(evt);
if (filtered != null) count++;
}
return count;
}
/// <summary>
/// Measures privacy filter with argument redaction.
/// Target: &lt;10% overhead vs baseline
/// </summary>
[Benchmark]
public int PrivacyFilter_WithRedaction()
{
var count = 0;
foreach (var evt in _testEvents)
{
if (evt.Args != null)
{
// Ensure events have args for redaction testing
evt.Args = new List<object> { "password=secret123", "normal_arg" };
}
var filtered = _privacyFilter.Filter(evt);
if (filtered != null) count++;
}
return count;
}
/// <summary>
/// Measures hot symbol bridge recording throughput.
/// Target: &gt;10,000 events/second
/// </summary>
[Benchmark]
public async Task<int> HotSymbolBridge_RecordObservations()
{
await _hotSymbolBridge.RecordObservationsAsync("sha256:test123", _testRuntimeEvents);
return _testRuntimeEvents.Count;
}
/// <summary>
/// Measures frame canonicalization latency.
/// Target: &lt;1ms per frame
/// </summary>
[Benchmark]
public async Task<int> FrameCanonicalizer_CanonicalizeBatch()
{
var count = 0;
await foreach (var frame in _frameCanonicalizer.CanonicalizeBatchAsync(_testFrames, "/usr/bin/app"))
{
count++;
}
return count;
}
/// <summary>
/// Measures function ID computation speed.
/// Target: &lt;0.1ms per call
/// </summary>
[Benchmark]
public int FunctionIdComputation()
{
var count = 0;
foreach (var frame in _testFrames)
{
var id = _frameCanonicalizer.ComputeFunctionId("abc123", frame.Symbol ?? "func", frame.Offset);
if (!string.IsNullOrEmpty(id)) count++;
}
return count;
}
/// <summary>
/// Measures demangling throughput.
/// Target: &gt;100,000 symbols/second
/// </summary>
[Benchmark]
public int Demangling_Throughput()
{
var symbols = new[] { "_ZN4test8functionEv", "_RNvCs123_4test8function", "go.test.function", "normal_func" };
var count = 0;
for (var i = 0; i < 10000; i++)
{
foreach (var symbol in symbols)
{
var demangled = _frameCanonicalizer.Demangle(symbol);
if (!string.IsNullOrEmpty(demangled)) count++;
}
}
return count;
}
private static List<TetragonEvent> GenerateTestEvents(int count)
{
var events = new List<TetragonEvent>(count);
var random = new Random(42);
for (var i = 0; i < count; i++)
{
events.Add(new TetragonEvent
{
Type = (TetragonEventType)(random.Next(4)),
Time = DateTimeOffset.UtcNow.AddMilliseconds(-random.Next(10000)),
Process = new TetragonProcess
{
Pid = random.Next(1000, 50000),
Tid = random.Next(1000, 50000),
Pod = new TetragonPod { Namespace = "stella-ops" }
},
StackTrace = new TetragonStackTrace
{
Frames = Enumerable.Range(0, random.Next(3, 10))
.Select(_ => new TetragonStackFrame
{
Address = (ulong)random.Next(0x1000, int.MaxValue),
Offset = (ulong)random.Next(0, 1000),
Symbol = $"func_{random.Next(100)}",
Module = "/usr/lib/libtest.so"
})
.ToList()
}
});
}
return events;
}
private static List<RuntimeCallEvent> GenerateRuntimeCallEvents(int count)
{
var events = new List<RuntimeCallEvent>(count);
var random = new Random(42);
for (var i = 0; i < count; i++)
{
events.Add(new RuntimeCallEvent
{
EventId = Guid.NewGuid(),
Timestamp = DateTimeOffset.UtcNow.AddMilliseconds(-random.Next(10000)),
Source = RuntimeEventSource.Tetragon,
ProcessId = random.Next(1000, 50000),
ContainerId = $"container-{random.Next(100):D4}",
Frames = Enumerable.Range(0, random.Next(3, 10))
.Select(_ => new CanonicalStackFrame
{
Address = (ulong)random.Next(0x1000, int.MaxValue),
Symbol = $"func_{random.Next(100)}",
Confidence = random.NextDouble() * 0.5 + 0.5
})
.ToList()
});
}
return events;
}
private static List<TetragonStackFrame> GenerateTestFrames(int count)
{
var frames = new List<TetragonStackFrame>(count);
var random = new Random(42);
for (var i = 0; i < count; i++)
{
frames.Add(new TetragonStackFrame
{
Address = (ulong)random.Next(0x1000, int.MaxValue),
Offset = (ulong)random.Next(0, 1000),
Symbol = $"func_{random.Next(100)}",
Module = "/usr/lib/libtest.so",
Flags = (StackFrameFlags)random.Next(4)
});
}
return frames;
}
}
/// <summary>
/// Unit tests for Tetragon performance thresholds.
/// These tests fail CI if benchmarks regress.
/// </summary>
public sealed class TetragonPerformanceTests
{
private readonly Mock<ILogger<TetragonPrivacyFilter>> _mockPrivacyLogger;
private readonly Mock<ILogger<TetragonHotSymbolBridge>> _mockBridgeLogger;
private readonly Mock<ILogger<TetragonFrameCanonicalizer>> _mockCanonicalizerLogger;
private readonly Mock<IHotSymbolRepository> _mockRepository;
private readonly Mock<ISymbolResolver> _mockSymbolResolver;
private readonly Mock<IBuildIdResolver> _mockBuildIdResolver;
public TetragonPerformanceTests()
{
_mockPrivacyLogger = new Mock<ILogger<TetragonPrivacyFilter>>();
_mockBridgeLogger = new Mock<ILogger<TetragonHotSymbolBridge>>();
_mockCanonicalizerLogger = new Mock<ILogger<TetragonFrameCanonicalizer>>();
_mockRepository = new Mock<IHotSymbolRepository>();
_mockRepository.Setup(r => r.IngestBatchAsync(It.IsAny<IEnumerable<SymbolObservation>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(0);
_mockSymbolResolver = new Mock<ISymbolResolver>();
_mockSymbolResolver.Setup(r => r.ResolveAsync(It.IsAny<ulong>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ResolvedSymbol { Name = "test", Confidence = 0.9 });
_mockBuildIdResolver = new Mock<IBuildIdResolver>();
_mockBuildIdResolver.Setup(r => r.ResolveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync("abc123");
}
[Fact]
public void PrivacyFilter_ShouldProcess10000EventsInUnder1Second()
{
// Arrange
var options = Options.Create(new TetragonPrivacyOptions { RedactArguments = true, UseDefaultRedactionPatterns = true });
var filter = new TetragonPrivacyFilter(options, _mockPrivacyLogger.Object);
var events = GenerateTestEvents(10000);
// Act
var sw = Stopwatch.StartNew();
var count = 0;
foreach (var evt in events)
{
if (filter.Filter(evt) != null) count++;
}
sw.Stop();
// Assert
sw.ElapsedMilliseconds.Should().BeLessThan(1000,
"Privacy filter should process 10,000 events in under 1 second (target: >10,000 events/sec)");
count.Should().BeGreaterThan(0);
}
[Fact]
public void PrivacyFilter_SingleEventLatency_ShouldBeUnder1ms()
{
// Arrange
var options = Options.Create(new TetragonPrivacyOptions { RedactArguments = true, UseDefaultRedactionPatterns = true });
var filter = new TetragonPrivacyFilter(options, _mockPrivacyLogger.Object);
var evt = GenerateTestEvents(1)[0];
// Warm up
filter.Filter(evt);
// Act - measure multiple iterations for accuracy
var sw = Stopwatch.StartNew();
for (var i = 0; i < 1000; i++)
{
filter.Filter(evt);
}
sw.Stop();
var avgMicroseconds = sw.Elapsed.TotalMicroseconds / 1000;
// Assert - should average under 100 microseconds per event
avgMicroseconds.Should().BeLessThan(100,
"Privacy filter single event latency should be under 100 microseconds");
}
[Fact]
public async Task HotSymbolBridge_ShouldProcessBatchInUnder100ms()
{
// Arrange
var options = Options.Create(new TetragonHotSymbolBridgeOptions { AggregationWindowSeconds = 60 });
var bridge = new TetragonHotSymbolBridge(_mockRepository.Object, options, _mockBridgeLogger.Object);
var events = GenerateRuntimeCallEvents(1000);
// Act
var sw = Stopwatch.StartNew();
await bridge.RecordObservationsAsync("sha256:test", events);
sw.Stop();
// Assert
sw.ElapsedMilliseconds.Should().BeLessThan(100,
"Hot symbol bridge should process 1000 events batch in under 100ms");
}
[Fact]
public async Task FrameCanonicalizer_ShouldProcessFrameInUnder10ms()
{
// Arrange
var options = Options.Create(new TetragonCanonicalizerOptions());
var canonicalizer = new TetragonFrameCanonicalizer(
_mockSymbolResolver.Object,
_mockBuildIdResolver.Object,
options,
_mockCanonicalizerLogger.Object);
var frame = new TetragonStackFrame
{
Address = 0x7FFF12345678,
Offset = 0x100,
Symbol = "test_function",
Module = "/usr/lib/libtest.so"
};
// Warm up
await canonicalizer.CanonicalizeAsync(frame, null);
// Act
var sw = Stopwatch.StartNew();
for (var i = 0; i < 100; i++)
{
await canonicalizer.CanonicalizeAsync(frame, null);
}
sw.Stop();
var avgMs = sw.ElapsedMilliseconds / 100.0;
// Assert
avgMs.Should().BeLessThan(10,
"Frame canonicalization should complete in under 10ms per frame");
}
[Fact]
public void FunctionIdComputation_ShouldCompleteInUnder100Microseconds()
{
// Arrange
var options = Options.Create(new TetragonCanonicalizerOptions());
var canonicalizer = new TetragonFrameCanonicalizer(
_mockSymbolResolver.Object,
_mockBuildIdResolver.Object,
options,
_mockCanonicalizerLogger.Object);
// Act
var sw = Stopwatch.StartNew();
for (var i = 0; i < 10000; i++)
{
canonicalizer.ComputeFunctionId("abc123def456", "test_function", 0x100);
}
sw.Stop();
var avgMicroseconds = sw.Elapsed.TotalMicroseconds / 10000;
// Assert
avgMicroseconds.Should().BeLessThan(100,
"Function ID computation should complete in under 100 microseconds");
}
[Fact]
public void Demangling_ShouldProcess100000SymbolsInUnder1Second()
{
// Arrange
var options = Options.Create(new TetragonCanonicalizerOptions());
var canonicalizer = new TetragonFrameCanonicalizer(
_mockSymbolResolver.Object,
_mockBuildIdResolver.Object,
options,
_mockCanonicalizerLogger.Object);
var symbols = new[] { "_ZN4test8functionEv", "_RNvCs123_4test8function", "go.test.function", "normal" };
// Act
var sw = Stopwatch.StartNew();
for (var i = 0; i < 25000; i++)
{
foreach (var symbol in symbols)
{
canonicalizer.Demangle(symbol);
}
}
sw.Stop();
// Assert
sw.ElapsedMilliseconds.Should().BeLessThan(1000,
"Demangling should process 100,000 symbols in under 1 second");
}
[Fact]
public void MemoryOverhead_ShouldBeUnder100MB()
{
// Arrange - create components with large data sets
var options = Options.Create(new TetragonPrivacyOptions { RedactArguments = true });
var filter = new TetragonPrivacyFilter(options, _mockPrivacyLogger.Object);
var events = GenerateTestEvents(100000);
// Act - measure memory before and after processing
GC.Collect();
GC.WaitForPendingFinalizers();
var memoryBefore = GC.GetTotalMemory(true);
var results = events.Select(e => filter.Filter(e)).Where(e => e != null).ToList();
GC.Collect();
GC.WaitForPendingFinalizers();
var memoryAfter = GC.GetTotalMemory(true);
var memoryUsedMB = (memoryAfter - memoryBefore) / (1024.0 * 1024.0);
// Assert - allow for reasonable overhead during processing
memoryUsedMB.Should().BeLessThan(100,
"Memory overhead during processing should be under 100MB for 100K events");
results.Should().NotBeEmpty();
}
[Fact]
public void CaptureLatency_P95_ShouldBeUnder100ms()
{
// Arrange
var options = Options.Create(new TetragonPrivacyOptions());
var filter = new TetragonPrivacyFilter(options, _mockPrivacyLogger.Object);
var events = GenerateTestEvents(1000);
var latencies = new List<double>(1000);
// Act - measure individual event processing latencies
foreach (var evt in events)
{
var sw = Stopwatch.StartNew();
filter.Filter(evt);
sw.Stop();
latencies.Add(sw.Elapsed.TotalMilliseconds);
}
// Calculate P95
latencies.Sort();
var p95Index = (int)(latencies.Count * 0.95);
var p95Latency = latencies[p95Index];
// Assert
p95Latency.Should().BeLessThan(100,
"Capture latency P95 should be under 100ms");
}
private static List<TetragonEvent> GenerateTestEvents(int count)
{
var events = new List<TetragonEvent>(count);
var random = new Random(42);
for (var i = 0; i < count; i++)
{
events.Add(new TetragonEvent
{
Type = (TetragonEventType)(random.Next(4)),
Time = DateTimeOffset.UtcNow.AddMilliseconds(-random.Next(10000)),
Process = new TetragonProcess
{
Pid = random.Next(1000, 50000),
Tid = random.Next(1000, 50000),
Pod = new TetragonPod { Namespace = "stella-ops" }
},
StackTrace = new TetragonStackTrace
{
Frames = Enumerable.Range(0, random.Next(3, 10))
.Select(_ => new TetragonStackFrame
{
Address = (ulong)random.Next(0x1000, int.MaxValue),
Offset = (ulong)random.Next(0, 1000),
Symbol = $"func_{random.Next(100)}",
Module = "/usr/lib/libtest.so"
})
.ToList()
}
});
}
return events;
}
private static List<RuntimeCallEvent> GenerateRuntimeCallEvents(int count)
{
var events = new List<RuntimeCallEvent>(count);
var random = new Random(42);
for (var i = 0; i < count; i++)
{
events.Add(new RuntimeCallEvent
{
EventId = Guid.NewGuid(),
Timestamp = DateTimeOffset.UtcNow.AddMilliseconds(-random.Next(10000)),
Source = RuntimeEventSource.Tetragon,
ProcessId = random.Next(1000, 50000),
ContainerId = $"container-{random.Next(100):D4}",
Frames = Enumerable.Range(0, random.Next(3, 10))
.Select(_ => new CanonicalStackFrame
{
Address = (ulong)random.Next(0x1000, int.MaxValue),
Symbol = $"func_{random.Next(100)}",
Confidence = random.NextDouble() * 0.5 + 0.5
})
.ToList()
});
}
return events;
}
}
#region Benchmark Config
public sealed class TetragonBenchmarkConfig : ManualConfig
{
public TetragonBenchmarkConfig()
{
AddJob(Job.ShortRun
.WithWarmupCount(3)
.WithIterationCount(5));
AddLogger(ConsoleLogger.Default);
AddColumnProvider(DefaultColumnProviders.Instance);
}
}
#endregion
#region Mock Types for Benchmarks
public interface IHotSymbolRepository
{
Task<int> IngestBatchAsync(IEnumerable<SymbolObservation> observations, CancellationToken ct = default);
}
public sealed record SymbolObservation
{
public required string ImageDigest { get; init; }
public required string FunctionId { get; init; }
public required DateTimeOffset ObservedAt { get; init; }
public required double Confidence { get; init; }
}
public interface ISymbolResolver
{
Task<ResolvedSymbol?> ResolveAsync(ulong address, string? binaryPath, string? module, CancellationToken ct = default);
}
public sealed record ResolvedSymbol
{
public string? Name { get; init; }
public string? SourceFile { get; init; }
public int? SourceLine { get; init; }
public double Confidence { get; init; }
}
#endregion

View File

@@ -0,0 +1,286 @@
// -----------------------------------------------------------------------------
// TetragonHotSymbolBridgeTests.cs
// Sprint: SPRINT_20260118_019_Infra_tetragon_integration
// Task: TASK-019-010 - Create integration tests
// Description: Unit tests for TetragonHotSymbolBridge
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using StellaOps.RuntimeInstrumentation.Tetragon;
using Xunit;
namespace StellaOps.RuntimeInstrumentation.Tetragon.Tests;
public class TetragonHotSymbolBridgeTests : IDisposable
{
private readonly Mock<IHotSymbolRepository> _mockRepository;
private readonly Mock<ILogger<TetragonHotSymbolBridge>> _mockLogger;
private readonly TetragonHotSymbolBridge _bridge;
public TetragonHotSymbolBridgeTests()
{
_mockRepository = new Mock<IHotSymbolRepository>();
_mockLogger = new Mock<ILogger<TetragonHotSymbolBridge>>();
var options = Options.Create(new TetragonBridgeOptions
{
AggregationWindow = TimeSpan.FromSeconds(5),
MaxBufferSize = 100,
MinConfidenceThreshold = 0.5
});
_bridge = new TetragonHotSymbolBridge(
_mockRepository.Object,
options,
_mockLogger.Object);
}
public void Dispose()
{
_bridge.Dispose();
}
[Fact]
public async Task RecordObservationsAsync_WithValidEvents_BuffersObservations()
{
// Arrange
var events = new List<RuntimeCallEvent>
{
CreateTestEvent("func1", "module1", 0.8),
CreateTestEvent("func2", "module1", 0.9)
};
// Act
await _bridge.RecordObservationsAsync("sha256:test123", events);
// Assert - observations are buffered, not immediately flushed
_mockRepository.Verify(
r => r.IngestBatchAsync(It.IsAny<HotSymbolIngestRequest>(), It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task FlushAsync_WithBufferedObservations_IngestsToRepository()
{
// Arrange
var events = new List<RuntimeCallEvent>
{
CreateTestEvent("func1", "module1", 0.8),
CreateTestEvent("func2", "module1", 0.9)
};
_mockRepository
.Setup(r => r.IngestBatchAsync(It.IsAny<HotSymbolIngestRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new HotSymbolIngestResponse
{
IngestedCount = 2,
NewSymbolsCount = 2,
UpdatedSymbolsCount = 0,
ProcessingTime = TimeSpan.FromMilliseconds(10)
});
await _bridge.RecordObservationsAsync("sha256:test123", events);
// Act
var ingested = await _bridge.FlushAsync();
// Assert
Assert.Equal(2, ingested);
_mockRepository.Verify(
r => r.IngestBatchAsync(
It.Is<HotSymbolIngestRequest>(req =>
req.ImageDigest == "sha256:test123" &&
req.Source == "tetragon" &&
req.Observations.Count == 2),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RecordObservationsAsync_FiltersByConfidenceThreshold()
{
// Arrange - one event below threshold
var events = new List<RuntimeCallEvent>
{
CreateTestEvent("func1", "module1", 0.8), // Above threshold
CreateTestEvent("func2", "module1", 0.3) // Below threshold (0.5)
};
_mockRepository
.Setup(r => r.IngestBatchAsync(It.IsAny<HotSymbolIngestRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new HotSymbolIngestResponse
{
IngestedCount = 1,
NewSymbolsCount = 1,
UpdatedSymbolsCount = 0,
ProcessingTime = TimeSpan.FromMilliseconds(10)
});
await _bridge.RecordObservationsAsync("sha256:test123", events);
// Act
await _bridge.FlushAsync();
// Assert - only one observation should be ingested
_mockRepository.Verify(
r => r.IngestBatchAsync(
It.Is<HotSymbolIngestRequest>(req => req.Observations.Count == 1),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RecordObservationsAsync_AggregatesDuplicateSymbols()
{
// Arrange - same function called multiple times
var events = new List<RuntimeCallEvent>
{
CreateTestEvent("func1", "module1", 0.8),
CreateTestEvent("func1", "module1", 0.8),
CreateTestEvent("func1", "module1", 0.8)
};
HotSymbolIngestRequest? capturedRequest = null;
_mockRepository
.Setup(r => r.IngestBatchAsync(It.IsAny<HotSymbolIngestRequest>(), It.IsAny<CancellationToken>()))
.Callback<HotSymbolIngestRequest, CancellationToken>((req, _) => capturedRequest = req)
.ReturnsAsync(new HotSymbolIngestResponse
{
IngestedCount = 1,
NewSymbolsCount = 1,
UpdatedSymbolsCount = 0,
ProcessingTime = TimeSpan.FromMilliseconds(10)
});
await _bridge.RecordObservationsAsync("sha256:test123", events);
// Act
await _bridge.FlushAsync();
// Assert - should aggregate to single entry with count 3
Assert.NotNull(capturedRequest);
Assert.Single(capturedRequest.Observations);
Assert.Equal(3, capturedRequest.Observations[0].Count);
}
[Fact]
public async Task FlushAsync_WithEmptyBuffer_ReturnsZero()
{
// Act
var ingested = await _bridge.FlushAsync();
// Assert
Assert.Equal(0, ingested);
_mockRepository.Verify(
r => r.IngestBatchAsync(It.IsAny<HotSymbolIngestRequest>(), It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task RecordObservationsAsync_RequiresImageDigest()
{
// Arrange
var events = new List<RuntimeCallEvent> { CreateTestEvent("func1", "module1", 0.8) };
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => _bridge.RecordObservationsAsync("", events));
await Assert.ThrowsAsync<ArgumentException>(
() => _bridge.RecordObservationsAsync(null!, events));
}
[Fact]
public async Task GetStatisticsAsync_DelegatesToRepository()
{
// Arrange
var expectedStats = new HotSymbolStatistics
{
TotalSymbols = 100,
TotalObservations = 5000,
UniqueBuildIds = 10,
SecurityRelevantSymbols = 25,
SymbolsWithCves = 5,
EarliestObservation = DateTime.UtcNow.AddDays(-7),
LatestObservation = DateTime.UtcNow,
TopModules = new List<ModuleObservationSummary>()
};
_mockRepository
.Setup(r => r.GetStatisticsAsync("sha256:test", It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedStats);
// Act
var stats = await _bridge.GetStatisticsAsync("sha256:test");
// Assert
Assert.Equal(100, stats.TotalSymbols);
Assert.Equal(5000, stats.TotalObservations);
}
[Fact]
public async Task RecordObservationsAsync_ExcludesKernelFrames()
{
// Arrange - event with kernel frame
var evt = new RuntimeCallEvent
{
EventId = Guid.NewGuid(),
Timestamp = DateTimeOffset.UtcNow,
Source = RuntimeEventSource.Syscall,
ProcessId = 1234,
ThreadId = 5678,
Frames = new List<CanonicalStackFrame>
{
new() { Symbol = "sys_read", Module = "vmlinux", IsKernel = true, Confidence = 1.0 },
new() { Symbol = "user_func", Module = "myapp", IsKernel = false, Confidence = 0.8 }
}
};
HotSymbolIngestRequest? capturedRequest = null;
_mockRepository
.Setup(r => r.IngestBatchAsync(It.IsAny<HotSymbolIngestRequest>(), It.IsAny<CancellationToken>()))
.Callback<HotSymbolIngestRequest, CancellationToken>((req, _) => capturedRequest = req)
.ReturnsAsync(new HotSymbolIngestResponse
{
IngestedCount = 1,
NewSymbolsCount = 1,
UpdatedSymbolsCount = 0,
ProcessingTime = TimeSpan.FromMilliseconds(10)
});
await _bridge.RecordObservationsAsync("sha256:test123", new[] { evt });
// Act
await _bridge.FlushAsync();
// Assert - only user-space frame should be included
Assert.NotNull(capturedRequest);
Assert.Single(capturedRequest.Observations);
Assert.Equal("user_func", capturedRequest.Observations[0].FunctionName);
}
private static RuntimeCallEvent CreateTestEvent(string funcName, string module, double confidence)
{
return new RuntimeCallEvent
{
EventId = Guid.NewGuid(),
Timestamp = DateTimeOffset.UtcNow,
Source = RuntimeEventSource.Syscall,
ProcessId = 1234,
ThreadId = 5678,
Frames = new List<CanonicalStackFrame>
{
new()
{
Symbol = funcName,
Module = module,
IsKernel = false,
Confidence = confidence,
Address = 0x12345678
}
}
};
}
}

View File

@@ -0,0 +1,316 @@
// -----------------------------------------------------------------------------
// TetragonPrivacyFilterTests.cs
// Sprint: SPRINT_20260118_019_Infra_tetragon_integration
// Task: TASK-019-010 - Create integration tests
// Description: Unit tests for TetragonPrivacyFilter
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using StellaOps.RuntimeInstrumentation.Tetragon;
using Xunit;
namespace StellaOps.RuntimeInstrumentation.Tetragon.Tests;
public class TetragonPrivacyFilterTests
{
private readonly Mock<ILogger<TetragonPrivacyFilter>> _mockLogger;
public TetragonPrivacyFilterTests()
{
_mockLogger = new Mock<ILogger<TetragonPrivacyFilter>>();
}
[Fact]
public void Filter_WithAllowedNamespace_ReturnsEvent()
{
// Arrange
var options = Options.Create(new TetragonPrivacyOptions
{
AllowedNamespaces = new[] { "stella-ops-workloads", "default" }
});
var filter = new TetragonPrivacyFilter(options, _mockLogger.Object);
var evt = CreateTestEvent("stella-ops-workloads");
// Act
var result = filter.Filter(evt);
// Assert
Assert.NotNull(result);
}
[Fact]
public void Filter_WithDisallowedNamespace_ReturnsNull()
{
// Arrange
var options = Options.Create(new TetragonPrivacyOptions
{
AllowedNamespaces = new[] { "stella-ops-workloads" }
});
var filter = new TetragonPrivacyFilter(options, _mockLogger.Object);
var evt = CreateTestEvent("kube-system");
// Act
var result = filter.Filter(evt);
// Assert
Assert.Null(result);
}
[Fact]
public void Filter_WithEmptyAllowlist_AllowsAllNamespaces()
{
// Arrange
var options = Options.Create(new TetragonPrivacyOptions
{
AllowedNamespaces = null // Empty = allow all
});
var filter = new TetragonPrivacyFilter(options, _mockLogger.Object);
var evt = CreateTestEvent("any-namespace");
// Act
var result = filter.Filter(evt);
// Assert
Assert.NotNull(result);
}
[Theory]
[InlineData("password=secret123", "[REDACTED]")]
[InlineData("API_KEY=abc123", "[REDACTED]")]
[InlineData("normal text here", "normal text here")]
[InlineData("user@example.com", "[REDACTED]")]
public void Filter_RedactsArguments(string input, string expected)
{
// Arrange
var options = Options.Create(new TetragonPrivacyOptions
{
RedactArguments = true,
UseDefaultRedactionPatterns = true
});
var filter = new TetragonPrivacyFilter(options, _mockLogger.Object);
var evt = new TetragonEvent
{
Type = TetragonEventType.Kprobe,
Time = DateTimeOffset.UtcNow,
Process = new TetragonProcess
{
Pid = 1234,
Tid = 5678,
Pod = new TetragonPod { Namespace = "default" }
},
Args = new List<object> { input }
};
// Act
var result = filter.Filter(evt);
// Assert
Assert.NotNull(result);
Assert.Single(result.Args!);
Assert.Equal(expected, result.Args![0].ToString());
}
[Fact]
public void Filter_WithSymbolIdOnlyMode_StripsSymbolNames()
{
// Arrange
var options = Options.Create(new TetragonPrivacyOptions
{
SymbolIdOnlyMode = true
});
var filter = new TetragonPrivacyFilter(options, _mockLogger.Object);
var evt = new TetragonEvent
{
Type = TetragonEventType.Kprobe,
Time = DateTimeOffset.UtcNow,
Process = new TetragonProcess
{
Pid = 1234,
Tid = 5678,
Pod = new TetragonPod { Namespace = "default" }
},
StackTrace = new TetragonStackTrace
{
Frames = new List<TetragonStackFrame>
{
new()
{
Address = 0x7FFF12345678,
Symbol = "my_secret_function",
Module = "libsecret.so"
}
}
}
};
// Act
var result = filter.Filter(evt);
// Assert
Assert.NotNull(result);
Assert.NotNull(result.StackTrace?.Frames);
Assert.Single(result.StackTrace!.Frames!);
Assert.DoesNotContain("my_secret_function", result.StackTrace!.Frames![0].Symbol!);
Assert.StartsWith("sym_", result.StackTrace!.Frames![0].Symbol!);
}
[Fact]
public void Filter_RuntimeCallEvent_FiltersNamespace()
{
// Arrange
var options = Options.Create(new TetragonPrivacyOptions
{
AllowedNamespaces = new[] { "allowed-ns" }
});
var filter = new TetragonPrivacyFilter(options, _mockLogger.Object);
var allowedEvt = new RuntimeCallEvent
{
EventId = Guid.NewGuid(),
Timestamp = DateTimeOffset.UtcNow,
Source = RuntimeEventSource.Syscall,
Namespace = "allowed-ns",
Frames = new List<CanonicalStackFrame>()
};
var disallowedEvt = new RuntimeCallEvent
{
EventId = Guid.NewGuid(),
Timestamp = DateTimeOffset.UtcNow,
Source = RuntimeEventSource.Syscall,
Namespace = "disallowed-ns",
Frames = new List<CanonicalStackFrame>()
};
// Act
var allowedResult = filter.Filter(allowedEvt);
var disallowedResult = filter.Filter(disallowedEvt);
// Assert
Assert.NotNull(allowedResult);
Assert.Null(disallowedResult);
}
[Fact]
public void Filter_RuntimeCallEvent_WithSymbolIdOnlyMode_StripsDemangled()
{
// Arrange
var options = Options.Create(new TetragonPrivacyOptions
{
SymbolIdOnlyMode = true
});
var filter = new TetragonPrivacyFilter(options, _mockLogger.Object);
var evt = new RuntimeCallEvent
{
EventId = Guid.NewGuid(),
Timestamp = DateTimeOffset.UtcNow,
Source = RuntimeEventSource.Syscall,
Frames = new List<CanonicalStackFrame>
{
new()
{
Symbol = "func",
Demangled = "MyClass::myFunction(int)",
SourceFile = "/src/myfile.cpp",
SourceLine = 42,
Confidence = 0.9
}
}
};
// Act
var result = filter.Filter(evt);
// Assert
Assert.NotNull(result);
Assert.Single(result.Frames);
Assert.Equal("func", result.Frames[0].Symbol); // Symbol preserved
Assert.Null(result.Frames[0].Demangled); // Stripped
Assert.Null(result.Frames[0].SourceFile); // Stripped
Assert.Null(result.Frames[0].SourceLine); // Stripped
}
[Fact]
public void GetStatistics_ReturnsCorrectCounts()
{
// Arrange
var options = Options.Create(new TetragonPrivacyOptions
{
AllowedNamespaces = new[] { "ns1", "ns2", "ns3" },
AllowedLabels = new[] { "label1", "label2" },
UseDefaultRedactionPatterns = true,
AdditionalRedactionPatterns = new[] { @"custom\d+" },
SymbolIdOnlyMode = true
});
var filter = new TetragonPrivacyFilter(options, _mockLogger.Object);
// Act
var stats = filter.GetStatistics();
// Assert
Assert.Equal(3, stats.AllowedNamespacesCount);
Assert.Equal(2, stats.AllowedLabelsCount);
Assert.True(stats.RedactionPatternsCount > 1); // Default + custom
Assert.True(stats.SymbolIdOnlyMode);
}
[Fact]
public async Task FilterStreamAsync_FiltersMultipleEvents()
{
// Arrange
var options = Options.Create(new TetragonPrivacyOptions
{
AllowedNamespaces = new[] { "allowed" }
});
var filter = new TetragonPrivacyFilter(options, _mockLogger.Object);
var events = new List<TetragonEvent>
{
CreateTestEvent("allowed"),
CreateTestEvent("disallowed"),
CreateTestEvent("allowed")
};
// Act
var results = new List<TetragonEvent>();
await foreach (var evt in filter.FilterStreamAsync(ToAsyncEnumerable(events)))
{
results.Add(evt);
}
// Assert
Assert.Equal(2, results.Count);
}
private static TetragonEvent CreateTestEvent(string ns)
{
return new TetragonEvent
{
Type = TetragonEventType.ProcessExec,
Time = DateTimeOffset.UtcNow,
Process = new TetragonProcess
{
Pid = 1234,
Tid = 5678,
Pod = new TetragonPod { Namespace = ns }
}
};
}
private static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(IEnumerable<T> items)
{
foreach (var item in items)
{
await Task.Yield();
yield return item;
}
}
}