// ----------------------------------------------------------------------------- // TetragonWitnessBridge.cs // Sprint: SPRINT_20260118_019_Infra_tetragon_integration // Task: TASK-019-006 - Create witness emission bridge to SPRINT_016 pipeline // Description: Bridges Tetragon captures to RuntimeWitnessRequest for signing // ----------------------------------------------------------------------------- using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace StellaOps.RuntimeInstrumentation.Tetragon; /// /// Bridges Tetragon runtime observations to the witness signing pipeline. /// Buffers captures by claim_id and emits to SignedWitnessGenerator. /// public sealed class TetragonWitnessBridge : ITetragonWitnessBridge, IDisposable { private readonly IRuntimeWitnessGenerator _witnessGenerator; private readonly TetragonWitnessBridgeOptions _options; private readonly ILogger _logger; private readonly ConcurrentDictionary _buffers = new(); private readonly SemaphoreSlim _backpressure; private readonly Timer _flushTimer; private readonly ActivitySource _activitySource = new("StellaOps.Tetragon.WitnessBridge"); private bool _disposed; /// /// Creates a new witness bridge. /// public TetragonWitnessBridge( IRuntimeWitnessGenerator witnessGenerator, IOptions options, ILogger logger) { _witnessGenerator = witnessGenerator ?? throw new ArgumentNullException(nameof(witnessGenerator)); _options = options?.Value ?? new TetragonWitnessBridgeOptions(); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _backpressure = new SemaphoreSlim(_options.MaxConcurrentWitnesses); _flushTimer = new Timer( FlushTimerCallback, null, _options.BufferTimeout, _options.BufferTimeout); } /// public async Task BufferObservationAsync( RuntimeObservation observation, WitnessContext context, CancellationToken ct = default) { using var activity = _activitySource.StartActivity("BufferObservation"); activity?.SetTag("claim_id", context.ClaimId); var buffer = _buffers.GetOrAdd(context.ClaimId, _ => new ClaimBuffer { ClaimId = context.ClaimId, Context = context, CreatedAt = DateTimeOffset.UtcNow }); buffer.Observations.Add(observation); buffer.LastUpdated = DateTimeOffset.UtcNow; activity?.SetTag("buffer_size", buffer.Observations.Count); // Check if buffer should be emitted if (buffer.Observations.Count >= _options.MinObservationsForWitness) { await TryEmitWitnessAsync(context.ClaimId, ct); } } /// public async IAsyncEnumerable ProcessStreamAsync( IAsyncEnumerable<(RuntimeObservation observation, WitnessContext context)> stream, [EnumeratorCancellation] CancellationToken ct = default) { await foreach (var (observation, context) in stream.WithCancellation(ct)) { await BufferObservationAsync(observation, context, ct); // Yield any ready witnesses foreach (var claimId in _buffers.Keys.ToList()) { if (_buffers.TryGetValue(claimId, out var buffer) && buffer.Observations.Count >= _options.MinObservationsForWitness) { var result = await EmitWitnessAsync(claimId, ct); if (result != null) { yield return result; } } } } // Final flush of remaining buffers await foreach (var result in FlushAllAsync(ct)) { yield return result; } } /// public async Task TryEmitWitnessAsync( string claimId, CancellationToken ct = default) { if (!_buffers.TryGetValue(claimId, out var buffer)) { return null; } if (buffer.Observations.Count < _options.MinObservationsForWitness) { return null; } return await EmitWitnessAsync(claimId, ct); } /// public async IAsyncEnumerable FlushAllAsync( [EnumeratorCancellation] CancellationToken ct = default) { foreach (var claimId in _buffers.Keys.ToList()) { var result = await EmitWitnessAsync(claimId, ct); if (result != null) { yield return result; } } } private async Task EmitWitnessAsync( string claimId, CancellationToken ct) { if (!_buffers.TryRemove(claimId, out var buffer)) { return null; } if (buffer.Observations.Count == 0) { return null; } using var activity = _activitySource.StartActivity("EmitWitness"); activity?.SetTag("claim_id", claimId); activity?.SetTag("observation_count", buffer.Observations.Count); // Apply backpressure await _backpressure.WaitAsync(ct); try { var request = new RuntimeWitnessRequest { ClaimId = claimId, ArtifactDigest = buffer.Context.ArtifactDigest, ComponentPurl = buffer.Context.ComponentPurl, VulnerabilityId = buffer.Context.VulnerabilityId, Observations = buffer.Observations.ToList(), PublishToRekor = _options.PublishToRekor, SigningOptions = new RuntimeWitnessSigningOptions { UseKeyless = _options.UseKeylessSigning, KeyId = _options.SigningKeyId, Algorithm = _options.SigningAlgorithm } }; var result = await _witnessGenerator.GenerateAsync(request, ct); if (result.Success) { _logger.LogInformation( "Generated runtime witness for claim {ClaimId} with {Count} observations, Rekor index: {RekorIndex}", claimId, buffer.Observations.Count, result.RekorLogIndex); } else { _logger.LogWarning( "Failed to generate witness for claim {ClaimId}: {Error}", claimId, result.ErrorMessage); } return result; } finally { _backpressure.Release(); } } private void FlushTimerCallback(object? state) { try { var cutoff = DateTimeOffset.UtcNow - _options.BufferTimeout; foreach (var (claimId, buffer) in _buffers) { if (buffer.LastUpdated < cutoff && buffer.Observations.Count > 0) { _logger.LogDebug("Flushing stale buffer for claim {ClaimId}", claimId); _ = EmitWitnessAsync(claimId, CancellationToken.None); } } } catch (Exception ex) { _logger.LogError(ex, "Error in flush timer callback"); } } /// public void Dispose() { if (_disposed) return; _disposed = true; _flushTimer.Dispose(); _backpressure.Dispose(); _activitySource.Dispose(); } private sealed class ClaimBuffer { public required string ClaimId { get; init; } public required WitnessContext Context { get; init; } public DateTimeOffset CreatedAt { get; init; } public DateTimeOffset LastUpdated { get; set; } public List Observations { get; } = new(); } } /// /// Interface for the Tetragon witness bridge. /// public interface ITetragonWitnessBridge { /// /// Buffers an observation for later witness emission. /// Task BufferObservationAsync( RuntimeObservation observation, WitnessContext context, CancellationToken ct = default); /// /// Processes a stream of observations and emits witnesses. /// IAsyncEnumerable ProcessStreamAsync( IAsyncEnumerable<(RuntimeObservation observation, WitnessContext context)> stream, CancellationToken ct = default); /// /// Attempts to emit a witness for a claim if ready. /// Task TryEmitWitnessAsync( string claimId, CancellationToken ct = default); /// /// Flushes all buffered observations as witnesses. /// IAsyncEnumerable FlushAllAsync(CancellationToken ct = default); } /// /// Context for witness generation. /// public sealed record WitnessContext { /// /// Claim ID linking to static analysis claim. /// public required string ClaimId { get; init; } /// /// Artifact digest for context. /// public required string ArtifactDigest { get; init; } /// /// Component PURL being observed. /// public required string ComponentPurl { get; init; } /// /// Optional vulnerability ID. /// public string? VulnerabilityId { get; init; } } /// /// Configuration for the witness bridge. /// public sealed record TetragonWitnessBridgeOptions { /// Configuration section name. public const string SectionName = "Tetragon:WitnessBridge"; /// Minimum observations before emitting witness (default: 5). public int MinObservationsForWitness { get; init; } = 5; /// Maximum concurrent witness generations (default: 4). public int MaxConcurrentWitnesses { get; init; } = 4; /// Buffer timeout for stale observations (default: 5 minutes). public TimeSpan BufferTimeout { get; init; } = TimeSpan.FromMinutes(5); /// Whether to publish to Rekor (default: true). public bool PublishToRekor { get; init; } = true; /// Whether to use keyless signing (default: true). public bool UseKeylessSigning { get; init; } = true; /// Signing key ID if not using keyless. public string? SigningKeyId { get; init; } /// Signing algorithm (default: ECDSA_P256_SHA256). public string SigningAlgorithm { get; init; } = "ECDSA_P256_SHA256"; } // Interface placeholders - should import from Scanner module /// /// Generator for signed runtime witnesses. /// public interface IRuntimeWitnessGenerator { Task GenerateAsync(RuntimeWitnessRequest request, CancellationToken ct = default); } /// /// Request for runtime witness generation. /// public sealed record RuntimeWitnessRequest { public required string ClaimId { get; init; } public required string ArtifactDigest { get; init; } public required string ComponentPurl { get; init; } public string? VulnerabilityId { get; init; } public required IReadOnlyList Observations { get; init; } public bool PublishToRekor { get; init; } = true; public RuntimeWitnessSigningOptions SigningOptions { get; init; } = new(); } /// /// Signing options for runtime witnesses. /// public sealed record RuntimeWitnessSigningOptions { public string? KeyId { get; init; } public bool UseKeyless { get; init; } = true; public string Algorithm { get; init; } = "ECDSA_P256_SHA256"; } /// /// Result of runtime witness generation. /// public sealed record RuntimeWitnessResult { public required bool Success { get; init; } public object? Witness { get; init; } public byte[]? EnvelopeBytes { get; init; } public long? RekorLogIndex { get; init; } public string? RekorLogId { get; init; } public string? CasUri { get; init; } public string? ErrorMessage { get; init; } public string? ClaimId { get; init; } } /// /// Runtime observation for witness evidence. /// public sealed record RuntimeObservation { public required DateTimeOffset ObservedAt { get; init; } public int ObservationCount { get; init; } = 1; public string? StackSampleHash { get; init; } public int? ProcessId { get; init; } public string? ContainerId { get; init; } public string? PodName { get; init; } public string? Namespace { get; init; } public required RuntimeObservationSourceType SourceType { get; init; } public string? ObservationId { get; init; } } /// /// Source type for observations. /// public enum RuntimeObservationSourceType { Tetragon, OpenTelemetry, Profiler, Tracer, Custom }