warnings fixes, tests fixes, sprints completions
This commit is contained in:
@@ -20,10 +20,12 @@ public sealed class RvaBuilder
|
||||
private DateTimeOffset? _expiresAt;
|
||||
private readonly Dictionary<string, string> _metadata = [];
|
||||
private readonly ICryptoHash _cryptoHash;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public RvaBuilder(ICryptoHash cryptoHash)
|
||||
public RvaBuilder(ICryptoHash cryptoHash, TimeProvider timeProvider)
|
||||
{
|
||||
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
public RvaBuilder WithVerdict(RiskVerdictStatus verdict)
|
||||
@@ -162,7 +164,7 @@ public sealed class RvaBuilder
|
||||
if (_snapshotId is null)
|
||||
throw new InvalidOperationException("Knowledge snapshot ID is required");
|
||||
|
||||
var createdAt = DateTimeOffset.UtcNow;
|
||||
var createdAt = _timeProvider.GetUtcNow();
|
||||
|
||||
var attestation = new RiskVerdictAttestation
|
||||
{
|
||||
|
||||
@@ -16,14 +16,17 @@ public sealed class RvaVerifier : IRvaVerifier
|
||||
private readonly ICryptoSigner? _signer;
|
||||
private readonly ISnapshotService _snapshotService;
|
||||
private readonly ILogger<RvaVerifier> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public RvaVerifier(
|
||||
ISnapshotService snapshotService,
|
||||
ILogger<RvaVerifier> logger,
|
||||
TimeProvider timeProvider,
|
||||
ICryptoSigner? signer = null)
|
||||
{
|
||||
_snapshotService = snapshotService ?? throw new ArgumentNullException(nameof(snapshotService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_signer = signer;
|
||||
}
|
||||
|
||||
@@ -51,7 +54,7 @@ public sealed class RvaVerifier : IRvaVerifier
|
||||
issues.Add($"Signature verification failed: {sigResult.Error}");
|
||||
if (!options.ContinueOnSignatureFailure)
|
||||
{
|
||||
return RvaVerificationResult.Fail(issues);
|
||||
return RvaVerificationResult.Fail(issues, _timeProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +64,7 @@ public sealed class RvaVerifier : IRvaVerifier
|
||||
if (attestation is null)
|
||||
{
|
||||
issues.Add("Failed to parse RVA payload");
|
||||
return RvaVerificationResult.Fail(issues);
|
||||
return RvaVerificationResult.Fail(issues, _timeProvider);
|
||||
}
|
||||
|
||||
// Step 3: Verify content-addressed ID
|
||||
@@ -69,18 +72,18 @@ public sealed class RvaVerifier : IRvaVerifier
|
||||
if (!idValid)
|
||||
{
|
||||
issues.Add("Attestation ID does not match content");
|
||||
return RvaVerificationResult.Fail(issues);
|
||||
return RvaVerificationResult.Fail(issues, _timeProvider);
|
||||
}
|
||||
|
||||
// Step 4: Verify expiration
|
||||
if (options.CheckExpiration && attestation.ExpiresAt.HasValue)
|
||||
{
|
||||
if (attestation.ExpiresAt.Value < DateTimeOffset.UtcNow)
|
||||
if (attestation.ExpiresAt.Value < _timeProvider.GetUtcNow())
|
||||
{
|
||||
issues.Add($"Attestation expired at {attestation.ExpiresAt.Value:o}");
|
||||
if (!options.AllowExpired)
|
||||
{
|
||||
return RvaVerificationResult.Fail(issues);
|
||||
return RvaVerificationResult.Fail(issues, _timeProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +109,7 @@ public sealed class RvaVerifier : IRvaVerifier
|
||||
Attestation = attestation,
|
||||
SignerIdentity = signerIdentity,
|
||||
Issues = issues,
|
||||
VerifiedAt = DateTimeOffset.UtcNow
|
||||
VerifiedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,18 +130,18 @@ public sealed class RvaVerifier : IRvaVerifier
|
||||
if (!idValid)
|
||||
{
|
||||
issues.Add("Attestation ID does not match content");
|
||||
return Task.FromResult(RvaVerificationResult.Fail(issues));
|
||||
return Task.FromResult(RvaVerificationResult.Fail(issues, _timeProvider));
|
||||
}
|
||||
|
||||
// Verify expiration
|
||||
if (options.CheckExpiration && attestation.ExpiresAt.HasValue)
|
||||
{
|
||||
if (attestation.ExpiresAt.Value < DateTimeOffset.UtcNow)
|
||||
if (attestation.ExpiresAt.Value < _timeProvider.GetUtcNow())
|
||||
{
|
||||
issues.Add($"Attestation expired at {attestation.ExpiresAt.Value:o}");
|
||||
if (!options.AllowExpired)
|
||||
{
|
||||
return Task.FromResult(RvaVerificationResult.Fail(issues));
|
||||
return Task.FromResult(RvaVerificationResult.Fail(issues, _timeProvider));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,7 +155,7 @@ public sealed class RvaVerifier : IRvaVerifier
|
||||
Attestation = attestation,
|
||||
SignerIdentity = null,
|
||||
Issues = issues,
|
||||
VerifiedAt = DateTimeOffset.UtcNow
|
||||
VerifiedAt = _timeProvider.GetUtcNow()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -291,10 +294,10 @@ public sealed record RvaVerificationResult
|
||||
public RiskVerdictAttestation? Attestation { get; init; }
|
||||
public string? SignerIdentity { get; init; }
|
||||
public IReadOnlyList<string> Issues { get; init; } = [];
|
||||
public DateTimeOffset VerifiedAt { get; init; }
|
||||
public required DateTimeOffset VerifiedAt { get; init; }
|
||||
|
||||
public static RvaVerificationResult Fail(IReadOnlyList<string> issues) =>
|
||||
new() { IsValid = false, Issues = issues, VerifiedAt = DateTimeOffset.UtcNow };
|
||||
public static RvaVerificationResult Fail(IReadOnlyList<string> issues, TimeProvider timeProvider) =>
|
||||
new() { IsValid = false, Issues = issues, VerifiedAt = timeProvider.GetUtcNow() };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -143,13 +143,15 @@ public sealed record ScoreProvenanceChain
|
||||
public static ScoreProvenanceChain FromVerdictPredicate(
|
||||
VerdictPredicate predicate,
|
||||
ProvenanceFindingRef finding,
|
||||
ProvenanceEvidenceSet evidenceSet)
|
||||
ProvenanceEvidenceSet evidenceSet,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(predicate);
|
||||
ArgumentNullException.ThrowIfNull(finding);
|
||||
ArgumentNullException.ThrowIfNull(evidenceSet);
|
||||
ArgumentNullException.ThrowIfNull(timeProvider);
|
||||
|
||||
var scoreNode = ProvenanceScoreNode.FromVerdictEws(predicate.EvidenceWeightedScore, predicate.FindingId);
|
||||
var scoreNode = ProvenanceScoreNode.FromVerdictEws(predicate.EvidenceWeightedScore, predicate.FindingId, timeProvider);
|
||||
var verdictRef = ProvenanceVerdictRef.FromVerdictPredicate(predicate);
|
||||
|
||||
return new ScoreProvenanceChain(
|
||||
@@ -157,7 +159,7 @@ public sealed record ScoreProvenanceChain
|
||||
evidenceSet: evidenceSet,
|
||||
score: scoreNode,
|
||||
verdict: verdictRef,
|
||||
createdAt: DateTimeOffset.UtcNow
|
||||
createdAt: timeProvider.GetUtcNow()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -533,8 +535,9 @@ public sealed record ProvenanceScoreNode
|
||||
/// <summary>
|
||||
/// Creates a ProvenanceScoreNode from a VerdictEvidenceWeightedScore.
|
||||
/// </summary>
|
||||
public static ProvenanceScoreNode FromVerdictEws(VerdictEvidenceWeightedScore? ews, string findingId)
|
||||
public static ProvenanceScoreNode FromVerdictEws(VerdictEvidenceWeightedScore? ews, string findingId, TimeProvider timeProvider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(timeProvider);
|
||||
if (ews is null)
|
||||
{
|
||||
// No EWS - create a placeholder node
|
||||
@@ -545,7 +548,7 @@ public sealed record ProvenanceScoreNode
|
||||
weights: new VerdictEvidenceWeights(0, 0, 0, 0, 0, 0),
|
||||
policyDigest: "none",
|
||||
calculatorVersion: "none",
|
||||
calculatedAt: DateTimeOffset.UtcNow
|
||||
calculatedAt: timeProvider.GetUtcNow()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -560,7 +563,7 @@ public sealed record ProvenanceScoreNode
|
||||
weights: new VerdictEvidenceWeights(0, 0, 0, 0, 0, 0),
|
||||
policyDigest: ews.PolicyDigest ?? "unknown",
|
||||
calculatorVersion: "unknown",
|
||||
calculatedAt: ews.CalculatedAt ?? DateTimeOffset.UtcNow,
|
||||
calculatedAt: ews.CalculatedAt ?? timeProvider.GetUtcNow(),
|
||||
appliedFlags: ews.Flags,
|
||||
guardrails: ews.Guardrails
|
||||
);
|
||||
|
||||
@@ -12,7 +12,9 @@ public static class ExceptionMapper
|
||||
/// <summary>
|
||||
/// Maps an ExceptionObject to a full DTO.
|
||||
/// </summary>
|
||||
public static ExceptionDto ToDto(ExceptionObject exception)
|
||||
/// <param name="exception">The exception to map.</param>
|
||||
/// <param name="referenceTime">The reference time for IsEffective/HasExpired checks.</param>
|
||||
public static ExceptionDto ToDto(ExceptionObject exception, DateTimeOffset referenceTime)
|
||||
{
|
||||
return new ExceptionDto
|
||||
{
|
||||
@@ -34,15 +36,17 @@ public static class ExceptionMapper
|
||||
CompensatingControls = exception.CompensatingControls.ToList(),
|
||||
Metadata = exception.Metadata,
|
||||
TicketRef = exception.TicketRef,
|
||||
IsEffective = exception.IsEffective,
|
||||
HasExpired = exception.HasExpired
|
||||
IsEffective = exception.IsEffectiveAt(referenceTime),
|
||||
HasExpired = exception.HasExpiredAt(referenceTime)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an ExceptionObject to a summary DTO for list responses.
|
||||
/// </summary>
|
||||
public static ExceptionSummaryDto ToSummaryDto(ExceptionObject exception)
|
||||
/// <param name="exception">The exception to map.</param>
|
||||
/// <param name="referenceTime">The reference time for IsEffective check.</param>
|
||||
public static ExceptionSummaryDto ToSummaryDto(ExceptionObject exception, DateTimeOffset referenceTime)
|
||||
{
|
||||
return new ExceptionSummaryDto
|
||||
{
|
||||
@@ -54,7 +58,7 @@ public static class ExceptionMapper
|
||||
OwnerId = exception.OwnerId,
|
||||
ExpiresAt = exception.ExpiresAt,
|
||||
ReasonCode = ReasonToString(exception.ReasonCode),
|
||||
IsEffective = exception.IsEffective
|
||||
IsEffective = exception.IsEffectiveAt(referenceTime)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
@@ -335,6 +336,8 @@ internal static class ViolationEndpoints
|
||||
HttpContext context,
|
||||
[FromBody] CreateViolationRequest request,
|
||||
IViolationEventRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
|
||||
@@ -356,7 +359,7 @@ internal static class ViolationEndpoints
|
||||
|
||||
var entity = new ViolationEventEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Id = guidProvider.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
PolicyId = request.PolicyId,
|
||||
RuleId = request.RuleId,
|
||||
@@ -366,7 +369,7 @@ internal static class ViolationEndpoints
|
||||
Details = request.Details ?? "{}",
|
||||
Remediation = request.Remediation,
|
||||
CorrelationId = request.CorrelationId,
|
||||
OccurredAt = request.OccurredAt ?? DateTimeOffset.UtcNow
|
||||
OccurredAt = request.OccurredAt ?? timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
try
|
||||
@@ -389,6 +392,8 @@ internal static class ViolationEndpoints
|
||||
HttpContext context,
|
||||
[FromBody] CreateViolationBatchRequest request,
|
||||
IViolationEventRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
|
||||
@@ -408,9 +413,10 @@ internal static class ViolationEndpoints
|
||||
});
|
||||
}
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var entities = request.Violations.Select(v => new ViolationEventEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Id = guidProvider.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
PolicyId = v.PolicyId,
|
||||
RuleId = v.RuleId,
|
||||
@@ -420,7 +426,7 @@ internal static class ViolationEndpoints
|
||||
Details = v.Details ?? "{}",
|
||||
Remediation = v.Remediation,
|
||||
CorrelationId = v.CorrelationId,
|
||||
OccurredAt = v.OccurredAt ?? DateTimeOffset.UtcNow
|
||||
OccurredAt = v.OccurredAt ?? now
|
||||
}).ToList();
|
||||
|
||||
try
|
||||
|
||||
@@ -185,7 +185,7 @@ public sealed record VexTrustGateResult
|
||||
/// <summary>
|
||||
/// Timestamp when decision was made.
|
||||
/// </summary>
|
||||
public DateTimeOffset EvaluatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public required DateTimeOffset EvaluatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional details for audit.
|
||||
@@ -400,7 +400,7 @@ public sealed class VexTrustGate : IVexTrustGate
|
||||
};
|
||||
}
|
||||
|
||||
private static VexTrustGateResult CreateAllowResult(
|
||||
private VexTrustGateResult CreateAllowResult(
|
||||
string gateId,
|
||||
string reason,
|
||||
VexTrustStatus? trustStatus)
|
||||
@@ -415,7 +415,7 @@ public sealed class VexTrustGate : IVexTrustGate
|
||||
? ComputeTier(trustStatus.TrustScore)
|
||||
: null,
|
||||
IssuerId = trustStatus?.IssuerId,
|
||||
EvaluatedAt = DateTimeOffset.UtcNow
|
||||
EvaluatedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,18 @@ namespace StellaOps.Policy.Engine.Services;
|
||||
internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, PolicyPackRecord> packs = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryPolicyPackRepository(TimeProvider timeProvider)
|
||||
{
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
public Task<PolicyPackRecord> CreateAsync(string packId, string? displayName, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(packId);
|
||||
|
||||
var created = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, displayName, DateTimeOffset.UtcNow));
|
||||
var created = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, displayName, _timeProvider.GetUtcNow()));
|
||||
return Task.FromResult(created);
|
||||
}
|
||||
|
||||
@@ -25,15 +31,16 @@ internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository
|
||||
|
||||
public Task<PolicyRevisionRecord> UpsertRevisionAsync(string packId, int version, bool requiresTwoPersonApproval, PolicyRevisionStatus initialStatus, CancellationToken cancellationToken)
|
||||
{
|
||||
var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, DateTimeOffset.UtcNow));
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, now));
|
||||
int revisionVersion = version > 0 ? version : pack.GetNextVersion();
|
||||
var revision = pack.GetOrAddRevision(
|
||||
revisionVersion,
|
||||
v => new PolicyRevisionRecord(v, requiresTwoPersonApproval, initialStatus, DateTimeOffset.UtcNow));
|
||||
v => new PolicyRevisionRecord(v, requiresTwoPersonApproval, initialStatus, now));
|
||||
|
||||
if (revision.Status != initialStatus)
|
||||
{
|
||||
revision.SetStatus(initialStatus, DateTimeOffset.UtcNow);
|
||||
revision.SetStatus(initialStatus, now);
|
||||
}
|
||||
|
||||
return Task.FromResult(revision);
|
||||
@@ -95,9 +102,10 @@ internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bundle);
|
||||
|
||||
var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, DateTimeOffset.UtcNow));
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, now));
|
||||
var revision = pack.GetOrAddRevision(version > 0 ? version : pack.GetNextVersion(),
|
||||
v => new PolicyRevisionRecord(v, requiresTwoPerson: false, status: PolicyRevisionStatus.Draft, DateTimeOffset.UtcNow));
|
||||
v => new PolicyRevisionRecord(v, requiresTwoPerson: false, status: PolicyRevisionStatus.Draft, now));
|
||||
|
||||
revision.SetBundle(bundle);
|
||||
return Task.FromResult(bundle);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.SbomService.Repositories;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Services;
|
||||
@@ -94,13 +95,19 @@ public sealed class VerdictLinkService : IVerdictLinkService
|
||||
{
|
||||
private readonly ISbomVerdictLinkRepository _repository;
|
||||
private readonly ILogger<VerdictLinkService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public VerdictLinkService(
|
||||
ISbomVerdictLinkRepository repository,
|
||||
ILogger<VerdictLinkService> logger)
|
||||
ILogger<VerdictLinkService> logger,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -114,14 +121,14 @@ public sealed class VerdictLinkService : IVerdictLinkService
|
||||
return;
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var links = new List<SbomVerdictLink>();
|
||||
|
||||
foreach (var verdict in request.Verdicts)
|
||||
{
|
||||
var link = new SbomVerdictLink
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Id = _guidProvider.NewGuid(),
|
||||
SbomVersionId = request.SbomVersionId,
|
||||
Cve = verdict.Cve,
|
||||
ConsensusProjectionId = verdict.ConsensusProjectionId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.RegularExpressions;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
|
||||
@@ -10,13 +11,15 @@ namespace StellaOps.Policy.Engine.Storage.InMemory;
|
||||
/// In-memory implementation of IExceptionRepository for offline/test runs.
|
||||
/// Provides minimal semantics needed for lifecycle processing.
|
||||
/// </summary>
|
||||
public sealed class InMemoryExceptionRepository : IExceptionRepository
|
||||
public sealed class InMemoryExceptionRepository(TimeProvider timeProvider, IGuidProvider guidProvider) : IExceptionRepository
|
||||
{
|
||||
private readonly TimeProvider _timeProvider = timeProvider;
|
||||
private readonly IGuidProvider _guidProvider = guidProvider;
|
||||
private readonly ConcurrentDictionary<(string Tenant, Guid Id), ExceptionEntity> _exceptions = new();
|
||||
|
||||
public Task<ExceptionEntity> CreateAsync(ExceptionEntity exception, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var id = exception.Id == Guid.Empty ? Guid.NewGuid() : exception.Id;
|
||||
var id = exception.Id == Guid.Empty ? _guidProvider.NewGuid() : exception.Id;
|
||||
var stored = Copy(exception, id);
|
||||
_exceptions[(Normalize(exception.TenantId), id)] = stored;
|
||||
return Task.FromResult(stored);
|
||||
@@ -123,7 +126,7 @@ public sealed class InMemoryExceptionRepository : IExceptionRepository
|
||||
_exceptions[key] = Copy(
|
||||
existing,
|
||||
statusOverride: ExceptionStatus.Revoked,
|
||||
revokedAtOverride: DateTimeOffset.UtcNow,
|
||||
revokedAtOverride: _timeProvider.GetUtcNow(),
|
||||
revokedByOverride: revokedBy);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
@@ -133,7 +136,7 @@ public sealed class InMemoryExceptionRepository : IExceptionRepository
|
||||
|
||||
public Task<int> ExpireAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var normalizedTenant = Normalize(tenantId);
|
||||
var expired = 0;
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
// Sprint: SPRINT_20251226_003_BE_exception_approval
|
||||
// Task: EXCEPT-05, EXCEPT-06, EXCEPT-07 - Exception approval API endpoints
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
@@ -89,6 +91,8 @@ public static class ExceptionApprovalEndpoints
|
||||
CreateApprovalRequestDto request,
|
||||
IExceptionApprovalRepository repository,
|
||||
IExceptionApprovalRulesService rulesService,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
ILogger<ExceptionApprovalRequestEntity> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -110,7 +114,8 @@ public static class ExceptionApprovalEndpoints
|
||||
}
|
||||
|
||||
// Generate request ID
|
||||
var requestId = $"EAR-{DateTimeOffset.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var requestId = $"EAR-{now.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{guidProvider.NewGuid().ToString("N", CultureInfo.InvariantCulture)[..8].ToUpperInvariant()}";
|
||||
|
||||
// Parse gate level
|
||||
if (!Enum.TryParse<GateLevel>(request.GateLevel, ignoreCase: true, out var gateLevel))
|
||||
@@ -139,10 +144,9 @@ public static class ExceptionApprovalEndpoints
|
||||
});
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var entity = new ExceptionApprovalRequestEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Id = guidProvider.NewGuid(),
|
||||
RequestId = requestId,
|
||||
TenantId = tenantId,
|
||||
ExceptionId = request.ExceptionId,
|
||||
@@ -204,7 +208,7 @@ public static class ExceptionApprovalEndpoints
|
||||
// Record audit entry
|
||||
await repository.RecordAuditAsync(new ExceptionApprovalAuditEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Id = guidProvider.NewGuid(),
|
||||
RequestId = requestId,
|
||||
TenantId = tenantId,
|
||||
SequenceNumber = 1,
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Exceptions.Models;
|
||||
using StellaOps.Policy.Exceptions.Repositories;
|
||||
using StellaOps.Policy.Gateway.Contracts;
|
||||
@@ -134,6 +135,8 @@ public static class ExceptionEndpoints
|
||||
CreateExceptionRequest request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (request is null)
|
||||
@@ -145,8 +148,10 @@ public static class ExceptionEndpoints
|
||||
});
|
||||
}
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
// Validate expiry is in future
|
||||
if (request.ExpiresAt <= DateTimeOffset.UtcNow)
|
||||
if (request.ExpiresAt <= now)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
@@ -157,7 +162,7 @@ public static class ExceptionEndpoints
|
||||
}
|
||||
|
||||
// Validate expiry is not more than 1 year
|
||||
if (request.ExpiresAt > DateTimeOffset.UtcNow.AddYears(1))
|
||||
if (request.ExpiresAt > now.AddYears(1))
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
@@ -170,7 +175,7 @@ public static class ExceptionEndpoints
|
||||
var actorId = GetActorId(context);
|
||||
var clientInfo = GetClientInfo(context);
|
||||
|
||||
var exceptionId = $"EXC-{Guid.NewGuid():N}"[..20];
|
||||
var exceptionId = $"EXC-{guidProvider.NewGuid():N}"[..20];
|
||||
|
||||
var exception = new ExceptionObject
|
||||
{
|
||||
@@ -188,8 +193,8 @@ public static class ExceptionEndpoints
|
||||
},
|
||||
OwnerId = request.OwnerId,
|
||||
RequesterId = actorId,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
ReasonCode = ParseReasonRequired(request.ReasonCode),
|
||||
Rationale = request.Rationale,
|
||||
@@ -210,6 +215,7 @@ public static class ExceptionEndpoints
|
||||
UpdateExceptionRequest request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -238,7 +244,7 @@ public static class ExceptionEndpoints
|
||||
var updated = existing with
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = timeProvider.GetUtcNow(),
|
||||
Rationale = request.Rationale ?? existing.Rationale,
|
||||
EvidenceRefs = request.EvidenceRefs?.ToImmutableArray() ?? existing.EvidenceRefs,
|
||||
CompensatingControls = request.CompensatingControls?.ToImmutableArray() ?? existing.CompensatingControls,
|
||||
@@ -258,6 +264,7 @@ public static class ExceptionEndpoints
|
||||
ApproveExceptionRequest? request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -290,12 +297,13 @@ public static class ExceptionEndpoints
|
||||
});
|
||||
}
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var updated = existing with
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
Status = ExceptionStatus.Approved,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
ApprovedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = now,
|
||||
ApprovedAt = now,
|
||||
ApproverIds = existing.ApproverIds.Add(actorId)
|
||||
};
|
||||
|
||||
@@ -310,6 +318,7 @@ public static class ExceptionEndpoints
|
||||
string id,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -335,7 +344,7 @@ public static class ExceptionEndpoints
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
Status = ExceptionStatus.Active,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
UpdatedAt = timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
var result = await repository.UpdateAsync(
|
||||
@@ -350,6 +359,7 @@ public static class ExceptionEndpoints
|
||||
ExtendExceptionRequest request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -384,7 +394,7 @@ public static class ExceptionEndpoints
|
||||
var updated = existing with
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = timeProvider.GetUtcNow(),
|
||||
ExpiresAt = request.NewExpiresAt
|
||||
};
|
||||
|
||||
@@ -400,6 +410,7 @@ public static class ExceptionEndpoints
|
||||
[FromBody] RevokeExceptionRequest? request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -425,7 +436,7 @@ public static class ExceptionEndpoints
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
Status = ExceptionStatus.Revoked,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
UpdatedAt = timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
var result = await repository.UpdateAsync(
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
// Sprint: SPRINT_20251226_001_BE_cicd_gate_integration
|
||||
// Task: CICD-GATE-01 - Create POST /api/v1/policy/gate/evaluate endpoint
|
||||
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Audit;
|
||||
using StellaOps.Policy.Deltas;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
@@ -39,6 +41,8 @@ public static class GateEndpoints
|
||||
IBaselineSelector baselineSelector,
|
||||
IGateBypassAuditor bypassAuditor,
|
||||
IMemoryCache cache,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
ILogger<DriftGateEvaluator> logger,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
@@ -79,12 +83,12 @@ public static class GateEndpoints
|
||||
|
||||
return Results.Ok(new GateEvaluateResponse
|
||||
{
|
||||
DecisionId = $"gate:{DateTimeOffset.UtcNow:yyyyMMddHHmmss}:{Guid.NewGuid():N}",
|
||||
DecisionId = $"gate:{timeProvider.GetUtcNow().ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture)}:{guidProvider.NewGuid():N}",
|
||||
Status = GateStatus.Pass,
|
||||
ExitCode = GateExitCodes.Pass,
|
||||
ImageDigest = request.ImageDigest,
|
||||
BaselineRef = request.BaselineRef,
|
||||
DecidedAt = DateTimeOffset.UtcNow,
|
||||
DecidedAt = timeProvider.GetUtcNow(),
|
||||
Summary = "First build - no baseline for comparison",
|
||||
Advisory = "This appears to be a first build. Future builds will be compared against this baseline."
|
||||
});
|
||||
@@ -224,7 +228,7 @@ public static class GateEndpoints
|
||||
.WithDescription("Retrieve a previous gate evaluation decision by ID");
|
||||
|
||||
// GET /api/v1/policy/gate/health - Health check for gate service
|
||||
gates.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTimeOffset.UtcNow }))
|
||||
gates.MapGet("/health", (TimeProvider timeProvider) => Results.Ok(new { status = "healthy", timestamp = timeProvider.GetUtcNow() }))
|
||||
.WithName("GateHealth")
|
||||
.WithDescription("Health check for the gate evaluation service");
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
|
||||
namespace StellaOps.Policy.Gateway.Endpoints;
|
||||
|
||||
@@ -104,6 +106,7 @@ public static class GovernanceEndpoints
|
||||
{
|
||||
var tenant = tenantId ?? GetTenantId(httpContext) ?? "default";
|
||||
var state = SealedModeStates.GetOrAdd(tenant, _ => new SealedModeState());
|
||||
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
|
||||
var response = new SealedModeStatusResponse
|
||||
{
|
||||
@@ -118,7 +121,7 @@ public static class GovernanceEndpoints
|
||||
.Select(MapOverrideToResponse)
|
||||
.ToList(),
|
||||
VerificationStatus = "verified",
|
||||
LastVerifiedAt = DateTimeOffset.UtcNow.ToString("O")
|
||||
LastVerifiedAt = timeProvider.GetUtcNow().ToString("O")
|
||||
};
|
||||
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
@@ -144,9 +147,9 @@ public static class GovernanceEndpoints
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
var state = SealedModeStates.GetOrAdd(tenant, _ => new SealedModeState());
|
||||
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
if (request.Enable)
|
||||
{
|
||||
@@ -173,7 +176,7 @@ public static class GovernanceEndpoints
|
||||
|
||||
// Audit
|
||||
RecordAudit(tenant, actor, "sealed_mode_toggled", "sealed-mode", "system_config",
|
||||
$"{(request.Enable ? "Enabled" : "Disabled")} sealed mode: {request.Reason}");
|
||||
$"{(request.Enable ? "Enabled" : "Disabled")} sealed mode: {request.Reason}", timeProvider, guidProvider);
|
||||
|
||||
var response = new SealedModeStatusResponse
|
||||
{
|
||||
@@ -197,9 +200,11 @@ public static class GovernanceEndpoints
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
var overrideId = $"override-{Guid.NewGuid():N}";
|
||||
var overrideId = $"override-{guidProvider.NewGuid():N}";
|
||||
var entity = new SealedModeOverrideEntity
|
||||
{
|
||||
Id = overrideId,
|
||||
@@ -207,7 +212,7 @@ public static class GovernanceEndpoints
|
||||
Type = request.Type,
|
||||
Target = request.Target,
|
||||
Reason = request.Reason,
|
||||
ApprovalId = $"approval-{Guid.NewGuid():N}",
|
||||
ApprovalId = $"approval-{guidProvider.NewGuid():N}",
|
||||
ApprovedBy = [actor],
|
||||
ExpiresAt = now.AddHours(request.DurationHours).ToString("O"),
|
||||
CreatedAt = now.ToString("O"),
|
||||
@@ -217,7 +222,7 @@ public static class GovernanceEndpoints
|
||||
Overrides[overrideId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "sealed_mode_override_created", overrideId, "sealed_mode_override",
|
||||
$"Created override for {request.Target}: {request.Reason}");
|
||||
$"Created override for {request.Target}: {request.Reason}", timeProvider, guidProvider);
|
||||
|
||||
return Task.FromResult(Results.Ok(MapOverrideToResponse(entity)));
|
||||
}
|
||||
@@ -229,6 +234,8 @@ public static class GovernanceEndpoints
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
|
||||
if (!Overrides.TryGetValue(overrideId, out var entity) || entity.TenantId != tenant)
|
||||
{
|
||||
@@ -243,7 +250,7 @@ public static class GovernanceEndpoints
|
||||
Overrides[overrideId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "sealed_mode_override_revoked", overrideId, "sealed_mode_override",
|
||||
$"Revoked override: {request.Reason}");
|
||||
$"Revoked override: {request.Reason}", timeProvider, guidProvider);
|
||||
|
||||
return Task.FromResult(Results.NoContent());
|
||||
}
|
||||
@@ -293,9 +300,11 @@ public static class GovernanceEndpoints
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
var profileId = $"profile-{Guid.NewGuid():N}";
|
||||
var profileId = $"profile-{guidProvider.NewGuid():N}";
|
||||
var entity = new RiskProfileEntity
|
||||
{
|
||||
Id = profileId,
|
||||
@@ -317,7 +326,7 @@ public static class GovernanceEndpoints
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_created", profileId, "risk_profile",
|
||||
$"Created risk profile: {request.Name}");
|
||||
$"Created risk profile: {request.Name}", timeProvider, guidProvider);
|
||||
|
||||
return Task.FromResult(Results.Created($"/api/v1/governance/risk-profiles/{profileId}", MapProfileToResponse(entity)));
|
||||
}
|
||||
@@ -329,7 +338,9 @@ public static class GovernanceEndpoints
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var existing))
|
||||
{
|
||||
@@ -354,7 +365,7 @@ public static class GovernanceEndpoints
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_updated", profileId, "risk_profile",
|
||||
$"Updated risk profile: {entity.Name}");
|
||||
$"Updated risk profile: {entity.Name}", timeProvider, guidProvider);
|
||||
|
||||
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
|
||||
}
|
||||
@@ -365,6 +376,8 @@ public static class GovernanceEndpoints
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
|
||||
if (!RiskProfiles.TryRemove(profileId, out var removed))
|
||||
{
|
||||
@@ -376,7 +389,7 @@ public static class GovernanceEndpoints
|
||||
}
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_deleted", profileId, "risk_profile",
|
||||
$"Deleted risk profile: {removed.Name}");
|
||||
$"Deleted risk profile: {removed.Name}", timeProvider, guidProvider);
|
||||
|
||||
return Task.FromResult(Results.NoContent());
|
||||
}
|
||||
@@ -387,7 +400,9 @@ public static class GovernanceEndpoints
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var existing))
|
||||
{
|
||||
@@ -408,7 +423,7 @@ public static class GovernanceEndpoints
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_activated", profileId, "risk_profile",
|
||||
$"Activated risk profile: {entity.Name}");
|
||||
$"Activated risk profile: {entity.Name}", timeProvider, guidProvider);
|
||||
|
||||
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
|
||||
}
|
||||
@@ -420,7 +435,9 @@ public static class GovernanceEndpoints
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
|
||||
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var existing))
|
||||
{
|
||||
@@ -442,7 +459,7 @@ public static class GovernanceEndpoints
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_deprecated", profileId, "risk_profile",
|
||||
$"Deprecated risk profile: {entity.Name} - {request.Reason}");
|
||||
$"Deprecated risk profile: {entity.Name} - {request.Reason}", timeProvider, guidProvider);
|
||||
|
||||
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
|
||||
}
|
||||
@@ -542,7 +559,7 @@ public static class GovernanceEndpoints
|
||||
{
|
||||
if (RiskProfiles.IsEmpty)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow.ToString("O");
|
||||
var now = TimeProvider.System.GetUtcNow().ToString("O", System.Globalization.CultureInfo.InvariantCulture);
|
||||
RiskProfiles["profile-default"] = new RiskProfileEntity
|
||||
{
|
||||
Id = "profile-default",
|
||||
@@ -582,15 +599,15 @@ public static class GovernanceEndpoints
|
||||
?? httpContext.Request.Headers["X-StellaOps-Actor"].FirstOrDefault();
|
||||
}
|
||||
|
||||
private static void RecordAudit(string tenantId, string actor, string eventType, string targetId, string targetType, string summary)
|
||||
private static void RecordAudit(string tenantId, string actor, string eventType, string targetId, string targetType, string summary, TimeProvider timeProvider, IGuidProvider guidProvider)
|
||||
{
|
||||
var id = $"audit-{Guid.NewGuid():N}";
|
||||
var id = $"audit-{guidProvider.NewGuid():N}";
|
||||
AuditEntries[id] = new GovernanceAuditEntry
|
||||
{
|
||||
Id = id,
|
||||
TenantId = tenantId,
|
||||
Type = eventType,
|
||||
Timestamp = DateTimeOffset.UtcNow.ToString("O"),
|
||||
Timestamp = timeProvider.GetUtcNow().ToString("O", System.Globalization.CultureInfo.InvariantCulture),
|
||||
Actor = actor,
|
||||
ActorType = "user",
|
||||
TargetResource = targetId,
|
||||
|
||||
@@ -50,6 +50,7 @@ internal static class RegistryWebhookEndpoints
|
||||
private static async Task<Results<Accepted<WebhookAcceptedResponse>, ProblemHttpResult>> HandleDockerRegistryWebhook(
|
||||
[FromBody] DockerRegistryNotification notification,
|
||||
IGateEvaluationQueue evaluationQueue,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<RegistryWebhookEndpointMarker> logger,
|
||||
CancellationToken ct)
|
||||
{
|
||||
@@ -77,7 +78,7 @@ internal static class RegistryWebhookEndpoints
|
||||
Tag = evt.Target.Tag,
|
||||
RegistryUrl = evt.Request?.Host,
|
||||
Source = "docker-registry",
|
||||
Timestamp = evt.Timestamp ?? DateTimeOffset.UtcNow
|
||||
Timestamp = evt.Timestamp ?? timeProvider.GetUtcNow()
|
||||
}, ct);
|
||||
|
||||
jobs.Add(jobId);
|
||||
@@ -100,6 +101,7 @@ internal static class RegistryWebhookEndpoints
|
||||
private static async Task<Results<Accepted<WebhookAcceptedResponse>, ProblemHttpResult>> HandleHarborWebhook(
|
||||
[FromBody] HarborWebhookEvent notification,
|
||||
IGateEvaluationQueue evaluationQueue,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<RegistryWebhookEndpointMarker> logger,
|
||||
CancellationToken ct)
|
||||
{
|
||||
@@ -136,7 +138,7 @@ internal static class RegistryWebhookEndpoints
|
||||
Tag = resource.Tag,
|
||||
RegistryUrl = notification.EventData.Repository?.RepoFullName,
|
||||
Source = "harbor",
|
||||
Timestamp = notification.OccurAt ?? DateTimeOffset.UtcNow
|
||||
Timestamp = notification.OccurAt ?? timeProvider.GetUtcNow()
|
||||
}, ct);
|
||||
|
||||
jobs.Add(jobId);
|
||||
@@ -159,6 +161,7 @@ internal static class RegistryWebhookEndpoints
|
||||
private static async Task<Results<Accepted<WebhookAcceptedResponse>, ProblemHttpResult>> HandleGenericWebhook(
|
||||
[FromBody] GenericRegistryWebhook notification,
|
||||
IGateEvaluationQueue evaluationQueue,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<RegistryWebhookEndpointMarker> logger,
|
||||
CancellationToken ct)
|
||||
{
|
||||
@@ -177,7 +180,7 @@ internal static class RegistryWebhookEndpoints
|
||||
RegistryUrl = notification.RegistryUrl,
|
||||
BaselineRef = notification.BaselineRef,
|
||||
Source = notification.Source ?? "generic",
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
Timestamp = timeProvider.GetUtcNow()
|
||||
}, ct);
|
||||
|
||||
logger.LogInformation(
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Exceptions.Models;
|
||||
using StellaOps.Policy.Exceptions.Repositories;
|
||||
|
||||
@@ -21,6 +22,7 @@ public sealed class ExceptionService : IExceptionService
|
||||
private readonly IExceptionRepository _repository;
|
||||
private readonly IExceptionNotificationService _notificationService;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly ILogger<ExceptionService> _logger;
|
||||
|
||||
/// <summary>
|
||||
@@ -30,11 +32,13 @@ public sealed class ExceptionService : IExceptionService
|
||||
IExceptionRepository repository,
|
||||
IExceptionNotificationService notificationService,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
ILogger<ExceptionService> logger)
|
||||
{
|
||||
_repository = repository;
|
||||
_notificationService = notificationService;
|
||||
_timeProvider = timeProvider;
|
||||
_guidProvider = guidProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -537,10 +541,10 @@ public sealed class ExceptionService : IExceptionService
|
||||
id.StartsWith("GO-", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string GenerateExceptionId()
|
||||
private string GenerateExceptionId()
|
||||
{
|
||||
// Format: EXC-{random alphanumeric}
|
||||
return $"EXC-{Guid.NewGuid():N}"[..20];
|
||||
return $"EXC-{_guidProvider.NewGuid():N}"[..20];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
// Description: In-memory queue for gate evaluation jobs with background processing
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Globalization;
|
||||
using System.Threading.Channels;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Gateway.Endpoints;
|
||||
|
||||
@@ -21,11 +23,15 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
|
||||
{
|
||||
private readonly Channel<GateEvaluationJob> _channel;
|
||||
private readonly ILogger<InMemoryGateEvaluationQueue> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public InMemoryGateEvaluationQueue(ILogger<InMemoryGateEvaluationQueue> logger)
|
||||
public InMemoryGateEvaluationQueue(ILogger<InMemoryGateEvaluationQueue> logger, TimeProvider timeProvider, IGuidProvider guidProvider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider;
|
||||
_guidProvider = guidProvider;
|
||||
|
||||
// Bounded channel to prevent unbounded memory growth
|
||||
_channel = Channel.CreateBounded<GateEvaluationJob>(new BoundedChannelOptions(1000)
|
||||
@@ -46,7 +52,7 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
|
||||
{
|
||||
JobId = jobId,
|
||||
Request = request,
|
||||
QueuedAt = DateTimeOffset.UtcNow
|
||||
QueuedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
await _channel.Writer.WriteAsync(job, cancellationToken).ConfigureAwait(false);
|
||||
@@ -65,11 +71,11 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
|
||||
/// </summary>
|
||||
public ChannelReader<GateEvaluationJob> Reader => _channel.Reader;
|
||||
|
||||
private static string GenerateJobId()
|
||||
private string GenerateJobId()
|
||||
{
|
||||
// Format: gate-{timestamp}-{random}
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
var random = Guid.NewGuid().ToString("N")[..8];
|
||||
var timestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture);
|
||||
var random = _guidProvider.NewGuid().ToString("N", CultureInfo.InvariantCulture)[..8];
|
||||
return $"gate-{timestamp}-{random}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Gateway.Options;
|
||||
|
||||
namespace StellaOps.Policy.Gateway.Services;
|
||||
@@ -17,6 +18,7 @@ internal sealed class PolicyGatewayDpopProofGenerator : IDisposable
|
||||
private readonly IHostEnvironment hostEnvironment;
|
||||
private readonly IOptionsMonitor<PolicyGatewayOptions> optionsMonitor;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly IGuidProvider guidProvider;
|
||||
private readonly ILogger<PolicyGatewayDpopProofGenerator> logger;
|
||||
private DpopKeyMaterial? keyMaterial;
|
||||
private readonly object sync = new();
|
||||
@@ -25,11 +27,13 @@ internal sealed class PolicyGatewayDpopProofGenerator : IDisposable
|
||||
IHostEnvironment hostEnvironment,
|
||||
IOptionsMonitor<PolicyGatewayOptions> optionsMonitor,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
ILogger<PolicyGatewayDpopProofGenerator> logger)
|
||||
{
|
||||
this.hostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment));
|
||||
this.optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
|
||||
this.timeProvider = timeProvider ?? TimeProvider.System;
|
||||
this.guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -85,7 +89,7 @@ internal sealed class PolicyGatewayDpopProofGenerator : IDisposable
|
||||
["htm"] = method.Method.ToUpperInvariant(),
|
||||
["htu"] = NormalizeTarget(targetUri),
|
||||
["iat"] = epochSeconds,
|
||||
["jti"] = Guid.NewGuid().ToString("N")
|
||||
["jti"] = guidProvider.NewGuid().ToString("N")
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(accessToken))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Registry.Contracts;
|
||||
|
||||
namespace StellaOps.Policy.Registry.Services;
|
||||
@@ -13,6 +14,7 @@ public sealed class BatchSimulationOrchestrator : IBatchSimulationOrchestrator,
|
||||
{
|
||||
private readonly IPolicySimulationService _simulationService;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, string JobId), BatchSimulationJob> _jobs = new();
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, string JobId), List<BatchSimulationInputResult>> _results = new();
|
||||
private readonly ConcurrentDictionary<string, string> _idempotencyKeys = new();
|
||||
@@ -22,10 +24,12 @@ public sealed class BatchSimulationOrchestrator : IBatchSimulationOrchestrator,
|
||||
|
||||
public BatchSimulationOrchestrator(
|
||||
IPolicySimulationService simulationService,
|
||||
TimeProvider? timeProvider = null)
|
||||
TimeProvider? timeProvider = null,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_simulationService = simulationService ?? throw new ArgumentNullException(nameof(simulationService));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? GuidProvider.Default;
|
||||
|
||||
// Start background processing
|
||||
_processingTask = Task.Run(ProcessJobsAsync);
|
||||
@@ -390,9 +394,9 @@ public sealed class BatchSimulationOrchestrator : IBatchSimulationOrchestrator,
|
||||
};
|
||||
}
|
||||
|
||||
private static string GenerateJobId(Guid tenantId, DateTimeOffset timestamp)
|
||||
private string GenerateJobId(Guid tenantId, DateTimeOffset timestamp)
|
||||
{
|
||||
var content = $"{tenantId}:{timestamp.ToUnixTimeMilliseconds()}:{Guid.NewGuid()}";
|
||||
var content = $"{tenantId}:{timestamp.ToUnixTimeMilliseconds()}:{_guidProvider.NewGuid()}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content));
|
||||
return $"batch_{Convert.ToHexString(hash)[..16].ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Registry.Contracts;
|
||||
using StellaOps.Policy.Registry.Storage;
|
||||
|
||||
@@ -13,13 +14,18 @@ public sealed class ReviewWorkflowService : IReviewWorkflowService
|
||||
{
|
||||
private readonly IPolicyPackStore _packStore;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, string ReviewId), ReviewRequest> _reviews = new();
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, string ReviewId), List<ReviewAuditEntry>> _auditTrails = new();
|
||||
|
||||
public ReviewWorkflowService(IPolicyPackStore packStore, TimeProvider? timeProvider = null)
|
||||
public ReviewWorkflowService(
|
||||
IPolicyPackStore packStore,
|
||||
TimeProvider? timeProvider = null,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_packStore = packStore ?? throw new ArgumentNullException(nameof(packStore));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? GuidProvider.Default;
|
||||
}
|
||||
|
||||
public async Task<ReviewRequest> SubmitForReviewAsync(
|
||||
@@ -345,9 +351,9 @@ public sealed class ReviewWorkflowService : IReviewWorkflowService
|
||||
return $"rev_{Convert.ToHexString(hash)[..16].ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static string GenerateAuditId(Guid tenantId, string reviewId, DateTimeOffset timestamp)
|
||||
private string GenerateAuditId(Guid tenantId, string reviewId, DateTimeOffset timestamp)
|
||||
{
|
||||
var content = $"{tenantId}:{reviewId}:{timestamp.ToUnixTimeMilliseconds()}:{Guid.NewGuid()}";
|
||||
var content = $"{tenantId}:{reviewId}:{timestamp.ToUnixTimeMilliseconds()}:{_guidProvider.NewGuid()}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content));
|
||||
return $"aud_{Convert.ToHexString(hash)[..12].ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Registry.Contracts;
|
||||
|
||||
namespace StellaOps.Policy.Registry.Storage;
|
||||
@@ -9,6 +10,14 @@ namespace StellaOps.Policy.Registry.Storage;
|
||||
public sealed class InMemoryOverrideStore : IOverrideStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid OverrideId), OverrideEntity> _overrides = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public InMemoryOverrideStore(TimeProvider timeProvider, IGuidProvider guidProvider)
|
||||
{
|
||||
_timeProvider = timeProvider;
|
||||
_guidProvider = guidProvider;
|
||||
}
|
||||
|
||||
public Task<OverrideEntity> CreateAsync(
|
||||
Guid tenantId,
|
||||
@@ -16,8 +25,8 @@ public sealed class InMemoryOverrideStore : IOverrideStore
|
||||
string? createdBy = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var overrideId = Guid.NewGuid();
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var overrideId = _guidProvider.NewGuid();
|
||||
|
||||
var entity = new OverrideEntity
|
||||
{
|
||||
@@ -73,7 +82,7 @@ public sealed class InMemoryOverrideStore : IOverrideStore
|
||||
return Task.FromResult<OverrideEntity?>(null);
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var updated = existing with
|
||||
{
|
||||
Status = OverrideStatus.Approved,
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Registry.Contracts;
|
||||
|
||||
namespace StellaOps.Policy.Registry.Storage;
|
||||
@@ -13,6 +14,14 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid PackId), PolicyPackEntity> _packs = new();
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid PackId), List<PolicyPackHistoryEntry>> _history = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public InMemoryPolicyPackStore(TimeProvider timeProvider, IGuidProvider guidProvider)
|
||||
{
|
||||
_timeProvider = timeProvider;
|
||||
_guidProvider = guidProvider;
|
||||
}
|
||||
|
||||
public Task<PolicyPackEntity> CreateAsync(
|
||||
Guid tenantId,
|
||||
@@ -20,8 +29,8 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore
|
||||
string? createdBy = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var packId = Guid.NewGuid();
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var packId = _guidProvider.NewGuid();
|
||||
|
||||
var entity = new PolicyPackEntity
|
||||
{
|
||||
@@ -130,7 +139,7 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore
|
||||
Description = request.Description ?? existing.Description,
|
||||
Rules = request.Rules ?? existing.Rules,
|
||||
Metadata = request.Metadata ?? existing.Metadata,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = _timeProvider.GetUtcNow(),
|
||||
UpdatedBy = updatedBy
|
||||
};
|
||||
|
||||
@@ -178,7 +187,7 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore
|
||||
return Task.FromResult<PolicyPackEntity?>(null);
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var updated = existing with
|
||||
{
|
||||
Status = newStatus,
|
||||
@@ -228,7 +237,7 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore
|
||||
{
|
||||
PackId = packId,
|
||||
Action = action,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Timestamp = _timeProvider.GetUtcNow(),
|
||||
PerformedBy = performedBy,
|
||||
PreviousStatus = previousStatus,
|
||||
NewStatus = newStatus,
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Registry.Contracts;
|
||||
|
||||
namespace StellaOps.Policy.Registry.Storage;
|
||||
@@ -9,8 +10,10 @@ namespace StellaOps.Policy.Registry.Storage;
|
||||
/// <summary>
|
||||
/// In-memory implementation of ISnapshotStore for testing and development.
|
||||
/// </summary>
|
||||
public sealed class InMemorySnapshotStore : ISnapshotStore
|
||||
public sealed class InMemorySnapshotStore(TimeProvider timeProvider, IGuidProvider guidProvider) : ISnapshotStore
|
||||
{
|
||||
private readonly TimeProvider _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
private readonly IGuidProvider _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid SnapshotId), SnapshotEntity> _snapshots = new();
|
||||
|
||||
public Task<SnapshotEntity> CreateAsync(
|
||||
@@ -19,8 +22,8 @@ public sealed class InMemorySnapshotStore : ISnapshotStore
|
||||
string? createdBy = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var snapshotId = Guid.NewGuid();
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var snapshotId = _guidProvider.NewGuid();
|
||||
|
||||
// Compute digest from pack IDs and timestamp for uniqueness
|
||||
var digest = ComputeDigest(request.PackIds, now);
|
||||
|
||||
@@ -6,8 +6,9 @@ namespace StellaOps.Policy.Registry.Storage;
|
||||
/// <summary>
|
||||
/// In-memory implementation of IVerificationPolicyStore for testing and development.
|
||||
/// </summary>
|
||||
public sealed class InMemoryVerificationPolicyStore : IVerificationPolicyStore
|
||||
public sealed class InMemoryVerificationPolicyStore(TimeProvider timeProvider) : IVerificationPolicyStore
|
||||
{
|
||||
private readonly TimeProvider _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, string PolicyId), VerificationPolicyEntity> _policies = new();
|
||||
|
||||
public Task<VerificationPolicyEntity> CreateAsync(
|
||||
@@ -16,7 +17,7 @@ public sealed class InMemoryVerificationPolicyStore : IVerificationPolicyStore
|
||||
string? createdBy = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
var entity = new VerificationPolicyEntity
|
||||
{
|
||||
@@ -102,7 +103,7 @@ public sealed class InMemoryVerificationPolicyStore : IVerificationPolicyStore
|
||||
SignerRequirements = request.SignerRequirements ?? existing.SignerRequirements,
|
||||
ValidityWindow = request.ValidityWindow ?? existing.ValidityWindow,
|
||||
Metadata = request.Metadata ?? existing.Metadata,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = _timeProvider.GetUtcNow(),
|
||||
UpdatedBy = updatedBy
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Registry.Contracts;
|
||||
|
||||
namespace StellaOps.Policy.Registry.Storage;
|
||||
@@ -9,14 +10,22 @@ namespace StellaOps.Policy.Registry.Storage;
|
||||
public sealed class InMemoryViolationStore : IViolationStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid ViolationId), ViolationEntity> _violations = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public InMemoryViolationStore(TimeProvider timeProvider, IGuidProvider guidProvider)
|
||||
{
|
||||
_timeProvider = timeProvider;
|
||||
_guidProvider = guidProvider;
|
||||
}
|
||||
|
||||
public Task<ViolationEntity> AppendAsync(
|
||||
Guid tenantId,
|
||||
CreateViolationRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var violationId = Guid.NewGuid();
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var violationId = _guidProvider.NewGuid();
|
||||
|
||||
var entity = new ViolationEntity
|
||||
{
|
||||
@@ -42,7 +51,7 @@ public sealed class InMemoryViolationStore : IViolationStore
|
||||
IReadOnlyList<CreateViolationRequest> requests,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
int created = 0;
|
||||
int failed = 0;
|
||||
var errors = new List<BatchError>();
|
||||
@@ -52,7 +61,7 @@ public sealed class InMemoryViolationStore : IViolationStore
|
||||
try
|
||||
{
|
||||
var request = requests[i];
|
||||
var violationId = Guid.NewGuid();
|
||||
var violationId = _guidProvider.NewGuid();
|
||||
|
||||
var entity = new ViolationEntity
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
using StellaOps.Policy.Scoring.Engine;
|
||||
|
||||
namespace StellaOps.Policy.Scoring.Receipts;
|
||||
@@ -45,12 +46,16 @@ public sealed class ReceiptBuilder : IReceiptBuilder
|
||||
private readonly ICvssV4Engine _engine;
|
||||
private readonly IReceiptRepository _repository;
|
||||
private readonly EnvelopeSignatureService _signatureService;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public ReceiptBuilder(ICvssV4Engine engine, IReceiptRepository repository)
|
||||
public ReceiptBuilder(ICvssV4Engine engine, IReceiptRepository repository, TimeProvider timeProvider, IGuidProvider guidProvider)
|
||||
{
|
||||
_engine = engine;
|
||||
_repository = repository;
|
||||
_signatureService = new EnvelopeSignatureService();
|
||||
_timeProvider = timeProvider;
|
||||
_guidProvider = guidProvider;
|
||||
}
|
||||
|
||||
public async Task<CvssScoreReceipt> CreateAsync(CreateReceiptRequest request, CancellationToken cancellationToken = default)
|
||||
@@ -60,7 +65,7 @@ public sealed class ReceiptBuilder : IReceiptBuilder
|
||||
|
||||
ValidateEvidence(request.Policy, request.Evidence);
|
||||
|
||||
var createdAt = request.CreatedAt ?? DateTimeOffset.UtcNow;
|
||||
var createdAt = request.CreatedAt ?? _timeProvider.GetUtcNow();
|
||||
|
||||
// Compute scores and vector
|
||||
var scores = _engine.ComputeScores(request.BaseMetrics, request.ThreatMetrics, request.EnvironmentalMetrics);
|
||||
@@ -83,7 +88,7 @@ public sealed class ReceiptBuilder : IReceiptBuilder
|
||||
|
||||
var receipt = new CvssScoreReceipt
|
||||
{
|
||||
ReceiptId = Guid.NewGuid().ToString("N"),
|
||||
ReceiptId = _guidProvider.NewGuid().ToString("N"),
|
||||
TenantId = request.TenantId,
|
||||
VulnerabilityId = request.VulnerabilityId,
|
||||
CreatedAt = createdAt,
|
||||
@@ -103,7 +108,7 @@ public sealed class ReceiptBuilder : IReceiptBuilder
|
||||
InputHash = ComputeInputHash(request, scores, policyRef, vector, evidence),
|
||||
History = ImmutableList<ReceiptHistoryEntry>.Empty.Add(new ReceiptHistoryEntry
|
||||
{
|
||||
HistoryId = Guid.NewGuid().ToString("N"),
|
||||
HistoryId = _guidProvider.NewGuid().ToString("N"),
|
||||
Timestamp = createdAt,
|
||||
Actor = request.CreatedBy,
|
||||
ChangeType = ReceiptChangeType.Created,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Determinism.Abstractions;
|
||||
|
||||
namespace StellaOps.Policy.Scoring.Receipts;
|
||||
|
||||
@@ -25,10 +26,14 @@ public sealed class ReceiptHistoryService : IReceiptHistoryService
|
||||
{
|
||||
private readonly IReceiptRepository _repository;
|
||||
private readonly EnvelopeSignatureService _signatureService = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public ReceiptHistoryService(IReceiptRepository repository)
|
||||
public ReceiptHistoryService(IReceiptRepository repository, TimeProvider timeProvider, IGuidProvider guidProvider)
|
||||
{
|
||||
_repository = repository;
|
||||
_timeProvider = timeProvider;
|
||||
_guidProvider = guidProvider;
|
||||
}
|
||||
|
||||
public async Task<CvssScoreReceipt> AmendAsync(AmendReceiptRequest request, CancellationToken cancellationToken = default)
|
||||
@@ -38,8 +43,8 @@ public sealed class ReceiptHistoryService : IReceiptHistoryService
|
||||
var existing = await _repository.GetAsync(request.TenantId, request.ReceiptId, cancellationToken)
|
||||
?? throw new InvalidOperationException($"Receipt '{request.ReceiptId}' not found.");
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var historyId = Guid.NewGuid().ToString("N");
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var historyId = _guidProvider.NewGuid().ToString("N");
|
||||
|
||||
var newHistory = existing.History.Add(new ReceiptHistoryEntry
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -80,69 +80,74 @@ public sealed class ExceptionObjectTests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExceptionObject_IsEffective_WhenActiveAndNotExpired_ShouldBeTrue()
|
||||
public void ExceptionObject_IsEffectiveAt_WhenActiveAndNotExpired_ShouldBeTrue()
|
||||
{
|
||||
// Arrange
|
||||
var referenceTime = DateTimeOffset.UtcNow;
|
||||
var exception = CreateException(
|
||||
status: ExceptionStatus.Active,
|
||||
expiresAt: DateTimeOffset.UtcNow.AddDays(30));
|
||||
expiresAt: referenceTime.AddDays(30));
|
||||
|
||||
// Act & Assert
|
||||
exception.IsEffective.Should().BeTrue();
|
||||
exception.HasExpired.Should().BeFalse();
|
||||
exception.IsEffectiveAt(referenceTime).Should().BeTrue();
|
||||
exception.HasExpiredAt(referenceTime).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExceptionObject_IsEffective_WhenActiveButExpired_ShouldBeFalse()
|
||||
public void ExceptionObject_IsEffectiveAt_WhenActiveButExpired_ShouldBeFalse()
|
||||
{
|
||||
// Arrange
|
||||
var referenceTime = DateTimeOffset.UtcNow;
|
||||
var exception = CreateException(
|
||||
status: ExceptionStatus.Active,
|
||||
expiresAt: DateTimeOffset.UtcNow.AddDays(-1));
|
||||
expiresAt: referenceTime.AddDays(-1));
|
||||
|
||||
// Act & Assert
|
||||
exception.IsEffective.Should().BeFalse();
|
||||
exception.HasExpired.Should().BeTrue();
|
||||
exception.IsEffectiveAt(referenceTime).Should().BeFalse();
|
||||
exception.HasExpiredAt(referenceTime).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExceptionObject_IsEffective_WhenProposed_ShouldBeFalse()
|
||||
public void ExceptionObject_IsEffectiveAt_WhenProposed_ShouldBeFalse()
|
||||
{
|
||||
// Arrange
|
||||
var referenceTime = DateTimeOffset.UtcNow;
|
||||
var exception = CreateException(
|
||||
status: ExceptionStatus.Proposed,
|
||||
expiresAt: DateTimeOffset.UtcNow.AddDays(30));
|
||||
expiresAt: referenceTime.AddDays(30));
|
||||
|
||||
// Act & Assert
|
||||
exception.IsEffective.Should().BeFalse();
|
||||
exception.IsEffectiveAt(referenceTime).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExceptionObject_IsEffective_WhenRevoked_ShouldBeFalse()
|
||||
public void ExceptionObject_IsEffectiveAt_WhenRevoked_ShouldBeFalse()
|
||||
{
|
||||
// Arrange
|
||||
var referenceTime = DateTimeOffset.UtcNow;
|
||||
var exception = CreateException(
|
||||
status: ExceptionStatus.Revoked,
|
||||
expiresAt: DateTimeOffset.UtcNow.AddDays(30));
|
||||
expiresAt: referenceTime.AddDays(30));
|
||||
|
||||
// Act & Assert
|
||||
exception.IsEffective.Should().BeFalse();
|
||||
exception.IsEffectiveAt(referenceTime).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExceptionObject_IsEffective_WhenExpiredStatus_ShouldBeFalse()
|
||||
public void ExceptionObject_IsEffectiveAt_WhenExpiredStatus_ShouldBeFalse()
|
||||
{
|
||||
// Arrange
|
||||
var referenceTime = DateTimeOffset.UtcNow;
|
||||
var exception = CreateException(
|
||||
status: ExceptionStatus.Expired,
|
||||
expiresAt: DateTimeOffset.UtcNow.AddDays(-1));
|
||||
expiresAt: referenceTime.AddDays(-1));
|
||||
|
||||
// Act & Assert
|
||||
exception.IsEffective.Should().BeFalse();
|
||||
exception.IsEffectiveAt(referenceTime).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
|
||||
@@ -86,73 +86,79 @@ public sealed class ExceptionObjectTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsEffective_WhenActiveAndNotExpired_ReturnsTrue()
|
||||
public void IsEffectiveAt_WhenActiveAndNotExpired_ReturnsTrue()
|
||||
{
|
||||
var referenceTime = DateTimeOffset.UtcNow;
|
||||
var exception = CreateValidException() with
|
||||
{
|
||||
Status = ExceptionStatus.Active,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30)
|
||||
ExpiresAt = referenceTime.AddDays(30)
|
||||
};
|
||||
|
||||
Assert.True(exception.IsEffective);
|
||||
Assert.True(exception.IsEffectiveAt(referenceTime));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsEffective_WhenActiveButExpired_ReturnsFalse()
|
||||
public void IsEffectiveAt_WhenActiveButExpired_ReturnsFalse()
|
||||
{
|
||||
var referenceTime = DateTimeOffset.UtcNow;
|
||||
var exception = CreateValidException() with
|
||||
{
|
||||
Status = ExceptionStatus.Active,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(-1)
|
||||
ExpiresAt = referenceTime.AddDays(-1)
|
||||
};
|
||||
|
||||
Assert.False(exception.IsEffective);
|
||||
Assert.False(exception.IsEffectiveAt(referenceTime));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsEffective_WhenProposed_ReturnsFalse()
|
||||
public void IsEffectiveAt_WhenProposed_ReturnsFalse()
|
||||
{
|
||||
var referenceTime = DateTimeOffset.UtcNow;
|
||||
var exception = CreateValidException() with
|
||||
{
|
||||
Status = ExceptionStatus.Proposed,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30)
|
||||
ExpiresAt = referenceTime.AddDays(30)
|
||||
};
|
||||
|
||||
Assert.False(exception.IsEffective);
|
||||
Assert.False(exception.IsEffectiveAt(referenceTime));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsEffective_WhenRevoked_ReturnsFalse()
|
||||
public void IsEffectiveAt_WhenRevoked_ReturnsFalse()
|
||||
{
|
||||
var referenceTime = DateTimeOffset.UtcNow;
|
||||
var exception = CreateValidException() with
|
||||
{
|
||||
Status = ExceptionStatus.Revoked,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30)
|
||||
ExpiresAt = referenceTime.AddDays(30)
|
||||
};
|
||||
|
||||
Assert.False(exception.IsEffective);
|
||||
Assert.False(exception.IsEffectiveAt(referenceTime));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasExpired_WhenPastExpiresAt_ReturnsTrue()
|
||||
public void HasExpiredAt_WhenPastExpiresAt_ReturnsTrue()
|
||||
{
|
||||
var referenceTime = DateTimeOffset.UtcNow;
|
||||
var exception = CreateValidException() with
|
||||
{
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(-1)
|
||||
ExpiresAt = referenceTime.AddDays(-1)
|
||||
};
|
||||
|
||||
Assert.True(exception.HasExpired);
|
||||
Assert.True(exception.HasExpiredAt(referenceTime));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasExpired_WhenBeforeExpiresAt_ReturnsFalse()
|
||||
public void HasExpiredAt_WhenBeforeExpiresAt_ReturnsFalse()
|
||||
{
|
||||
var referenceTime = DateTimeOffset.UtcNow;
|
||||
var exception = CreateValidException() with
|
||||
{
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30)
|
||||
ExpiresAt = referenceTime.AddDays(30)
|
||||
};
|
||||
|
||||
Assert.False(exception.HasExpired);
|
||||
Assert.False(exception.HasExpiredAt(referenceTime));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
Reference in New Issue
Block a user