152 lines
4.7 KiB
C#
152 lines
4.7 KiB
C#
// <copyright file="ActionAuditLedger.cs" company="StellaOps">
|
|
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
|
// </copyright>
|
|
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Immutable;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace StellaOps.AdvisoryAI.Actions;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
internal sealed class ActionAuditLedger : IActionAuditLedger
|
|
{
|
|
private readonly ConcurrentDictionary<string, ActionAuditEntry> _entries = new();
|
|
private readonly ILogger<ActionAuditLedger> _logger;
|
|
private readonly AuditLedgerOptions _options;
|
|
|
|
public ActionAuditLedger(
|
|
IOptions<AuditLedgerOptions> options,
|
|
ILogger<ActionAuditLedger> logger)
|
|
{
|
|
_options = options?.Value ?? new AuditLedgerOptions();
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<ImmutableArray<ActionAuditEntry>> 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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<ActionAuditEntry?> GetAsync(
|
|
string entryId,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(entryId);
|
|
|
|
_entries.TryGetValue(entryId, out var entry);
|
|
return Task.FromResult(entry);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<ImmutableArray<ActionAuditEntry>> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configuration options for the audit ledger.
|
|
/// </summary>
|
|
public sealed class AuditLedgerOptions
|
|
{
|
|
/// <summary>
|
|
/// Days to retain audit entries.
|
|
/// </summary>
|
|
public int RetentionDays { get; set; } = 365;
|
|
|
|
/// <summary>
|
|
/// Whether audit logging is enabled.
|
|
/// </summary>
|
|
public bool Enabled { get; set; } = true;
|
|
}
|