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,411 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// KeyRotationAuditRepository.cs
|
||||
// Sprint: SPRINT_20260118_018_AirGap_router_integration
|
||||
// Task: TASK-018-007 - Key Rotation Tracking
|
||||
// Description: Repository for key rotation audit with PostgreSQL storage
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Signer.KeyManagement.Entities;
|
||||
|
||||
namespace StellaOps.Signer.KeyManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Key rotation audit event types.
|
||||
/// </summary>
|
||||
public static class KeyAuditEventType
|
||||
{
|
||||
public const string Created = "created";
|
||||
public const string Activated = "activated";
|
||||
public const string Rotated = "rotated";
|
||||
public const string Revoked = "revoked";
|
||||
public const string Expired = "expired";
|
||||
public const string SignaturePerformed = "signature_performed";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audit entry for key operations.
|
||||
/// </summary>
|
||||
public sealed record KeyRotationAuditEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Audit entry ID.
|
||||
/// </summary>
|
||||
public Guid AuditId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Key fingerprint (SHA-256 of public key).
|
||||
/// </summary>
|
||||
public required string KeyFingerprint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Event type (created, activated, rotated, revoked).
|
||||
/// </summary>
|
||||
public required string EventType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Event timestamp.
|
||||
/// </summary>
|
||||
public DateTimeOffset EventTimestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Previous key fingerprint (for rotation events).
|
||||
/// </summary>
|
||||
public string? PreviousKeyFingerprint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for the event.
|
||||
/// </summary>
|
||||
public string? Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Actor who performed the operation.
|
||||
/// </summary>
|
||||
public required string Actor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata.
|
||||
/// </summary>
|
||||
public ImmutableDictionary<string, string> Metadata { get; init; } = ImmutableDictionary<string, string>.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Key usage statistics.
|
||||
/// </summary>
|
||||
public sealed record KeyUsageStats
|
||||
{
|
||||
/// <summary>
|
||||
/// Key fingerprint.
|
||||
/// </summary>
|
||||
public required string KeyFingerprint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of signatures performed.
|
||||
/// </summary>
|
||||
public long SignatureCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// First signature timestamp.
|
||||
/// </summary>
|
||||
public DateTimeOffset? FirstSignatureAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Last signature timestamp.
|
||||
/// </summary>
|
||||
public DateTimeOffset? LastSignatureAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Key status.
|
||||
/// </summary>
|
||||
public string Status { get; init; } = "active";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for key rotation audit.
|
||||
/// </summary>
|
||||
public interface IKeyRotationAuditRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Records an audit event.
|
||||
/// </summary>
|
||||
Task RecordEventAsync(KeyRotationAuditEntry entry, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets audit entries for a key fingerprint.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<KeyRotationAuditEntry>> GetAuditTrailAsync(
|
||||
string keyFingerprint,
|
||||
DateTimeOffset? from = null,
|
||||
DateTimeOffset? to = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets key usage statistics.
|
||||
/// </summary>
|
||||
Task<KeyUsageStats?> GetKeyUsageAsync(string keyFingerprint, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Records a signature event.
|
||||
/// </summary>
|
||||
Task RecordSignatureAsync(
|
||||
string keyFingerprint,
|
||||
string artifactDigest,
|
||||
string actor,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all keys with their current status.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<KeyUsageStats>> GetAllKeyStatsAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets keys approaching expiry.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<KeyExpiryWarning>> GetExpiryWarningsAsync(
|
||||
TimeSpan warningThreshold,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Key expiry warning.
|
||||
/// </summary>
|
||||
public sealed record KeyExpiryWarning
|
||||
{
|
||||
public required string KeyFingerprint { get; init; }
|
||||
public required string KeyId { get; init; }
|
||||
public DateTimeOffset ExpiresAt { get; init; }
|
||||
public TimeSpan TimeUntilExpiry { get; init; }
|
||||
public string Severity { get; init; } = "warning"; // warning, critical
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of key rotation audit repository.
|
||||
/// </summary>
|
||||
public sealed class PostgresKeyRotationAuditRepository : IKeyRotationAuditRepository
|
||||
{
|
||||
private readonly KeyManagementDbContext _dbContext;
|
||||
private readonly ILogger<PostgresKeyRotationAuditRepository> _logger;
|
||||
|
||||
public PostgresKeyRotationAuditRepository(
|
||||
KeyManagementDbContext dbContext,
|
||||
ILogger<PostgresKeyRotationAuditRepository> logger)
|
||||
{
|
||||
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task RecordEventAsync(KeyRotationAuditEntry entry, CancellationToken ct = default)
|
||||
{
|
||||
var entity = new KeyAuditLogEntity
|
||||
{
|
||||
LogId = entry.AuditId == Guid.Empty ? Guid.NewGuid() : entry.AuditId,
|
||||
AnchorId = Guid.Empty, // Will be resolved from key
|
||||
KeyId = entry.KeyFingerprint,
|
||||
Operation = entry.EventType,
|
||||
Actor = entry.Actor,
|
||||
Reason = entry.Reason,
|
||||
Metadata = entry.Metadata.Count > 0
|
||||
? JsonSerializer.Serialize(entry.Metadata)
|
||||
: null,
|
||||
Details = entry.PreviousKeyFingerprint != null
|
||||
? JsonSerializer.Serialize(new { previousKey = entry.PreviousKeyFingerprint })
|
||||
: null,
|
||||
CreatedAt = entry.EventTimestamp
|
||||
};
|
||||
|
||||
_dbContext.KeyAuditLog.Add(entity);
|
||||
await _dbContext.SaveChangesAsync(ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Recorded key audit event: {EventType} for {KeyFingerprint} by {Actor}",
|
||||
entry.EventType,
|
||||
entry.KeyFingerprint[..Math.Min(16, entry.KeyFingerprint.Length)],
|
||||
entry.Actor);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<KeyRotationAuditEntry>> GetAuditTrailAsync(
|
||||
string keyFingerprint,
|
||||
DateTimeOffset? from = null,
|
||||
DateTimeOffset? to = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var query = _dbContext.KeyAuditLog
|
||||
.Where(l => l.KeyId == keyFingerprint);
|
||||
|
||||
if (from.HasValue)
|
||||
{
|
||||
query = query.Where(l => l.CreatedAt >= from.Value);
|
||||
}
|
||||
|
||||
if (to.HasValue)
|
||||
{
|
||||
query = query.Where(l => l.CreatedAt <= to.Value);
|
||||
}
|
||||
|
||||
var entities = await query
|
||||
.OrderByDescending(l => l.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return entities.Select(e => new KeyRotationAuditEntry
|
||||
{
|
||||
AuditId = e.LogId,
|
||||
KeyFingerprint = e.KeyId ?? "",
|
||||
EventType = e.Operation,
|
||||
EventTimestamp = e.CreatedAt,
|
||||
PreviousKeyFingerprint = ParsePreviousKey(e.Details),
|
||||
Reason = e.Reason,
|
||||
Actor = e.Actor ?? "unknown",
|
||||
Metadata = ParseMetadata(e.Metadata)
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<KeyUsageStats?> GetKeyUsageAsync(string keyFingerprint, CancellationToken ct = default)
|
||||
{
|
||||
var signatureEvents = await _dbContext.KeyAuditLog
|
||||
.Where(l => l.KeyId == keyFingerprint && l.Operation == KeyAuditEventType.SignaturePerformed)
|
||||
.ToListAsync(ct);
|
||||
|
||||
if (signatureEvents.Count == 0)
|
||||
{
|
||||
// Check if key exists
|
||||
var keyExists = await _dbContext.KeyHistory
|
||||
.AnyAsync(k => k.KeyId == keyFingerprint || k.PublicKey.Contains(keyFingerprint), ct);
|
||||
|
||||
if (!keyExists)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new KeyUsageStats
|
||||
{
|
||||
KeyFingerprint = keyFingerprint,
|
||||
SignatureCount = 0,
|
||||
Status = "active"
|
||||
};
|
||||
}
|
||||
|
||||
var key = await _dbContext.KeyHistory
|
||||
.FirstOrDefaultAsync(k => k.KeyId == keyFingerprint, ct);
|
||||
|
||||
var status = key?.RevokedAt != null ? "revoked" :
|
||||
key?.ExpiresAt < DateTimeOffset.UtcNow ? "expired" : "active";
|
||||
|
||||
return new KeyUsageStats
|
||||
{
|
||||
KeyFingerprint = keyFingerprint,
|
||||
SignatureCount = signatureEvents.Count,
|
||||
FirstSignatureAt = signatureEvents.Min(e => e.CreatedAt),
|
||||
LastSignatureAt = signatureEvents.Max(e => e.CreatedAt),
|
||||
Status = status
|
||||
};
|
||||
}
|
||||
|
||||
public async Task RecordSignatureAsync(
|
||||
string keyFingerprint,
|
||||
string artifactDigest,
|
||||
string actor,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var entry = new KeyRotationAuditEntry
|
||||
{
|
||||
AuditId = Guid.NewGuid(),
|
||||
KeyFingerprint = keyFingerprint,
|
||||
EventType = KeyAuditEventType.SignaturePerformed,
|
||||
EventTimestamp = DateTimeOffset.UtcNow,
|
||||
Actor = actor,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["artifactDigest"] = artifactDigest
|
||||
}.ToImmutableDictionary()
|
||||
};
|
||||
|
||||
await RecordEventAsync(entry, ct);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<KeyUsageStats>> GetAllKeyStatsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var keys = await _dbContext.KeyHistory
|
||||
.ToListAsync(ct);
|
||||
|
||||
var signatureCounts = await _dbContext.KeyAuditLog
|
||||
.Where(l => l.Operation == KeyAuditEventType.SignaturePerformed)
|
||||
.GroupBy(l => l.KeyId)
|
||||
.Select(g => new { KeyId = g.Key, Count = g.Count() })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var countDict = signatureCounts.ToDictionary(x => x.KeyId ?? "", x => x.Count);
|
||||
|
||||
return keys.Select(k =>
|
||||
{
|
||||
var status = k.RevokedAt != null ? "revoked" :
|
||||
k.ExpiresAt < DateTimeOffset.UtcNow ? "expired" : "active";
|
||||
|
||||
countDict.TryGetValue(k.KeyId, out var count);
|
||||
|
||||
return new KeyUsageStats
|
||||
{
|
||||
KeyFingerprint = k.KeyId,
|
||||
SignatureCount = count,
|
||||
Status = status
|
||||
};
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<KeyExpiryWarning>> GetExpiryWarningsAsync(
|
||||
TimeSpan warningThreshold,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var warningDate = now + warningThreshold;
|
||||
var criticalDate = now + TimeSpan.FromDays(7);
|
||||
|
||||
var expiringKeys = await _dbContext.KeyHistory
|
||||
.Where(k => k.RevokedAt == null &&
|
||||
k.ExpiresAt != null &&
|
||||
k.ExpiresAt <= warningDate)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return expiringKeys.Select(k => new KeyExpiryWarning
|
||||
{
|
||||
KeyFingerprint = ComputeFingerprint(k.PublicKey),
|
||||
KeyId = k.KeyId,
|
||||
ExpiresAt = k.ExpiresAt!.Value,
|
||||
TimeUntilExpiry = k.ExpiresAt!.Value - now,
|
||||
Severity = k.ExpiresAt <= criticalDate ? "critical" : "warning"
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private static string? ParsePreviousKey(string? details)
|
||||
{
|
||||
if (string.IsNullOrEmpty(details)) return null;
|
||||
|
||||
try
|
||||
{
|
||||
var doc = JsonDocument.Parse(details);
|
||||
if (doc.RootElement.TryGetProperty("previousKey", out var prop))
|
||||
{
|
||||
return prop.GetString();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore parse errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ImmutableDictionary<string, string> ParseMetadata(string? metadata)
|
||||
{
|
||||
if (string.IsNullOrEmpty(metadata))
|
||||
{
|
||||
return ImmutableDictionary<string, string>.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(metadata);
|
||||
return dict?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ImmutableDictionary<string, string>.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeFingerprint(string publicKey)
|
||||
{
|
||||
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(publicKey);
|
||||
var hash = sha256.ComputeHash(bytes);
|
||||
return Convert.ToHexStringLower(hash)[..32];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user