warnings fixes, tests fixes, sprints completions
This commit is contained in:
@@ -1,3 +1,126 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Policy.Exceptions.Models;
|
||||
public sealed record ExceptionApplication{public Guid Id{get;init;}public Guid TenantId{get;init;}public required string ExceptionId{get;init;}public required string FindingId{get;init;}public string? VulnerabilityId{get;init;}public required string OriginalStatus{get;init;}public required string AppliedStatus{get;init;}public required string EffectName{get;init;}public required string EffectType{get;init;}public Guid? EvaluationRunId{get;init;}public string? PolicyBundleDigest{get;init;}public DateTimeOffset AppliedAt{get;init;}public ImmutableDictionary<string,string> Metadata{get;init;}=ImmutableDictionary<string,string>.Empty;public static ExceptionApplication Create(Guid tenantId,string exceptionId,string findingId,string originalStatus,string appliedStatus,string effectName,string effectType,string? vulnerabilityId=null,Guid? evaluationRunId=null,string? policyBundleDigest=null,ImmutableDictionary<string,string>? metadata=null){ArgumentException.ThrowIfNullOrWhiteSpace(exceptionId);ArgumentException.ThrowIfNullOrWhiteSpace(findingId);return new ExceptionApplication{Id=Guid.NewGuid(),TenantId=tenantId,ExceptionId=exceptionId,FindingId=findingId,VulnerabilityId=vulnerabilityId,OriginalStatus=originalStatus,AppliedStatus=appliedStatus,EffectName=effectName,EffectType=effectType,EvaluationRunId=evaluationRunId,PolicyBundleDigest=policyBundleDigest,AppliedAt=DateTimeOffset.UtcNow,Metadata=metadata??ImmutableDictionary<string,string>.Empty};}}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an application of an exception to a specific finding.
|
||||
/// </summary>
|
||||
public sealed record ExceptionApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this application.
|
||||
/// </summary>
|
||||
public Guid Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant identifier.
|
||||
/// </summary>
|
||||
public Guid TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The exception that was applied.
|
||||
/// </summary>
|
||||
public required string ExceptionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The finding this exception was applied to.
|
||||
/// </summary>
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional vulnerability identifier.
|
||||
/// </summary>
|
||||
public string? VulnerabilityId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The original status before the exception was applied.
|
||||
/// </summary>
|
||||
public required string OriginalStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The status after the exception was applied.
|
||||
/// </summary>
|
||||
public required string AppliedStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the exception effect.
|
||||
/// </summary>
|
||||
public required string EffectName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the exception effect.
|
||||
/// </summary>
|
||||
public required string EffectType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional evaluation run identifier.
|
||||
/// </summary>
|
||||
public Guid? EvaluationRunId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional policy bundle digest.
|
||||
/// </summary>
|
||||
public string? PolicyBundleDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when the exception was applied.
|
||||
/// </summary>
|
||||
public DateTimeOffset AppliedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata.
|
||||
/// </summary>
|
||||
public ImmutableDictionary<string, string> Metadata { get; init; } = ImmutableDictionary<string, string>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new exception application with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="tenantId">Tenant identifier.</param>
|
||||
/// <param name="exceptionId">Exception identifier.</param>
|
||||
/// <param name="findingId">Finding identifier.</param>
|
||||
/// <param name="originalStatus">Original status before exception.</param>
|
||||
/// <param name="appliedStatus">Status after exception.</param>
|
||||
/// <param name="effectName">Name of the effect.</param>
|
||||
/// <param name="effectType">Type of the effect.</param>
|
||||
/// <param name="applicationId">Application ID for determinism. Required.</param>
|
||||
/// <param name="appliedAt">Timestamp for determinism. Required.</param>
|
||||
/// <param name="vulnerabilityId">Optional vulnerability ID.</param>
|
||||
/// <param name="evaluationRunId">Optional evaluation run ID.</param>
|
||||
/// <param name="policyBundleDigest">Optional policy bundle digest.</param>
|
||||
/// <param name="metadata">Optional metadata.</param>
|
||||
public static ExceptionApplication Create(
|
||||
Guid tenantId,
|
||||
string exceptionId,
|
||||
string findingId,
|
||||
string originalStatus,
|
||||
string appliedStatus,
|
||||
string effectName,
|
||||
string effectType,
|
||||
Guid applicationId,
|
||||
DateTimeOffset appliedAt,
|
||||
string? vulnerabilityId = null,
|
||||
Guid? evaluationRunId = null,
|
||||
string? policyBundleDigest = null,
|
||||
ImmutableDictionary<string, string>? metadata = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(exceptionId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(findingId);
|
||||
|
||||
return new ExceptionApplication
|
||||
{
|
||||
Id = applicationId,
|
||||
TenantId = tenantId,
|
||||
ExceptionId = exceptionId,
|
||||
FindingId = findingId,
|
||||
VulnerabilityId = vulnerabilityId,
|
||||
OriginalStatus = originalStatus,
|
||||
AppliedStatus = appliedStatus,
|
||||
EffectName = effectName,
|
||||
EffectType = effectType,
|
||||
EvaluationRunId = evaluationRunId,
|
||||
PolicyBundleDigest = policyBundleDigest,
|
||||
AppliedAt = appliedAt,
|
||||
Metadata = metadata ?? ImmutableDictionary<string, string>.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
|
||||
namespace StellaOps.Policy.Exceptions.Models;
|
||||
|
||||
@@ -120,15 +121,17 @@ public sealed record ExceptionEvent
|
||||
public static ExceptionEvent ForCreated(
|
||||
string exceptionId,
|
||||
string actorId,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
string? description = null,
|
||||
string? clientInfo = null) => new()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
EventId = guidProvider.NewGuid(),
|
||||
ExceptionId = exceptionId,
|
||||
SequenceNumber = 1,
|
||||
EventType = ExceptionEventType.Created,
|
||||
ActorId = actorId,
|
||||
OccurredAt = DateTimeOffset.UtcNow,
|
||||
OccurredAt = timeProvider.GetUtcNow(),
|
||||
PreviousStatus = null,
|
||||
NewStatus = ExceptionStatus.Proposed,
|
||||
NewVersion = 1,
|
||||
@@ -144,15 +147,17 @@ public sealed record ExceptionEvent
|
||||
int sequenceNumber,
|
||||
string actorId,
|
||||
int newVersion,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
string? description = null,
|
||||
string? clientInfo = null) => new()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
EventId = guidProvider.NewGuid(),
|
||||
ExceptionId = exceptionId,
|
||||
SequenceNumber = sequenceNumber,
|
||||
EventType = ExceptionEventType.Approved,
|
||||
ActorId = actorId,
|
||||
OccurredAt = DateTimeOffset.UtcNow,
|
||||
OccurredAt = timeProvider.GetUtcNow(),
|
||||
PreviousStatus = ExceptionStatus.Proposed,
|
||||
NewStatus = ExceptionStatus.Approved,
|
||||
NewVersion = newVersion,
|
||||
@@ -169,15 +174,17 @@ public sealed record ExceptionEvent
|
||||
string actorId,
|
||||
int newVersion,
|
||||
ExceptionStatus previousStatus,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
string? description = null,
|
||||
string? clientInfo = null) => new()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
EventId = guidProvider.NewGuid(),
|
||||
ExceptionId = exceptionId,
|
||||
SequenceNumber = sequenceNumber,
|
||||
EventType = ExceptionEventType.Activated,
|
||||
ActorId = actorId,
|
||||
OccurredAt = DateTimeOffset.UtcNow,
|
||||
OccurredAt = timeProvider.GetUtcNow(),
|
||||
PreviousStatus = previousStatus,
|
||||
NewStatus = ExceptionStatus.Active,
|
||||
NewVersion = newVersion,
|
||||
@@ -195,14 +202,16 @@ public sealed record ExceptionEvent
|
||||
int newVersion,
|
||||
ExceptionStatus previousStatus,
|
||||
string reason,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
string? clientInfo = null) => new()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
EventId = guidProvider.NewGuid(),
|
||||
ExceptionId = exceptionId,
|
||||
SequenceNumber = sequenceNumber,
|
||||
EventType = ExceptionEventType.Revoked,
|
||||
ActorId = actorId,
|
||||
OccurredAt = DateTimeOffset.UtcNow,
|
||||
OccurredAt = timeProvider.GetUtcNow(),
|
||||
PreviousStatus = previousStatus,
|
||||
NewStatus = ExceptionStatus.Revoked,
|
||||
NewVersion = newVersion,
|
||||
@@ -217,14 +226,16 @@ public sealed record ExceptionEvent
|
||||
public static ExceptionEvent ForExpired(
|
||||
string exceptionId,
|
||||
int sequenceNumber,
|
||||
int newVersion) => new()
|
||||
int newVersion,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider) => new()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
EventId = guidProvider.NewGuid(),
|
||||
ExceptionId = exceptionId,
|
||||
SequenceNumber = sequenceNumber,
|
||||
EventType = ExceptionEventType.Expired,
|
||||
ActorId = "system",
|
||||
OccurredAt = DateTimeOffset.UtcNow,
|
||||
OccurredAt = timeProvider.GetUtcNow(),
|
||||
PreviousStatus = ExceptionStatus.Active,
|
||||
NewStatus = ExceptionStatus.Expired,
|
||||
NewVersion = newVersion,
|
||||
@@ -241,15 +252,17 @@ public sealed record ExceptionEvent
|
||||
int newVersion,
|
||||
DateTimeOffset previousExpiry,
|
||||
DateTimeOffset newExpiry,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
string? reason = null,
|
||||
string? clientInfo = null) => new()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
EventId = guidProvider.NewGuid(),
|
||||
ExceptionId = exceptionId,
|
||||
SequenceNumber = sequenceNumber,
|
||||
EventType = ExceptionEventType.Extended,
|
||||
ActorId = actorId,
|
||||
OccurredAt = DateTimeOffset.UtcNow,
|
||||
OccurredAt = timeProvider.GetUtcNow(),
|
||||
PreviousStatus = ExceptionStatus.Active,
|
||||
NewStatus = ExceptionStatus.Active,
|
||||
NewVersion = newVersion,
|
||||
|
||||
@@ -295,15 +295,19 @@ public sealed record ExceptionObject
|
||||
LastRecheckResult.RecommendedAction == RecheckAction.RequireReapproval;
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this exception is currently effective.
|
||||
/// Determines if this exception is currently effective at the given reference time.
|
||||
/// </summary>
|
||||
public bool IsEffective =>
|
||||
/// <param name="referenceTime">The time to evaluate against.</param>
|
||||
/// <returns>True if status is Active and not yet expired.</returns>
|
||||
public bool IsEffectiveAt(DateTimeOffset referenceTime) =>
|
||||
Status == ExceptionStatus.Active &&
|
||||
DateTimeOffset.UtcNow < ExpiresAt;
|
||||
referenceTime < ExpiresAt;
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this exception has expired.
|
||||
/// Determines if this exception has expired at the given reference time.
|
||||
/// </summary>
|
||||
public bool HasExpired =>
|
||||
DateTimeOffset.UtcNow >= ExpiresAt;
|
||||
/// <param name="referenceTime">The time to evaluate against.</param>
|
||||
/// <returns>True if the reference time is at or past the expiration.</returns>
|
||||
public bool HasExpiredAt(DateTimeOffset referenceTime) =>
|
||||
referenceTime >= ExpiresAt;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Exceptions.Models;
|
||||
|
||||
namespace StellaOps.Policy.Exceptions.Repositories;
|
||||
@@ -18,6 +19,8 @@ public sealed class PostgresExceptionRepository : IExceptionRepository
|
||||
{
|
||||
private readonly NpgsqlDataSource _dataSource;
|
||||
private readonly ILogger<PostgresExceptionRepository> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
@@ -30,10 +33,18 @@ public sealed class PostgresExceptionRepository : IExceptionRepository
|
||||
/// </summary>
|
||||
/// <param name="dataSource">The PostgreSQL data source.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public PostgresExceptionRepository(NpgsqlDataSource dataSource, ILogger<PostgresExceptionRepository> logger)
|
||||
/// <param name="timeProvider">The time provider for deterministic timestamps.</param>
|
||||
/// <param name="guidProvider">The GUID provider for deterministic IDs.</param>
|
||||
public PostgresExceptionRepository(
|
||||
NpgsqlDataSource dataSource,
|
||||
ILogger<PostgresExceptionRepository> logger,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -73,7 +84,7 @@ public sealed class PostgresExceptionRepository : IExceptionRepository
|
||||
""";
|
||||
|
||||
await using var insertCmd = new NpgsqlCommand(insertSql, connection, transaction);
|
||||
AddExceptionParameters(insertCmd, exception, Guid.NewGuid());
|
||||
AddExceptionParameters(insertCmd, exception, _guidProvider.NewGuid());
|
||||
|
||||
await using var reader = await insertCmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -523,7 +534,7 @@ public sealed class PostgresExceptionRepository : IExceptionRepository
|
||||
|
||||
#region Private Helper Methods
|
||||
|
||||
private static ExceptionEvent CreateEventForType(
|
||||
private ExceptionEvent CreateEventForType(
|
||||
ExceptionEventType eventType,
|
||||
string exceptionId,
|
||||
int sequenceNumber,
|
||||
@@ -536,12 +547,12 @@ public sealed class PostgresExceptionRepository : IExceptionRepository
|
||||
{
|
||||
return new ExceptionEvent
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
EventId = _guidProvider.NewGuid(),
|
||||
ExceptionId = exceptionId,
|
||||
SequenceNumber = sequenceNumber,
|
||||
EventType = eventType,
|
||||
ActorId = actorId,
|
||||
OccurredAt = DateTimeOffset.UtcNow,
|
||||
OccurredAt = _timeProvider.GetUtcNow(),
|
||||
PreviousStatus = previousStatus,
|
||||
NewStatus = newStatus,
|
||||
NewVersion = newVersion,
|
||||
|
||||
@@ -15,19 +15,22 @@ public sealed class EvidenceRequirementValidator : IEvidenceRequirementValidator
|
||||
private readonly ITrustScoreService _trustScoreService;
|
||||
private readonly IEvidenceSchemaValidator _schemaValidator;
|
||||
private readonly ILogger<EvidenceRequirementValidator> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public EvidenceRequirementValidator(
|
||||
IEvidenceHookRegistry hookRegistry,
|
||||
IAttestationVerifier attestationVerifier,
|
||||
ITrustScoreService trustScoreService,
|
||||
IEvidenceSchemaValidator schemaValidator,
|
||||
ILogger<EvidenceRequirementValidator> logger)
|
||||
ILogger<EvidenceRequirementValidator> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_hookRegistry = hookRegistry ?? throw new ArgumentNullException(nameof(hookRegistry));
|
||||
_attestationVerifier = attestationVerifier ?? throw new ArgumentNullException(nameof(attestationVerifier));
|
||||
_trustScoreService = trustScoreService ?? throw new ArgumentNullException(nameof(trustScoreService));
|
||||
_schemaValidator = schemaValidator ?? throw new ArgumentNullException(nameof(schemaValidator));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -106,7 +109,7 @@ public sealed class EvidenceRequirementValidator : IEvidenceRequirementValidator
|
||||
{
|
||||
if (hook.MaxAge.HasValue)
|
||||
{
|
||||
var age = DateTimeOffset.UtcNow - evidence.SubmittedAt;
|
||||
var age = _timeProvider.GetUtcNow() - evidence.SubmittedAt;
|
||||
if (age > hook.MaxAge.Value)
|
||||
{
|
||||
return (false, $"Evidence is stale (age: {age.TotalHours:F0}h, max: {hook.MaxAge.Value.TotalHours:F0}h)");
|
||||
|
||||
@@ -86,10 +86,14 @@ public interface IExceptionEvaluator
|
||||
public sealed class ExceptionEvaluator : IExceptionEvaluator
|
||||
{
|
||||
private readonly IExceptionRepository _repository;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public ExceptionEvaluator(IExceptionRepository repository)
|
||||
public ExceptionEvaluator(
|
||||
IExceptionRepository repository,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_repository = repository;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -114,8 +118,9 @@ public sealed class ExceptionEvaluator : IExceptionEvaluator
|
||||
var candidates = await _repository.GetActiveByScopeAsync(scope, cancellationToken);
|
||||
|
||||
// Filter to only those that truly match the context
|
||||
var referenceTime = _timeProvider.GetUtcNow();
|
||||
var matching = candidates
|
||||
.Where(ex => MatchesContext(ex, context))
|
||||
.Where(ex => MatchesContext(ex, context, referenceTime))
|
||||
.OrderByDescending(ex => GetSpecificity(ex))
|
||||
.ToList();
|
||||
|
||||
@@ -160,7 +165,7 @@ public sealed class ExceptionEvaluator : IExceptionEvaluator
|
||||
/// <summary>
|
||||
/// Determines if an exception matches the given finding context.
|
||||
/// </summary>
|
||||
private static bool MatchesContext(ExceptionObject exception, FindingContext context)
|
||||
private static bool MatchesContext(ExceptionObject exception, FindingContext context, DateTimeOffset referenceTime)
|
||||
{
|
||||
var scope = exception.Scope;
|
||||
|
||||
@@ -207,7 +212,7 @@ public sealed class ExceptionEvaluator : IExceptionEvaluator
|
||||
}
|
||||
|
||||
// Check if exception is still effective (not expired)
|
||||
if (!exception.IsEffective)
|
||||
if (!exception.IsEffectiveAt(referenceTime))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user