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,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];
}
}