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,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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user