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:
@@ -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: <5%
|
||||
/// - Memory overhead of agent: <100MB
|
||||
/// - Capture latency (P95): <100ms
|
||||
/// - Throughput: >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: >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: <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: >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: <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: <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: >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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user