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,412 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// Bridges Tetragon runtime observations to the witness signing pipeline.
|
||||
/// Buffers captures by claim_id and emits to SignedWitnessGenerator.
|
||||
/// </summary>
|
||||
public sealed class TetragonWitnessBridge : ITetragonWitnessBridge, IDisposable
|
||||
{
|
||||
private readonly IRuntimeWitnessGenerator _witnessGenerator;
|
||||
private readonly TetragonWitnessBridgeOptions _options;
|
||||
private readonly ILogger<TetragonWitnessBridge> _logger;
|
||||
private readonly ConcurrentDictionary<string, ClaimBuffer> _buffers = new();
|
||||
private readonly SemaphoreSlim _backpressure;
|
||||
private readonly Timer _flushTimer;
|
||||
private readonly ActivitySource _activitySource = new("StellaOps.Tetragon.WitnessBridge");
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new witness bridge.
|
||||
/// </summary>
|
||||
public TetragonWitnessBridge(
|
||||
IRuntimeWitnessGenerator witnessGenerator,
|
||||
IOptions<TetragonWitnessBridgeOptions> options,
|
||||
ILogger<TetragonWitnessBridge> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<RuntimeWitnessResult> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<RuntimeWitnessResult?> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<RuntimeWitnessResult> 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<RuntimeWitnessResult?> 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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<RuntimeObservation> Observations { get; } = new();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for the Tetragon witness bridge.
|
||||
/// </summary>
|
||||
public interface ITetragonWitnessBridge
|
||||
{
|
||||
/// <summary>
|
||||
/// Buffers an observation for later witness emission.
|
||||
/// </summary>
|
||||
Task BufferObservationAsync(
|
||||
RuntimeObservation observation,
|
||||
WitnessContext context,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Processes a stream of observations and emits witnesses.
|
||||
/// </summary>
|
||||
IAsyncEnumerable<RuntimeWitnessResult> ProcessStreamAsync(
|
||||
IAsyncEnumerable<(RuntimeObservation observation, WitnessContext context)> stream,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to emit a witness for a claim if ready.
|
||||
/// </summary>
|
||||
Task<RuntimeWitnessResult?> TryEmitWitnessAsync(
|
||||
string claimId,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Flushes all buffered observations as witnesses.
|
||||
/// </summary>
|
||||
IAsyncEnumerable<RuntimeWitnessResult> FlushAllAsync(CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context for witness generation.
|
||||
/// </summary>
|
||||
public sealed record WitnessContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Claim ID linking to static analysis claim.
|
||||
/// </summary>
|
||||
public required string ClaimId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Artifact digest for context.
|
||||
/// </summary>
|
||||
public required string ArtifactDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Component PURL being observed.
|
||||
/// </summary>
|
||||
public required string ComponentPurl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional vulnerability ID.
|
||||
/// </summary>
|
||||
public string? VulnerabilityId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the witness bridge.
|
||||
/// </summary>
|
||||
public sealed record TetragonWitnessBridgeOptions
|
||||
{
|
||||
/// <summary>Configuration section name.</summary>
|
||||
public const string SectionName = "Tetragon:WitnessBridge";
|
||||
|
||||
/// <summary>Minimum observations before emitting witness (default: 5).</summary>
|
||||
public int MinObservationsForWitness { get; init; } = 5;
|
||||
|
||||
/// <summary>Maximum concurrent witness generations (default: 4).</summary>
|
||||
public int MaxConcurrentWitnesses { get; init; } = 4;
|
||||
|
||||
/// <summary>Buffer timeout for stale observations (default: 5 minutes).</summary>
|
||||
public TimeSpan BufferTimeout { get; init; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>Whether to publish to Rekor (default: true).</summary>
|
||||
public bool PublishToRekor { get; init; } = true;
|
||||
|
||||
/// <summary>Whether to use keyless signing (default: true).</summary>
|
||||
public bool UseKeylessSigning { get; init; } = true;
|
||||
|
||||
/// <summary>Signing key ID if not using keyless.</summary>
|
||||
public string? SigningKeyId { get; init; }
|
||||
|
||||
/// <summary>Signing algorithm (default: ECDSA_P256_SHA256).</summary>
|
||||
public string SigningAlgorithm { get; init; } = "ECDSA_P256_SHA256";
|
||||
}
|
||||
|
||||
// Interface placeholders - should import from Scanner module
|
||||
|
||||
/// <summary>
|
||||
/// Generator for signed runtime witnesses.
|
||||
/// </summary>
|
||||
public interface IRuntimeWitnessGenerator
|
||||
{
|
||||
Task<RuntimeWitnessResult> GenerateAsync(RuntimeWitnessRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request for runtime witness generation.
|
||||
/// </summary>
|
||||
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<RuntimeObservation> Observations { get; init; }
|
||||
public bool PublishToRekor { get; init; } = true;
|
||||
public RuntimeWitnessSigningOptions SigningOptions { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signing options for runtime witnesses.
|
||||
/// </summary>
|
||||
public sealed record RuntimeWitnessSigningOptions
|
||||
{
|
||||
public string? KeyId { get; init; }
|
||||
public bool UseKeyless { get; init; } = true;
|
||||
public string Algorithm { get; init; } = "ECDSA_P256_SHA256";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of runtime witness generation.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime observation for witness evidence.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Source type for observations.
|
||||
/// </summary>
|
||||
public enum RuntimeObservationSourceType
|
||||
{
|
||||
Tetragon,
|
||||
OpenTelemetry,
|
||||
Profiler,
|
||||
Tracer,
|
||||
Custom
|
||||
}
|
||||
Reference in New Issue
Block a user