// // Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later. // using System.Collections.Concurrent; using System.Collections.Immutable; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace StellaOps.AdvisoryAI.Actions; /// /// In-memory action audit ledger for development and testing. /// In production, this would use PostgreSQL with proper indexing. /// Sprint: SPRINT_20260109_011_004_BE Task PACT-006 /// internal sealed class ActionAuditLedger : IActionAuditLedger { private readonly ConcurrentDictionary _entries = new(); private readonly ILogger _logger; private readonly AuditLedgerOptions _options; public ActionAuditLedger( IOptions options, ILogger logger) { _options = options?.Value ?? new AuditLedgerOptions(); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// public Task RecordAsync(ActionAuditEntry entry, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(entry); _entries[entry.EntryId] = entry; _logger.LogDebug( "Recorded audit entry {EntryId}: {ActionType} by {Actor} -> {Outcome}", entry.EntryId, entry.ActionType, entry.Actor, entry.Outcome); return Task.CompletedTask; } /// public Task> QueryAsync( ActionAuditQuery query, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(query); var entries = _entries.Values.AsEnumerable(); // Apply filters if (!string.IsNullOrEmpty(query.TenantId)) { entries = entries.Where(e => e.TenantId.Equals(query.TenantId, StringComparison.OrdinalIgnoreCase)); } if (!string.IsNullOrEmpty(query.ActionType)) { entries = entries.Where(e => e.ActionType.Equals(query.ActionType, StringComparison.OrdinalIgnoreCase)); } if (!string.IsNullOrEmpty(query.Actor)) { entries = entries.Where(e => e.Actor.Equals(query.Actor, StringComparison.OrdinalIgnoreCase)); } if (query.Outcome.HasValue) { entries = entries.Where(e => e.Outcome == query.Outcome.Value); } if (!string.IsNullOrEmpty(query.RunId)) { entries = entries.Where(e => e.RunId != null && e.RunId.Equals(query.RunId, StringComparison.OrdinalIgnoreCase)); } if (!string.IsNullOrEmpty(query.CveId)) { entries = entries.Where(e => e.CveId != null && e.CveId.Equals(query.CveId, StringComparison.OrdinalIgnoreCase)); } if (!string.IsNullOrEmpty(query.ImageDigest)) { entries = entries.Where(e => e.ImageDigest != null && e.ImageDigest.Equals(query.ImageDigest, StringComparison.OrdinalIgnoreCase)); } if (query.FromTimestamp.HasValue) { entries = entries.Where(e => e.Timestamp >= query.FromTimestamp.Value); } if (query.ToTimestamp.HasValue) { entries = entries.Where(e => e.Timestamp < query.ToTimestamp.Value); } // Order by timestamp descending, apply pagination var result = entries .OrderByDescending(e => e.Timestamp) .Skip(query.Offset) .Take(query.Limit) .ToImmutableArray(); return Task.FromResult(result); } /// public Task GetAsync( string entryId, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrEmpty(entryId); _entries.TryGetValue(entryId, out var entry); return Task.FromResult(entry); } /// public Task> GetByRunAsync( string runId, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrEmpty(runId); var entries = _entries.Values .Where(e => e.RunId != null && e.RunId.Equals(runId, StringComparison.OrdinalIgnoreCase)) .OrderBy(e => e.Timestamp) .ToImmutableArray(); return Task.FromResult(entries); } } /// /// Configuration options for the audit ledger. /// public sealed class AuditLedgerOptions { /// /// Days to retain audit entries. /// public int RetentionDays { get; set; } = 365; /// /// Whether audit logging is enabled. /// public bool Enabled { get; set; } = true; }