warnings fixes, tests fixes, sprints completions

This commit is contained in:
Codex Assistant
2026-01-08 08:38:27 +02:00
parent 75611a505f
commit 0b5d786ddb
125 changed files with 14610 additions and 368 deletions

View File

@@ -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
};
}
}

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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)");

View File

@@ -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;

View File

@@ -22,8 +22,9 @@ public static class LegacyDocumentConverter
/// Converts a legacy PolicyDocument (as JSON) to PackMigrationData.
/// </summary>
/// <param name="json">The JSON representation of the legacy document.</param>
/// <param name="migrationTimestamp">Timestamp to use for missing dates in legacy documents.</param>
/// <returns>Migration data transfer object.</returns>
public static PackMigrationData ConvertPackFromJson(string json)
public static PackMigrationData ConvertPackFromJson(string json, DateTimeOffset migrationTimestamp)
{
ArgumentException.ThrowIfNullOrEmpty(json);
@@ -41,8 +42,8 @@ public static class LegacyDocumentConverter
LatestVersion = GetInt(root, "latestVersion", 0),
IsBuiltin = GetBool(root, "isBuiltin", false),
Metadata = ExtractMetadata(root),
CreatedAt = GetDateTimeOffset(root, "createdAt", DateTimeOffset.UtcNow),
UpdatedAt = GetDateTimeOffset(root, "updatedAt", DateTimeOffset.UtcNow),
CreatedAt = GetDateTimeOffset(root, "createdAt", migrationTimestamp),
UpdatedAt = GetDateTimeOffset(root, "updatedAt", migrationTimestamp),
CreatedBy = GetString(root, "createdBy")
};
}
@@ -51,8 +52,9 @@ public static class LegacyDocumentConverter
/// Converts a legacy PolicyRevisionDocument (as JSON) to PackVersionMigrationData.
/// </summary>
/// <param name="json">The JSON representation of the legacy document.</param>
/// <param name="migrationTimestamp">Timestamp to use for missing dates in legacy documents.</param>
/// <returns>Migration data transfer object.</returns>
public static PackVersionMigrationData ConvertVersionFromJson(string json)
public static PackVersionMigrationData ConvertVersionFromJson(string json, DateTimeOffset migrationTimestamp)
{
ArgumentException.ThrowIfNullOrEmpty(json);
@@ -71,7 +73,7 @@ public static class LegacyDocumentConverter
IsPublished = isPublished,
PublishedAt = isPublished ? GetNullableDateTimeOffset(root, "activatedAt") : null,
PublishedBy = GetString(root, "publishedBy"),
CreatedAt = GetDateTimeOffset(root, "createdAt", DateTimeOffset.UtcNow),
CreatedAt = GetDateTimeOffset(root, "createdAt", migrationTimestamp),
CreatedBy = GetString(root, "createdBy")
};
}
@@ -81,11 +83,13 @@ public static class LegacyDocumentConverter
/// </summary>
/// <param name="name">Rule name.</param>
/// <param name="content">Rego content.</param>
/// <param name="migrationTimestamp">Timestamp to use for creation date.</param>
/// <param name="severity">Optional severity.</param>
/// <returns>Rule migration data.</returns>
public static RuleMigrationData CreateRuleFromContent(
string name,
string content,
DateTimeOffset migrationTimestamp,
string? severity = null)
{
return new RuleMigrationData
@@ -94,7 +98,7 @@ public static class LegacyDocumentConverter
Content = content,
RuleType = "rego",
Severity = severity ?? "medium",
CreatedAt = DateTimeOffset.UtcNow
CreatedAt = migrationTimestamp
};
}
@@ -102,8 +106,9 @@ public static class LegacyDocumentConverter
/// Parses multiple pack documents from a JSON array.
/// </summary>
/// <param name="jsonArray">JSON array of pack documents.</param>
/// <param name="migrationTimestamp">Timestamp to use for missing dates in legacy documents.</param>
/// <returns>List of migration data objects.</returns>
public static IReadOnlyList<PackMigrationData> ConvertPacksFromJsonArray(string jsonArray)
public static IReadOnlyList<PackMigrationData> ConvertPacksFromJsonArray(string jsonArray, DateTimeOffset migrationTimestamp)
{
ArgumentException.ThrowIfNullOrEmpty(jsonArray);
@@ -117,7 +122,7 @@ public static class LegacyDocumentConverter
foreach (var element in doc.RootElement.EnumerateArray())
{
results.Add(ConvertPackElement(element));
results.Add(ConvertPackElement(element, migrationTimestamp));
}
return results;
@@ -127,8 +132,9 @@ public static class LegacyDocumentConverter
/// Parses multiple version documents from a JSON array.
/// </summary>
/// <param name="jsonArray">JSON array of version documents.</param>
/// <param name="migrationTimestamp">Timestamp to use for missing dates in legacy documents.</param>
/// <returns>List of migration data objects.</returns>
public static IReadOnlyList<PackVersionMigrationData> ConvertVersionsFromJsonArray(string jsonArray)
public static IReadOnlyList<PackVersionMigrationData> ConvertVersionsFromJsonArray(string jsonArray, DateTimeOffset migrationTimestamp)
{
ArgumentException.ThrowIfNullOrEmpty(jsonArray);
@@ -142,13 +148,13 @@ public static class LegacyDocumentConverter
foreach (var element in doc.RootElement.EnumerateArray())
{
results.Add(ConvertVersionElement(element));
results.Add(ConvertVersionElement(element, migrationTimestamp));
}
return results;
}
private static PackMigrationData ConvertPackElement(JsonElement root)
private static PackMigrationData ConvertPackElement(JsonElement root, DateTimeOffset migrationTimestamp)
{
return new PackMigrationData
{
@@ -161,13 +167,13 @@ public static class LegacyDocumentConverter
LatestVersion = GetInt(root, "latestVersion", 0),
IsBuiltin = GetBool(root, "isBuiltin", false),
Metadata = ExtractMetadata(root),
CreatedAt = GetDateTimeOffset(root, "createdAt", DateTimeOffset.UtcNow),
UpdatedAt = GetDateTimeOffset(root, "updatedAt", DateTimeOffset.UtcNow),
CreatedAt = GetDateTimeOffset(root, "createdAt", migrationTimestamp),
UpdatedAt = GetDateTimeOffset(root, "updatedAt", migrationTimestamp),
CreatedBy = GetString(root, "createdBy")
};
}
private static PackVersionMigrationData ConvertVersionElement(JsonElement root)
private static PackVersionMigrationData ConvertVersionElement(JsonElement root, DateTimeOffset migrationTimestamp)
{
var status = GetString(root, "status") ?? "Draft";
var isPublished = status == "Active" || status == "Approved";
@@ -181,7 +187,7 @@ public static class LegacyDocumentConverter
IsPublished = isPublished,
PublishedAt = isPublished ? GetNullableDateTimeOffset(root, "activatedAt") : null,
PublishedBy = GetString(root, "publishedBy"),
CreatedAt = GetDateTimeOffset(root, "createdAt", DateTimeOffset.UtcNow),
CreatedAt = GetDateTimeOffset(root, "createdAt", migrationTimestamp),
CreatedBy = GetString(root, "createdBy")
};
}

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using StellaOps.Determinism.Abstractions;
using StellaOps.Policy.Persistence.Postgres.Models;
using StellaOps.Policy.Persistence.Postgres.Repositories;
@@ -18,17 +19,23 @@ public sealed class PolicyMigrator
private readonly IPackVersionRepository _versionRepository;
private readonly IRuleRepository _ruleRepository;
private readonly ILogger<PolicyMigrator> _logger;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public PolicyMigrator(
IPackRepository packRepository,
IPackVersionRepository versionRepository,
IRuleRepository ruleRepository,
ILogger<PolicyMigrator> logger)
ILogger<PolicyMigrator> logger,
TimeProvider timeProvider,
IGuidProvider guidProvider)
{
_packRepository = packRepository ?? throw new ArgumentNullException(nameof(packRepository));
_versionRepository = versionRepository ?? throw new ArgumentNullException(nameof(versionRepository));
_ruleRepository = ruleRepository ?? throw new ArgumentNullException(nameof(ruleRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
}
/// <summary>
@@ -76,7 +83,7 @@ public sealed class PolicyMigrator
// Create pack entity
var packEntity = new PackEntity
{
Id = Guid.NewGuid(),
Id = _guidProvider.NewGuid(),
TenantId = pack.TenantId,
Name = pack.Name,
DisplayName = pack.DisplayName,
@@ -154,7 +161,7 @@ public sealed class PolicyMigrator
var versionEntity = new PackVersionEntity
{
Id = Guid.NewGuid(),
Id = _guidProvider.NewGuid(),
PackId = packId,
Version = version.Version,
Description = version.Description,
@@ -176,7 +183,7 @@ public sealed class PolicyMigrator
{
var ruleEntity = new RuleEntity
{
Id = Guid.NewGuid(),
Id = _guidProvider.NewGuid(),
PackVersionId = createdVersion.Id,
Name = rule.Name,
Description = rule.Description,
@@ -187,7 +194,7 @@ public sealed class PolicyMigrator
Category = rule.Category,
Tags = rule.Tags ?? [],
Metadata = rule.Metadata ?? "{}",
CreatedAt = rule.CreatedAt ?? DateTimeOffset.UtcNow
CreatedAt = rule.CreatedAt ?? _timeProvider.GetUtcNow()
};
await _ruleRepository.CreateAsync(ruleEntity, cancellationToken);

View File

@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Determinism.Abstractions;
using StellaOps.Infrastructure.Postgres.Repositories;
using StellaOps.Policy.Persistence.Postgres.Models;
@@ -10,9 +11,18 @@ namespace StellaOps.Policy.Persistence.Postgres.Repositories;
/// </summary>
public sealed class ExceptionApprovalRepository : RepositoryBase<PolicyDataSource>, IExceptionApprovalRepository
{
public ExceptionApprovalRepository(PolicyDataSource dataSource, ILogger<ExceptionApprovalRepository> logger)
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public ExceptionApprovalRepository(
PolicyDataSource dataSource,
ILogger<ExceptionApprovalRepository> logger,
TimeProvider timeProvider,
IGuidProvider guidProvider)
: base(dataSource, logger)
{
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
}
// ========================================================================
@@ -279,13 +289,14 @@ public sealed class ExceptionApprovalRepository : RepositoryBase<PolicyDataSourc
? ApprovalRequestStatus.Approved
: ApprovalRequestStatus.Partial;
var now = _timeProvider.GetUtcNow();
var updated = request with
{
ApprovedByIds = approvedByIds,
Status = newStatus,
ResolvedAt = newStatus == ApprovalRequestStatus.Approved ? DateTimeOffset.UtcNow : null,
ResolvedAt = newStatus == ApprovalRequestStatus.Approved ? now : null,
Version = request.Version + 1,
UpdatedAt = DateTimeOffset.UtcNow
UpdatedAt = now
};
if (await UpdateRequestAsync(updated, request.Version, ct))
@@ -293,13 +304,13 @@ public sealed class ExceptionApprovalRepository : RepositoryBase<PolicyDataSourc
// Record audit entry
await RecordAuditAsync(new ExceptionApprovalAuditEntity
{
Id = Guid.NewGuid(),
Id = _guidProvider.NewGuid(),
RequestId = requestId,
TenantId = tenantId,
SequenceNumber = await GetNextSequenceAsync(tenantId, requestId, ct),
ActionType = "approved",
ActorId = approverId,
OccurredAt = DateTimeOffset.UtcNow,
OccurredAt = now,
PreviousStatus = request.Status.ToString().ToLowerInvariant(),
NewStatus = newStatus.ToString().ToLowerInvariant(),
Description = comment ?? $"Approved by {approverId}"
@@ -325,27 +336,28 @@ public sealed class ExceptionApprovalRepository : RepositoryBase<PolicyDataSourc
if (request.Status is not (ApprovalRequestStatus.Pending or ApprovalRequestStatus.Partial))
return request;
var now = _timeProvider.GetUtcNow();
var updated = request with
{
RejectedById = rejectorId,
Status = ApprovalRequestStatus.Rejected,
ResolvedAt = DateTimeOffset.UtcNow,
ResolvedAt = now,
RejectionReason = reason,
Version = request.Version + 1,
UpdatedAt = DateTimeOffset.UtcNow
UpdatedAt = now
};
if (await UpdateRequestAsync(updated, request.Version, ct))
{
await RecordAuditAsync(new ExceptionApprovalAuditEntity
{
Id = Guid.NewGuid(),
Id = _guidProvider.NewGuid(),
RequestId = requestId,
TenantId = tenantId,
SequenceNumber = await GetNextSequenceAsync(tenantId, requestId, ct),
ActionType = "rejected",
ActorId = rejectorId,
OccurredAt = DateTimeOffset.UtcNow,
OccurredAt = now,
PreviousStatus = request.Status.ToString().ToLowerInvariant(),
NewStatus = "rejected",
Description = reason
@@ -371,25 +383,26 @@ public sealed class ExceptionApprovalRepository : RepositoryBase<PolicyDataSourc
if (request.Status is not (ApprovalRequestStatus.Pending or ApprovalRequestStatus.Partial))
return false;
var now = _timeProvider.GetUtcNow();
var updated = request with
{
Status = ApprovalRequestStatus.Cancelled,
ResolvedAt = DateTimeOffset.UtcNow,
ResolvedAt = now,
Version = request.Version + 1,
UpdatedAt = DateTimeOffset.UtcNow
UpdatedAt = now
};
if (await UpdateRequestAsync(updated, request.Version, ct))
{
await RecordAuditAsync(new ExceptionApprovalAuditEntity
{
Id = Guid.NewGuid(),
Id = _guidProvider.NewGuid(),
RequestId = requestId,
TenantId = tenantId,
SequenceNumber = await GetNextSequenceAsync(tenantId, requestId, ct),
ActionType = "cancelled",
ActorId = actorId,
OccurredAt = DateTimeOffset.UtcNow,
OccurredAt = now,
PreviousStatus = request.Status.ToString().ToLowerInvariant(),
NewStatus = "cancelled",
Description = reason ?? "Request cancelled by requestor"

View File

@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Determinism.Abstractions;
using StellaOps.Infrastructure.Postgres.Repositories;
using StellaOps.Policy.Persistence.Postgres.Models;
@@ -10,8 +11,16 @@ namespace StellaOps.Policy.Persistence.Postgres.Repositories;
/// </summary>
public sealed class ExplanationRepository : RepositoryBase<PolicyDataSource>, IExplanationRepository
{
public ExplanationRepository(PolicyDataSource dataSource, ILogger<ExplanationRepository> logger)
: base(dataSource, logger) { }
private readonly IGuidProvider _guidProvider;
public ExplanationRepository(
PolicyDataSource dataSource,
ILogger<ExplanationRepository> logger,
IGuidProvider guidProvider)
: base(dataSource, logger)
{
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
}
public async Task<ExplanationEntity?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
@@ -68,7 +77,7 @@ public sealed class ExplanationRepository : RepositoryBase<PolicyDataSource>, IE
VALUES (@id, @evaluation_run_id, @rule_id, @rule_name, @result, @severity, @message, @details::jsonb, @remediation, @resource_path, @line_number)
RETURNING *
""";
var id = explanation.Id == Guid.Empty ? Guid.NewGuid() : explanation.Id;
var id = explanation.Id == Guid.Empty ? _guidProvider.NewGuid() : explanation.Id;
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "id", id);
@@ -99,7 +108,7 @@ public sealed class ExplanationRepository : RepositoryBase<PolicyDataSource>, IE
foreach (var explanation in explanations)
{
await using var command = CreateCommand(sql, connection);
var id = explanation.Id == Guid.Empty ? Guid.NewGuid() : explanation.Id;
var id = explanation.Id == Guid.Empty ? _guidProvider.NewGuid() : explanation.Id;
AddParameter(command, "id", id);
AddParameter(command, "evaluation_run_id", explanation.EvaluationRunId);
AddParameter(command, "rule_id", explanation.RuleId);

View File

@@ -3,6 +3,7 @@ using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Determinism.Abstractions;
using StellaOps.Infrastructure.Postgres.Repositories;
using StellaOps.Policy.Exceptions.Models;
using StellaOps.Policy.Exceptions.Repositories;
@@ -19,6 +20,9 @@ namespace StellaOps.Policy.Persistence.Postgres.Repositories;
/// </remarks>
public sealed class PostgresExceptionObjectRepository : RepositoryBase<PolicyDataSource>, IAuditableExceptionRepository
{
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
@@ -28,9 +32,15 @@ public sealed class PostgresExceptionObjectRepository : RepositoryBase<PolicyDat
/// <summary>
/// Creates a new exception object repository.
/// </summary>
public PostgresExceptionObjectRepository(PolicyDataSource dataSource, ILogger<PostgresExceptionObjectRepository> logger)
public PostgresExceptionObjectRepository(
PolicyDataSource dataSource,
ILogger<PostgresExceptionObjectRepository> logger,
TimeProvider timeProvider,
IGuidProvider guidProvider)
: base(dataSource, logger)
{
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
}
/// <inheritdoc />
@@ -194,12 +204,12 @@ public sealed class PostgresExceptionObjectRepository : RepositoryBase<PolicyDat
// Insert event
var updateEvent = new ExceptionEvent
{
EventId = Guid.NewGuid(),
EventId = _guidProvider.NewGuid(),
ExceptionId = exception.ExceptionId,
SequenceNumber = sequenceNumber,
EventType = eventType,
ActorId = actorId,
OccurredAt = DateTimeOffset.UtcNow,
OccurredAt = _timeProvider.GetUtcNow(),
PreviousStatus = currentStatus,
NewStatus = exception.Status,
NewVersion = exception.Version,

View File

@@ -70,23 +70,23 @@ public sealed record PolicyExplanation(
/// <param name="inputs">Optional evaluated inputs.</param>
/// <param name="policyVersion">Optional policy version.</param>
/// <param name="correlationId">Optional correlation ID.</param>
/// <param name="evaluatedAt">Optional timestamp for deterministic testing. If null, uses current time.</param>
/// <param name="evaluatedAt">Timestamp for the evaluation. Required for determinism.</param>
public static PolicyExplanation Create(
string findingId,
PolicyVerdictStatus decision,
string? ruleName,
string reason,
IEnumerable<PolicyExplanationNode> nodes,
DateTimeOffset evaluatedAt,
IEnumerable<RuleHit>? ruleHits = null,
IDictionary<string, object?>? inputs = null,
string? policyVersion = null,
string? correlationId = null,
DateTimeOffset? evaluatedAt = null) =>
string? correlationId = null) =>
new(findingId, decision, ruleName, reason, nodes.ToImmutableArray())
{
RuleHits = ruleHits?.ToImmutableArray() ?? ImmutableArray<RuleHit>.Empty,
EvaluatedInputs = inputs?.ToImmutableDictionary() ?? ImmutableDictionary<string, object?>.Empty,
EvaluatedAt = evaluatedAt ?? DateTimeOffset.UtcNow,
EvaluatedAt = evaluatedAt,
PolicyVersion = policyVersion,
CorrelationId = correlationId
};
@@ -229,23 +229,22 @@ public sealed record PolicyExplanationRecord(
/// <param name="policyId">The policy ID.</param>
/// <param name="tenantId">Optional tenant identifier.</param>
/// <param name="actor">Optional actor who triggered the evaluation.</param>
/// <param name="recordId">Optional record ID for deterministic testing. If null, generates a new GUID.</param>
/// <param name="evaluatedAt">Optional timestamp for deterministic testing. If null, uses current time.</param>
/// <param name="recordId">Record ID for determinism. Required.</param>
/// <param name="evaluatedAt">Timestamp for the evaluation. Required for determinism.</param>
public static PolicyExplanationRecord FromExplanation(
PolicyExplanation explanation,
string policyId,
string recordId,
DateTimeOffset evaluatedAt,
string? tenantId = null,
string? actor = null,
string? recordId = null,
DateTimeOffset? evaluatedAt = null)
string? actor = null)
{
var id = recordId ?? $"pexp-{Guid.NewGuid():N}";
var ruleHitsJson = System.Text.Json.JsonSerializer.Serialize(explanation.RuleHits);
var inputsJson = System.Text.Json.JsonSerializer.Serialize(explanation.EvaluatedInputs);
var treeJson = System.Text.Json.JsonSerializer.Serialize(explanation.Nodes);
return new PolicyExplanationRecord(
Id: id,
Id: recordId,
FindingId: explanation.FindingId,
PolicyId: policyId,
PolicyVersion: explanation.PolicyVersion ?? "unknown",
@@ -254,7 +253,7 @@ public sealed record PolicyExplanationRecord(
RuleHitsJson: ruleHitsJson,
InputsJson: inputsJson,
ExplanationTreeJson: treeJson,
EvaluatedAt: explanation.EvaluatedAt ?? evaluatedAt ?? DateTimeOffset.UtcNow,
EvaluatedAt: explanation.EvaluatedAt ?? evaluatedAt,
CorrelationId: explanation.CorrelationId,
TenantId: tenantId,
Actor: actor);

View File

@@ -117,17 +117,17 @@ public sealed class ProofLedger
/// <summary>
/// Serialize the ledger to JSON.
/// </summary>
/// <param name="createdAtUtc">The timestamp for the ledger creation.</param>
/// <param name="options">Optional JSON serializer options.</param>
/// <param name="createdAtUtc">Optional timestamp for deterministic testing. If null, uses current time.</param>
/// <returns>The JSON representation of the ledger.</returns>
public string ToJson(JsonSerializerOptions? options = null, DateTimeOffset? createdAtUtc = null)
public string ToJson(DateTimeOffset createdAtUtc, JsonSerializerOptions? options = null)
{
lock (_lock)
{
var payload = new ProofLedgerPayload(
Nodes: [.. _nodes],
RootHash: RootHash(),
CreatedAtUtc: createdAtUtc ?? DateTimeOffset.UtcNow);
CreatedAtUtc: createdAtUtc);
return JsonSerializer.Serialize(payload, options ?? DefaultJsonOptions);
}

View File

@@ -326,7 +326,7 @@ public sealed class ScoreAttestationBuilder
/// <param name="breakdown">The score breakdown.</param>
/// <param name="policy">The scoring policy reference.</param>
/// <param name="inputs">The scoring inputs.</param>
/// <param name="scoredAt">Optional timestamp for deterministic testing. If null, uses current time.</param>
/// <param name="scoredAt">The timestamp when scoring occurred.</param>
public static ScoreAttestationBuilder Create(
string subjectDigest,
int overallScore,
@@ -334,11 +334,11 @@ public sealed class ScoreAttestationBuilder
ScoreBreakdown breakdown,
ScoringPolicyRef policy,
ScoringInputs inputs,
DateTimeOffset? scoredAt = null)
DateTimeOffset scoredAt)
{
return new ScoreAttestationBuilder(new ScoreAttestationStatement
{
ScoredAt = scoredAt ?? DateTimeOffset.UtcNow,
ScoredAt = scoredAt,
SubjectDigest = subjectDigest,
OverallScore = overallScore,
Confidence = confidence,

View File

@@ -348,14 +348,14 @@ public sealed class ScoringRulesSnapshotBuilder
/// </summary>
/// <param name="id">The snapshot ID.</param>
/// <param name="version">The snapshot version.</param>
/// <param name="createdAt">Optional timestamp for deterministic testing. If null, uses current time.</param>
public static ScoringRulesSnapshotBuilder Create(string id, int version, DateTimeOffset? createdAt = null)
/// <param name="createdAt">The timestamp for the snapshot creation.</param>
public static ScoringRulesSnapshotBuilder Create(string id, int version, DateTimeOffset createdAt)
{
return new ScoringRulesSnapshotBuilder(new ScoringRulesSnapshot
{
Id = id,
Version = version,
CreatedAt = createdAt ?? DateTimeOffset.UtcNow,
CreatedAt = createdAt,
Digest = "", // Will be computed on build
Weights = new ScoringWeights(),
Thresholds = new GradeThresholds(),

View File

@@ -183,11 +183,11 @@ public sealed class CsafVexNormalizer : IVexNormalizer
public Claim NormalizeStatement(
Subject subject,
CsafProductStatus status,
DateTimeOffset issuedAt,
CsafFlagLabel flag = CsafFlagLabel.None,
string? remediation = null,
Principal? principal = null,
TrustLabel? trustLabel = null,
DateTimeOffset? issuedAt = null)
TrustLabel? trustLabel = null)
{
var assertions = new List<AtomAssertion>();
@@ -221,7 +221,7 @@ public sealed class CsafVexNormalizer : IVexNormalizer
Issuer = principal ?? Principal.Unknown,
Assertions = assertions,
TrustLabel = trustLabel,
Time = new ClaimTimeInfo { IssuedAt = issuedAt ?? DateTimeOffset.UtcNow },
Time = new ClaimTimeInfo { IssuedAt = issuedAt },
};
}
}

View File

@@ -236,11 +236,11 @@ public sealed record PolicyBundle
/// Checks if a principal is trusted for a given scope.
/// </summary>
/// <param name="principal">The principal to check.</param>
/// <param name="asOf">Timestamp for trust evaluation. Allows deterministic testing.</param>
/// <param name="requiredScope">Optional required authority scope.</param>
/// <param name="asOf">Optional timestamp for deterministic testing. If null, uses current time.</param>
public bool IsTrusted(Principal principal, AuthorityScope? requiredScope = null, DateTimeOffset? asOf = null)
public bool IsTrusted(Principal principal, DateTimeOffset asOf, AuthorityScope? requiredScope = null)
{
var now = asOf ?? DateTimeOffset.UtcNow;
var now = asOf;
foreach (var root in TrustRoots)
{
@@ -261,10 +261,10 @@ public sealed record PolicyBundle
/// Gets the maximum assurance level for a principal.
/// </summary>
/// <param name="principal">The principal to check.</param>
/// <param name="asOf">Optional timestamp for deterministic testing. If null, uses current time.</param>
public AssuranceLevel? GetMaxAssurance(Principal principal, DateTimeOffset? asOf = null)
/// <param name="asOf">Timestamp for trust evaluation. Allows deterministic testing.</param>
public AssuranceLevel? GetMaxAssurance(Principal principal, DateTimeOffset asOf)
{
var now = asOf ?? DateTimeOffset.UtcNow;
var now = asOf;
foreach (var root in TrustRoots)
{

View File

@@ -42,7 +42,7 @@ public sealed record ProofInput
/// <summary>
/// Timestamp when the input was ingested.
/// </summary>
public DateTimeOffset IngestedAt { get; init; } = DateTimeOffset.UtcNow;
public required DateTimeOffset IngestedAt { get; init; }
}
/// <summary>
@@ -161,7 +161,7 @@ public sealed record ProofBundle
/// <summary>
/// Timestamp when the proof bundle was created.
/// </summary>
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
public required DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// The policy bundle used for evaluation.