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,388 @@
// -----------------------------------------------------------------------------
// TetragonAgentCapability.cs
// Sprint: SPRINT_20260118_019_Infra_tetragon_integration
// Task: TASK-019-004 - Extend existing agent framework for Tetragon
// Description: Agent capability for Tetragon eBPF event collection
// -----------------------------------------------------------------------------
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace StellaOps.Agent.Tetragon;
/// <summary>
/// Agent capability for Tetragon eBPF event collection.
/// Extends the existing agent framework following established patterns.
/// </summary>
public sealed class TetragonAgentCapability : IAgentCapability
{
private readonly ITetragonGrpcClient _tetragonClient;
private readonly ITetragonEventAdapter _eventAdapter;
private readonly ITetragonHotSymbolBridge _hotSymbolBridge;
private readonly ITetragonWitnessBridge _witnessBridge;
private readonly ITetragonPrivacyFilter _privacyFilter;
private readonly TetragonAgentOptions _options;
private readonly ILogger<TetragonAgentCapability> _logger;
private readonly ActivitySource _activitySource = new("StellaOps.Agent.Tetragon");
private CancellationTokenSource? _collectionCts;
private Task? _collectionTask;
private bool _initialized;
/// <inheritdoc />
public string Name => "tetragon";
/// <inheritdoc />
public string Version => "1.0.0";
/// <inheritdoc />
public IReadOnlyList<string> SupportedTaskTypes =>
[
"tetragon.start-collection",
"tetragon.stop-collection",
"tetragon.get-status",
"tetragon.flush-observations"
];
/// <summary>
/// Creates a new Tetragon agent capability.
/// </summary>
public TetragonAgentCapability(
ITetragonGrpcClient tetragonClient,
ITetragonEventAdapter eventAdapter,
ITetragonHotSymbolBridge hotSymbolBridge,
ITetragonWitnessBridge witnessBridge,
ITetragonPrivacyFilter privacyFilter,
IOptions<TetragonAgentOptions> options,
ILogger<TetragonAgentCapability> logger)
{
_tetragonClient = tetragonClient ?? throw new ArgumentNullException(nameof(tetragonClient));
_eventAdapter = eventAdapter ?? throw new ArgumentNullException(nameof(eventAdapter));
_hotSymbolBridge = hotSymbolBridge ?? throw new ArgumentNullException(nameof(hotSymbolBridge));
_witnessBridge = witnessBridge ?? throw new ArgumentNullException(nameof(witnessBridge));
_privacyFilter = privacyFilter ?? throw new ArgumentNullException(nameof(privacyFilter));
_options = options?.Value ?? new TetragonAgentOptions();
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async Task<bool> InitializeAsync(CancellationToken ct = default)
{
using var activity = _activitySource.StartActivity("Initialize");
try
{
_logger.LogInformation("Initializing Tetragon agent capability...");
// Verify Tetragon connection
var connected = await _tetragonClient.ConnectAsync(ct);
if (!connected)
{
_logger.LogError("Failed to connect to Tetragon gRPC endpoint");
return false;
}
// Verify Tetragon health
var health = await _tetragonClient.GetHealthAsync(ct);
if (!health.IsHealthy)
{
_logger.LogWarning("Tetragon is not healthy: {Status}", health.Status);
return false;
}
_logger.LogInformation(
"Connected to Tetragon v{Version}, policies: {PolicyCount}",
health.Version, health.ActivePolicyCount);
_initialized = true;
// Auto-start collection if configured
if (_options.AutoStartCollection)
{
await StartCollectionAsync(ct);
}
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error initializing Tetragon capability");
return false;
}
}
/// <inheritdoc />
public async Task<AgentTaskResult> ExecuteAsync(AgentTaskInfo task, CancellationToken ct = default)
{
using var activity = _activitySource.StartActivity("ExecuteTask");
activity?.SetTag("task_type", task.TaskType);
activity?.SetTag("task_id", task.TaskId);
return task.TaskType switch
{
"tetragon.start-collection" => await ExecuteStartCollectionAsync(task, ct),
"tetragon.stop-collection" => await ExecuteStopCollectionAsync(task, ct),
"tetragon.get-status" => await ExecuteGetStatusAsync(task, ct),
"tetragon.flush-observations" => await ExecuteFlushAsync(task, ct),
_ => AgentTaskResult.Failed(task.TaskId, $"Unknown task type: {task.TaskType}")
};
}
/// <inheritdoc />
public async Task<CapabilityHealthStatus> CheckHealthAsync(CancellationToken ct = default)
{
try
{
if (!_initialized)
{
return new CapabilityHealthStatus
{
IsHealthy = false,
Status = "Not initialized",
LastChecked = DateTimeOffset.UtcNow
};
}
var tetragonHealth = await _tetragonClient.GetHealthAsync(ct);
return new CapabilityHealthStatus
{
IsHealthy = tetragonHealth.IsHealthy && _collectionTask?.IsCompleted != true,
Status = _collectionTask != null ? "Collecting" : "Idle",
LastChecked = DateTimeOffset.UtcNow,
Details = new Dictionary<string, object>
{
["tetragon_version"] = tetragonHealth.Version ?? "unknown",
["active_policies"] = tetragonHealth.ActivePolicyCount,
["collection_running"] = _collectionTask != null && !_collectionTask.IsCompleted,
["privacy_mode"] = _privacyFilter.GetStatistics().SymbolIdOnlyMode
}
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Health check failed");
return new CapabilityHealthStatus
{
IsHealthy = false,
Status = $"Health check failed: {ex.Message}",
LastChecked = DateTimeOffset.UtcNow
};
}
}
private async Task<AgentTaskResult> ExecuteStartCollectionAsync(AgentTaskInfo task, CancellationToken ct)
{
if (_collectionTask != null && !_collectionTask.IsCompleted)
{
return AgentTaskResult.Failed(task.TaskId, "Collection already running");
}
await StartCollectionAsync(ct);
return AgentTaskResult.Succeeded(task.TaskId, new Dictionary<string, object>
{
["status"] = "started",
["started_at"] = DateTimeOffset.UtcNow
});
}
private async Task<AgentTaskResult> ExecuteStopCollectionAsync(AgentTaskInfo task, CancellationToken ct)
{
if (_collectionTask == null || _collectionTask.IsCompleted)
{
return AgentTaskResult.Failed(task.TaskId, "Collection not running");
}
await StopCollectionAsync();
return AgentTaskResult.Succeeded(task.TaskId, new Dictionary<string, object>
{
["status"] = "stopped",
["stopped_at"] = DateTimeOffset.UtcNow
});
}
private Task<AgentTaskResult> ExecuteGetStatusAsync(AgentTaskInfo task, CancellationToken ct)
{
var privacyStats = _privacyFilter.GetStatistics();
return Task.FromResult(AgentTaskResult.Succeeded(task.TaskId, new Dictionary<string, object>
{
["initialized"] = _initialized,
["collection_running"] = _collectionTask != null && !_collectionTask.IsCompleted,
["privacy_symbol_id_only"] = privacyStats.SymbolIdOnlyMode,
["privacy_allowed_namespaces"] = privacyStats.AllowedNamespacesCount
}));
}
private async Task<AgentTaskResult> ExecuteFlushAsync(AgentTaskInfo task, CancellationToken ct)
{
var flushed = await _hotSymbolBridge.FlushAsync(ct);
return AgentTaskResult.Succeeded(task.TaskId, new Dictionary<string, object>
{
["flushed_count"] = flushed,
["flushed_at"] = DateTimeOffset.UtcNow
});
}
private async Task StartCollectionAsync(CancellationToken ct)
{
_collectionCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
_collectionTask = RunCollectionLoopAsync(_collectionCts.Token);
_logger.LogInformation("Started Tetragon event collection");
await Task.CompletedTask;
}
private async Task StopCollectionAsync()
{
if (_collectionCts != null)
{
await _collectionCts.CancelAsync();
if (_collectionTask != null)
{
try
{
await _collectionTask;
}
catch (OperationCanceledException)
{
// Expected
}
}
_collectionCts.Dispose();
_collectionCts = null;
_collectionTask = null;
}
// Final flush
await _hotSymbolBridge.FlushAsync();
_logger.LogInformation("Stopped Tetragon event collection");
}
private async Task RunCollectionLoopAsync(CancellationToken ct)
{
_logger.LogDebug("Starting collection loop");
try
{
var eventStream = _tetragonClient.StreamEventsAsync(ct);
// Apply privacy filter
var filteredStream = _privacyFilter.FilterStreamAsync(eventStream, ct);
// Convert to RuntimeCallEvents
var runtimeEvents = _eventAdapter.AdaptStreamAsync(filteredStream, ct);
await foreach (var evt in runtimeEvents.WithCancellation(ct))
{
// Record to hot symbol index
if (!string.IsNullOrEmpty(evt.ContainerId))
{
var imageDigest = await ResolveImageDigestAsync(evt.ContainerId, ct);
if (!string.IsNullOrEmpty(imageDigest))
{
await _hotSymbolBridge.RecordObservationsAsync(imageDigest, new[] { evt }, ct);
}
}
}
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
_logger.LogDebug("Collection loop cancelled");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in collection loop");
throw;
}
}
private Task<string?> ResolveImageDigestAsync(string containerId, CancellationToken ct)
{
// In a real implementation, this would query the container runtime
// to resolve container ID -> image digest
// For now, return a placeholder
return Task.FromResult<string?>($"sha256:{containerId.GetHashCode():X64}".Substring(0, 71));
}
}
/// <summary>
/// Configuration for the Tetragon agent.
/// </summary>
public sealed record TetragonAgentOptions
{
/// <summary>Configuration section name.</summary>
public const string SectionName = "Tetragon:Agent";
/// <summary>Tetragon gRPC address (default: localhost:54321).</summary>
public string TetragonGrpcAddress { get; init; } = "localhost:54321";
/// <summary>Whether to auto-start collection on initialization.</summary>
public bool AutoStartCollection { get; init; } = true;
/// <summary>Connection timeout.</summary>
public TimeSpan ConnectionTimeout { get; init; } = TimeSpan.FromSeconds(30);
/// <summary>Reconnection delay on failure.</summary>
public TimeSpan ReconnectionDelay { get; init; } = TimeSpan.FromSeconds(5);
}
// Interface and model placeholders - should import from Agent.Core
/// <summary>
/// Agent capability interface.
/// </summary>
public interface IAgentCapability
{
string Name { get; }
string Version { get; }
IReadOnlyList<string> SupportedTaskTypes { get; }
Task<bool> InitializeAsync(CancellationToken ct = default);
Task<AgentTaskResult> ExecuteAsync(AgentTaskInfo task, CancellationToken ct = default);
Task<CapabilityHealthStatus> CheckHealthAsync(CancellationToken ct = default);
}
/// <summary>
/// Task information.
/// </summary>
public sealed record AgentTaskInfo
{
public required string TaskId { get; init; }
public required string TaskType { get; init; }
public IDictionary<string, object>? Parameters { get; init; }
}
/// <summary>
/// Task result.
/// </summary>
public sealed record AgentTaskResult
{
public required string TaskId { get; init; }
public required bool Success { get; init; }
public string? ErrorMessage { get; init; }
public IDictionary<string, object>? Output { get; init; }
public static AgentTaskResult Succeeded(string taskId, IDictionary<string, object>? output = null)
=> new() { TaskId = taskId, Success = true, Output = output };
public static AgentTaskResult Failed(string taskId, string error)
=> new() { TaskId = taskId, Success = false, ErrorMessage = error };
}
/// <summary>
/// Capability health status.
/// </summary>
public sealed record CapabilityHealthStatus
{
public required bool IsHealthy { get; init; }
public required string Status { get; init; }
public required DateTimeOffset LastChecked { get; init; }
public IDictionary<string, object>? Details { get; init; }
}

View File

@@ -0,0 +1,314 @@
// -----------------------------------------------------------------------------
// TetragonGrpcClient.cs
// Sprint: SPRINT_20260118_019_Infra_tetragon_integration
// Task: TASK-019-004 - Extend existing agent framework for Tetragon
// Description: gRPC client for Tetragon export API
// -----------------------------------------------------------------------------
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace StellaOps.Agent.Tetragon;
/// <summary>
/// gRPC client for Tetragon's export API.
/// Connects to the Tetragon daemon to stream eBPF events.
/// </summary>
public sealed class TetragonGrpcClient : ITetragonGrpcClient, IDisposable
{
private readonly TetragonGrpcClientOptions _options;
private readonly ILogger<TetragonGrpcClient> _logger;
private readonly ActivitySource _activitySource = new("StellaOps.Tetragon.GrpcClient");
private readonly SemaphoreSlim _connectionLock = new(1, 1);
private HttpClient? _httpClient;
private bool _connected;
private bool _disposed;
/// <summary>
/// Creates a new Tetragon gRPC client.
/// </summary>
public TetragonGrpcClient(
IOptions<TetragonGrpcClientOptions> options,
ILogger<TetragonGrpcClient> logger)
{
_options = options?.Value ?? new TetragonGrpcClientOptions();
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async Task<bool> ConnectAsync(CancellationToken ct = default)
{
await _connectionLock.WaitAsync(ct);
try
{
if (_connected)
{
return true;
}
_logger.LogDebug("Connecting to Tetragon at {Address}", _options.Address);
// Create HTTP client for gRPC-Web or REST fallback
_httpClient = new HttpClient
{
BaseAddress = new Uri(_options.Address),
Timeout = _options.ConnectionTimeout
};
// Verify connection with health check
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(_options.ConnectionTimeout);
try
{
var health = await GetHealthAsync(cts.Token);
_connected = health.IsHealthy;
if (_connected)
{
_logger.LogInformation("Connected to Tetragon v{Version}", health.Version);
}
return _connected;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to connect to Tetragon");
return false;
}
}
finally
{
_connectionLock.Release();
}
}
/// <inheritdoc />
public async Task<TetragonHealthStatus> GetHealthAsync(CancellationToken ct = default)
{
using var activity = _activitySource.StartActivity("GetHealth");
try
{
if (_httpClient == null)
{
return new TetragonHealthStatus
{
IsHealthy = false,
Status = "Not connected"
};
}
var response = await _httpClient.GetAsync("/v1/health", ct);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync(ct);
var healthResponse = JsonSerializer.Deserialize<TetragonHealthResponse>(content);
return new TetragonHealthStatus
{
IsHealthy = true,
Status = "healthy",
Version = healthResponse?.Version,
ActivePolicyCount = healthResponse?.ActivePolicies ?? 0
};
}
return new TetragonHealthStatus
{
IsHealthy = false,
Status = $"HTTP {(int)response.StatusCode}"
};
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Health check failed");
return new TetragonHealthStatus
{
IsHealthy = false,
Status = ex.Message
};
}
}
/// <inheritdoc />
public async IAsyncEnumerable<TetragonEvent> StreamEventsAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
using var activity = _activitySource.StartActivity("StreamEvents");
if (_httpClient == null || !_connected)
{
_logger.LogWarning("Not connected to Tetragon");
yield break;
}
_logger.LogDebug("Starting event stream from Tetragon");
var request = new HttpRequestMessage(HttpMethod.Get, "/v1/events");
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/x-ndjson"));
HttpResponseMessage? response = null;
Stream? stream = null;
StreamReader? reader = null;
try
{
response = await _httpClient.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
ct);
response.EnsureSuccessStatusCode();
stream = await response.Content.ReadAsStreamAsync(ct);
reader = new StreamReader(stream);
while (!ct.IsCancellationRequested && !reader.EndOfStream)
{
var line = await reader.ReadLineAsync(ct);
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
TetragonEvent? evt = null;
try
{
evt = JsonSerializer.Deserialize<TetragonEvent>(line, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch (JsonException ex)
{
_logger.LogDebug(ex, "Failed to parse Tetragon event: {Line}", line);
continue;
}
if (evt != null)
{
yield return evt;
}
}
}
finally
{
reader?.Dispose();
stream?.Dispose();
response?.Dispose();
}
}
/// <inheritdoc />
public async Task<IReadOnlyList<TetragonPolicy>> GetPoliciesAsync(CancellationToken ct = default)
{
using var activity = _activitySource.StartActivity("GetPolicies");
if (_httpClient == null)
{
return [];
}
try
{
var response = await _httpClient.GetAsync("/v1/policies", ct);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(ct);
return JsonSerializer.Deserialize<List<TetragonPolicy>>(content) ?? [];
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get policies");
return [];
}
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_httpClient?.Dispose();
_connectionLock.Dispose();
_activitySource.Dispose();
}
private sealed record TetragonHealthResponse
{
public string? Version { get; init; }
public int ActivePolicies { get; init; }
}
}
/// <summary>
/// Interface for Tetragon gRPC client.
/// </summary>
public interface ITetragonGrpcClient
{
/// <summary>
/// Connects to Tetragon.
/// </summary>
Task<bool> ConnectAsync(CancellationToken ct = default);
/// <summary>
/// Gets Tetragon health status.
/// </summary>
Task<TetragonHealthStatus> GetHealthAsync(CancellationToken ct = default);
/// <summary>
/// Streams events from Tetragon.
/// </summary>
IAsyncEnumerable<TetragonEvent> StreamEventsAsync(CancellationToken ct = default);
/// <summary>
/// Gets active policies.
/// </summary>
Task<IReadOnlyList<TetragonPolicy>> GetPoliciesAsync(CancellationToken ct = default);
}
/// <summary>
/// Tetragon health status.
/// </summary>
public sealed record TetragonHealthStatus
{
public required bool IsHealthy { get; init; }
public required string Status { get; init; }
public string? Version { get; init; }
public int ActivePolicyCount { get; init; }
}
/// <summary>
/// Tetragon policy information.
/// </summary>
public sealed record TetragonPolicy
{
public string? Name { get; init; }
public string? Namespace { get; init; }
public bool Enabled { get; init; }
}
/// <summary>
/// Configuration for the Tetragon gRPC client.
/// </summary>
public sealed record TetragonGrpcClientOptions
{
/// <summary>Configuration section name.</summary>
public const string SectionName = "Tetragon:GrpcClient";
/// <summary>Tetragon address (default: http://localhost:54321).</summary>
public string Address { get; init; } = "http://localhost:54321";
/// <summary>Connection timeout.</summary>
public TimeSpan ConnectionTimeout { get; init; } = TimeSpan.FromSeconds(30);
/// <summary>Request timeout.</summary>
public TimeSpan RequestTimeout { get; init; } = TimeSpan.FromSeconds(60);
}