From f5f12acbf0c6d5d632440275a3fe54b5dfcc1339 Mon Sep 17 00:00:00 2001 From: StellaOps Bot Date: Sun, 4 Jan 2026 13:33:21 +0200 Subject: [PATCH] DET-004: Refactor Policy library for determinism - Gates, Snapshots, TrustLattice, Scoring, Explanation - VexProofGate: Inject TimeProvider for proof age validation - SnapshotBuilder: Inject TimeProvider for WithVex/WithSbom/WithReachability/Build - CsafVexNormalizer, OpenVexNormalizer, VexNormalizers: Add optional issuedAt parameter - TrustLatticeEngine.ClaimBuilder: Add optional issuedAt parameter to Build - PolicyBundle: Add asOf parameter to IsTrusted and GetMaxAssurance - ProofLedger: Add createdAtUtc parameter to ToJson - ScoreAttestationBuilder: Add scoredAt parameter to Create - ScoringRulesSnapshotBuilder: Add createdAt parameter to Create - TrustSourceWeightService: Inject TimeProvider for stale data calculation - PolicyExplanation.Create: Add evaluatedAt parameter - PolicyExplanationRecord.FromExplanation: Add recordId and evaluatedAt parameters - PolicyPreviewService: Inject TimeProvider for snapshot creation - PolicySnapshotStore: Inject IGuidProvider for audit entry ID generation --- .../StellaOps.Policy/Gates/VexProofGate.cs | 6 ++-- .../StellaOps.Policy/PolicyExplanation.cs | 29 +++++++++++++++---- .../StellaOps.Policy/PolicyPreviewService.cs | 9 ++++-- .../StellaOps.Policy/PolicySnapshotStore.cs | 6 +++- .../StellaOps.Policy/Scoring/ProofLedger.cs | 5 ++-- .../Scoring/ScoreAttestationStatement.cs | 12 ++++++-- .../Scoring/ScoringRulesSnapshot.cs | 7 +++-- .../Scoring/TrustSourceWeights.cs | 6 ++-- .../Snapshots/SnapshotBuilder.cs | 12 ++++---- .../TrustLattice/CsafVexNormalizer.cs | 5 ++-- .../TrustLattice/OpenVexNormalizer.cs | 5 ++-- .../TrustLattice/PolicyBundle.cs | 13 ++++++--- .../TrustLattice/TrustLatticeEngine.cs | 5 ++-- .../TrustLattice/VexNormalizers.cs | 6 ++-- 14 files changed, 91 insertions(+), 35 deletions(-) diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/VexProofGate.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/VexProofGate.cs index 392abd1b1..e481c2092 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/VexProofGate.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/VexProofGate.cs @@ -104,6 +104,7 @@ public sealed record VexProofGateContext public sealed class VexProofGate : IPolicyGate { private readonly VexProofGateOptions _options; + private readonly TimeProvider _timeProvider; // Confidence tier ordering for comparison private static readonly IReadOnlyDictionary ConfidenceTierOrder = @@ -114,9 +115,10 @@ public sealed class VexProofGate : IPolicyGate ["high"] = 3, }; - public VexProofGate(VexProofGateOptions? options = null) + public VexProofGate(VexProofGateOptions? options = null, TimeProvider? timeProvider = null) { _options = options ?? new VexProofGateOptions(); + _timeProvider = timeProvider ?? TimeProvider.System; } public Task EvaluateAsync( @@ -207,7 +209,7 @@ public sealed class VexProofGate : IPolicyGate // Validate proof age if (_options.MaxProofAgeHours >= 0 && proofContext.ProofComputedAt.HasValue) { - var proofAge = DateTimeOffset.UtcNow - proofContext.ProofComputedAt.Value; + var proofAge = _timeProvider.GetUtcNow() - proofContext.ProofComputedAt.Value; details["proofAgeHours"] = proofAge.TotalHours; details["maxProofAgeHours"] = _options.MaxProofAgeHours; diff --git a/src/Policy/__Libraries/StellaOps.Policy/PolicyExplanation.cs b/src/Policy/__Libraries/StellaOps.Policy/PolicyExplanation.cs index c0c1054aa..c7337099e 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/PolicyExplanation.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/PolicyExplanation.cs @@ -61,6 +61,16 @@ public sealed record PolicyExplanation( /// /// Creates an explanation with full context for persistence. /// + /// The finding ID. + /// The policy decision. + /// The rule name. + /// The reason for the decision. + /// The explanation nodes. + /// Optional rule hits. + /// Optional evaluated inputs. + /// Optional policy version. + /// Optional correlation ID. + /// Optional timestamp for deterministic testing. If null, uses current time. public static PolicyExplanation Create( string findingId, PolicyVerdictStatus decision, @@ -70,12 +80,13 @@ public sealed record PolicyExplanation( IEnumerable? ruleHits = null, IDictionary? inputs = null, string? policyVersion = null, - string? correlationId = null) => + string? correlationId = null, + DateTimeOffset? evaluatedAt = null) => new(findingId, decision, ruleName, reason, nodes.ToImmutableArray()) { RuleHits = ruleHits?.ToImmutableArray() ?? ImmutableArray.Empty, EvaluatedInputs = inputs?.ToImmutableDictionary() ?? ImmutableDictionary.Empty, - EvaluatedAt = DateTimeOffset.UtcNow, + EvaluatedAt = evaluatedAt ?? DateTimeOffset.UtcNow, PolicyVersion = policyVersion, CorrelationId = correlationId }; @@ -214,13 +225,21 @@ public sealed record PolicyExplanationRecord( /// /// Creates a persistence record from an explanation. /// + /// The explanation to convert. + /// The policy ID. + /// Optional tenant identifier. + /// Optional actor who triggered the evaluation. + /// Optional record ID for deterministic testing. If null, generates a new GUID. + /// Optional timestamp for deterministic testing. If null, uses current time. public static PolicyExplanationRecord FromExplanation( PolicyExplanation explanation, string policyId, string? tenantId = null, - string? actor = null) + string? actor = null, + string? recordId = null, + DateTimeOffset? evaluatedAt = null) { - var id = $"pexp-{Guid.NewGuid():N}"; + 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); @@ -235,7 +254,7 @@ public sealed record PolicyExplanationRecord( RuleHitsJson: ruleHitsJson, InputsJson: inputsJson, ExplanationTreeJson: treeJson, - EvaluatedAt: explanation.EvaluatedAt ?? DateTimeOffset.UtcNow, + EvaluatedAt: explanation.EvaluatedAt ?? evaluatedAt ?? DateTimeOffset.UtcNow, CorrelationId: explanation.CorrelationId, TenantId: tenantId, Actor: actor); diff --git a/src/Policy/__Libraries/StellaOps.Policy/PolicyPreviewService.cs b/src/Policy/__Libraries/StellaOps.Policy/PolicyPreviewService.cs index fb833587f..bbf92ad3b 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/PolicyPreviewService.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/PolicyPreviewService.cs @@ -13,11 +13,16 @@ public sealed class PolicyPreviewService { private readonly PolicySnapshotStore _snapshotStore; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; - public PolicyPreviewService(PolicySnapshotStore snapshotStore, ILogger logger) + public PolicyPreviewService( + PolicySnapshotStore snapshotStore, + ILogger logger, + TimeProvider? timeProvider = null) { _snapshotStore = snapshotStore ?? throw new ArgumentNullException(nameof(snapshotStore)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task PreviewAsync(PolicyPreviewRequest request, CancellationToken cancellationToken = default) @@ -59,7 +64,7 @@ public sealed class PolicyPreviewService request.SnapshotOverride?.RevisionNumber + 1 ?? 0, request.SnapshotOverride?.RevisionId ?? "preview", digest, - DateTimeOffset.UtcNow, + _timeProvider.GetUtcNow(), request.ProposedPolicy.Actor, request.ProposedPolicy.Format, binding.Document, diff --git a/src/Policy/__Libraries/StellaOps.Policy/PolicySnapshotStore.cs b/src/Policy/__Libraries/StellaOps.Policy/PolicySnapshotStore.cs index 7ff8008e7..46dbec6ea 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/PolicySnapshotStore.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/PolicySnapshotStore.cs @@ -2,6 +2,7 @@ using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using StellaOps.Determinism; namespace StellaOps.Policy; @@ -10,6 +11,7 @@ public sealed class PolicySnapshotStore private readonly IPolicySnapshotRepository _snapshotRepository; private readonly IPolicyAuditRepository _auditRepository; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; private readonly ILogger _logger; private readonly SemaphoreSlim _mutex = new(1, 1); @@ -17,11 +19,13 @@ public sealed class PolicySnapshotStore IPolicySnapshotRepository snapshotRepository, IPolicyAuditRepository auditRepository, TimeProvider? timeProvider, + IGuidProvider? guidProvider, ILogger logger) { _snapshotRepository = snapshotRepository ?? throw new ArgumentNullException(nameof(snapshotRepository)); _auditRepository = auditRepository ?? throw new ArgumentNullException(nameof(auditRepository)); _timeProvider = timeProvider ?? TimeProvider.System; + _guidProvider = guidProvider ?? SystemGuidProvider.Instance; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -72,7 +76,7 @@ public sealed class PolicySnapshotStore var auditMessage = content.Description ?? "Policy snapshot created"; var auditEntry = new PolicyAuditEntry( - Guid.NewGuid(), + _guidProvider.NewGuid(), createdAt, "snapshot.created", revisionId, diff --git a/src/Policy/__Libraries/StellaOps.Policy/Scoring/ProofLedger.cs b/src/Policy/__Libraries/StellaOps.Policy/Scoring/ProofLedger.cs index b37fa5d84..5fd8baf42 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Scoring/ProofLedger.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Scoring/ProofLedger.cs @@ -118,15 +118,16 @@ public sealed class ProofLedger /// Serialize the ledger to JSON. /// /// Optional JSON serializer options. + /// Optional timestamp for deterministic testing. If null, uses current time. /// The JSON representation of the ledger. - public string ToJson(JsonSerializerOptions? options = null) + public string ToJson(JsonSerializerOptions? options = null, DateTimeOffset? createdAtUtc = null) { lock (_lock) { var payload = new ProofLedgerPayload( Nodes: [.. _nodes], RootHash: RootHash(), - CreatedAtUtc: DateTimeOffset.UtcNow); + CreatedAtUtc: createdAtUtc ?? DateTimeOffset.UtcNow); return JsonSerializer.Serialize(payload, options ?? DefaultJsonOptions); } diff --git a/src/Policy/__Libraries/StellaOps.Policy/Scoring/ScoreAttestationStatement.cs b/src/Policy/__Libraries/StellaOps.Policy/Scoring/ScoreAttestationStatement.cs index 8bf7c0e4f..39201a85a 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Scoring/ScoreAttestationStatement.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Scoring/ScoreAttestationStatement.cs @@ -320,17 +320,25 @@ public sealed class ScoreAttestationBuilder /// /// Creates a new builder. /// + /// The subject digest. + /// The overall score. + /// The confidence value. + /// The score breakdown. + /// The scoring policy reference. + /// The scoring inputs. + /// Optional timestamp for deterministic testing. If null, uses current time. public static ScoreAttestationBuilder Create( string subjectDigest, int overallScore, double confidence, ScoreBreakdown breakdown, ScoringPolicyRef policy, - ScoringInputs inputs) + ScoringInputs inputs, + DateTimeOffset? scoredAt = null) { return new ScoreAttestationBuilder(new ScoreAttestationStatement { - ScoredAt = DateTimeOffset.UtcNow, + ScoredAt = scoredAt ?? DateTimeOffset.UtcNow, SubjectDigest = subjectDigest, OverallScore = overallScore, Confidence = confidence, diff --git a/src/Policy/__Libraries/StellaOps.Policy/Scoring/ScoringRulesSnapshot.cs b/src/Policy/__Libraries/StellaOps.Policy/Scoring/ScoringRulesSnapshot.cs index c9873e458..66e51c901 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Scoring/ScoringRulesSnapshot.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Scoring/ScoringRulesSnapshot.cs @@ -346,13 +346,16 @@ public sealed class ScoringRulesSnapshotBuilder /// /// Creates a new builder with defaults. /// - public static ScoringRulesSnapshotBuilder Create(string id, int version) + /// The snapshot ID. + /// The snapshot version. + /// Optional timestamp for deterministic testing. If null, uses current time. + public static ScoringRulesSnapshotBuilder Create(string id, int version, DateTimeOffset? createdAt = null) { return new ScoringRulesSnapshotBuilder(new ScoringRulesSnapshot { Id = id, Version = version, - CreatedAt = DateTimeOffset.UtcNow, + CreatedAt = createdAt ?? DateTimeOffset.UtcNow, Digest = "", // Will be computed on build Weights = new ScoringWeights(), Thresholds = new GradeThresholds(), diff --git a/src/Policy/__Libraries/StellaOps.Policy/Scoring/TrustSourceWeights.cs b/src/Policy/__Libraries/StellaOps.Policy/Scoring/TrustSourceWeights.cs index 364b7b377..120894b12 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Scoring/TrustSourceWeights.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Scoring/TrustSourceWeights.cs @@ -281,10 +281,12 @@ public sealed record WeightedMergeResult public sealed class TrustSourceWeightService { private readonly TrustSourceWeightConfig _config; + private readonly TimeProvider _timeProvider; - public TrustSourceWeightService(TrustSourceWeightConfig? config = null) + public TrustSourceWeightService(TrustSourceWeightConfig? config = null, TimeProvider? timeProvider = null) { _config = config ?? new TrustSourceWeightConfig(); + _timeProvider = timeProvider ?? TimeProvider.System; } /// @@ -320,7 +322,7 @@ public sealed class TrustSourceWeightService // Penalty for stale data (>7 days old) if (source.FetchedAt.HasValue) { - var age = DateTimeOffset.UtcNow - source.FetchedAt.Value; + var age = _timeProvider.GetUtcNow() - source.FetchedAt.Value; if (age.TotalDays > 7) { weight *= 0.95; diff --git a/src/Policy/__Libraries/StellaOps.Policy/Snapshots/SnapshotBuilder.cs b/src/Policy/__Libraries/StellaOps.Policy/Snapshots/SnapshotBuilder.cs index 68201186a..a8bbe73f6 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Snapshots/SnapshotBuilder.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Snapshots/SnapshotBuilder.cs @@ -18,10 +18,12 @@ public sealed class SnapshotBuilder private TrustBundleRef? _trust; private DeterminismProfile? _environment; private readonly ICryptoHash _cryptoHash; + private readonly TimeProvider _timeProvider; - public SnapshotBuilder(ICryptoHash cryptoHash) + public SnapshotBuilder(ICryptoHash cryptoHash, TimeProvider? timeProvider = null) { _cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash)); + _timeProvider = timeProvider ?? TimeProvider.System; } public SnapshotBuilder WithEngine(string name, string version, string commit) @@ -81,7 +83,7 @@ public sealed class SnapshotBuilder { Name = name, Type = KnowledgeSourceTypes.Vex, - Epoch = DateTimeOffset.UtcNow.ToString("o", CultureInfo.InvariantCulture), + Epoch = _timeProvider.GetUtcNow().ToString("o", CultureInfo.InvariantCulture), Digest = digest, Origin = origin }); @@ -94,7 +96,7 @@ public sealed class SnapshotBuilder { Name = name, Type = KnowledgeSourceTypes.Sbom, - Epoch = DateTimeOffset.UtcNow.ToString("o", CultureInfo.InvariantCulture), + Epoch = _timeProvider.GetUtcNow().ToString("o", CultureInfo.InvariantCulture), Digest = digest, Origin = origin }); @@ -107,7 +109,7 @@ public sealed class SnapshotBuilder { Name = name, Type = KnowledgeSourceTypes.Reachability, - Epoch = DateTimeOffset.UtcNow.ToString("o", CultureInfo.InvariantCulture), + Epoch = _timeProvider.GetUtcNow().ToString("o", CultureInfo.InvariantCulture), Digest = digest, Origin = origin }); @@ -148,7 +150,7 @@ public sealed class SnapshotBuilder var manifest = new KnowledgeSnapshotManifest { SnapshotId = "", // Placeholder - CreatedAt = DateTimeOffset.UtcNow, + CreatedAt = _timeProvider.GetUtcNow(), Engine = _engine, Plugins = _plugins.ToList(), Policy = _policy, diff --git a/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/CsafVexNormalizer.cs b/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/CsafVexNormalizer.cs index 9f28b28ce..98447ce58 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/CsafVexNormalizer.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/CsafVexNormalizer.cs @@ -186,7 +186,8 @@ public sealed class CsafVexNormalizer : IVexNormalizer CsafFlagLabel flag = CsafFlagLabel.None, string? remediation = null, Principal? principal = null, - TrustLabel? trustLabel = null) + TrustLabel? trustLabel = null, + DateTimeOffset? issuedAt = null) { var assertions = new List(); @@ -220,7 +221,7 @@ public sealed class CsafVexNormalizer : IVexNormalizer Issuer = principal ?? Principal.Unknown, Assertions = assertions, TrustLabel = trustLabel, - Time = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow }, + Time = new ClaimTimeInfo { IssuedAt = issuedAt ?? DateTimeOffset.UtcNow }, }; } } diff --git a/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/OpenVexNormalizer.cs b/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/OpenVexNormalizer.cs index 72c04afd9..45697ef21 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/OpenVexNormalizer.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/OpenVexNormalizer.cs @@ -149,7 +149,8 @@ public sealed class OpenVexNormalizer : IVexNormalizer string? actionStatement = null, string? impactStatement = null, Principal? principal = null, - TrustLabel? trustLabel = null) + TrustLabel? trustLabel = null, + DateTimeOffset? issuedAt = null) { var assertions = new List(); @@ -191,7 +192,7 @@ public sealed class OpenVexNormalizer : IVexNormalizer Issuer = principal ?? Principal.Unknown, Assertions = assertions, TrustLabel = trustLabel, - Time = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow }, + Time = new ClaimTimeInfo { IssuedAt = issuedAt ?? DateTimeOffset.UtcNow }, }; } } diff --git a/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/PolicyBundle.cs b/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/PolicyBundle.cs index 6fb0c0824..57939395d 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/PolicyBundle.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/PolicyBundle.cs @@ -235,9 +235,12 @@ public sealed record PolicyBundle /// /// Checks if a principal is trusted for a given scope. /// - public bool IsTrusted(Principal principal, AuthorityScope? requiredScope = null) + /// The principal to check. + /// Optional required authority scope. + /// Optional timestamp for deterministic testing. If null, uses current time. + public bool IsTrusted(Principal principal, AuthorityScope? requiredScope = null, DateTimeOffset? asOf = null) { - var now = DateTimeOffset.UtcNow; + var now = asOf ?? DateTimeOffset.UtcNow; foreach (var root in TrustRoots) { @@ -257,9 +260,11 @@ public sealed record PolicyBundle /// /// Gets the maximum assurance level for a principal. /// - public AssuranceLevel? GetMaxAssurance(Principal principal) + /// The principal to check. + /// Optional timestamp for deterministic testing. If null, uses current time. + public AssuranceLevel? GetMaxAssurance(Principal principal, DateTimeOffset? asOf = null) { - var now = DateTimeOffset.UtcNow; + var now = asOf ?? DateTimeOffset.UtcNow; foreach (var root in TrustRoots) { diff --git a/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/TrustLatticeEngine.cs b/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/TrustLatticeEngine.cs index 5ac3c0987..8731d4189 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/TrustLatticeEngine.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/TrustLatticeEngine.cs @@ -397,7 +397,8 @@ public sealed class TrustLatticeEngine /// /// Builds and ingests the claim. /// - public Claim Build() + /// Optional timestamp for deterministic testing. If null, uses current time. + public Claim Build(DateTimeOffset? issuedAt = null) { if (_subject is null) throw new InvalidOperationException("Subject is required."); @@ -409,7 +410,7 @@ public sealed class TrustLatticeEngine TrustLabel = _trustLabel, Assertions = _assertions, EvidenceRefs = _evidenceRefs, - Time = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow }, + Time = new ClaimTimeInfo { IssuedAt = issuedAt ?? DateTimeOffset.UtcNow }, }; return _engine.IngestClaim(claim); diff --git a/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/VexNormalizers.cs b/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/VexNormalizers.cs index c10041ee1..1872cc483 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/VexNormalizers.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/VexNormalizers.cs @@ -270,6 +270,7 @@ public sealed class CycloneDxVexNormalizer : IVexNormalizer /// Optional detail text. /// The principal making the assertion. /// Optional trust label. + /// Optional timestamp for deterministic testing. /// A normalized claim. public Claim NormalizeStatement( Subject subject, @@ -277,7 +278,8 @@ public sealed class CycloneDxVexNormalizer : IVexNormalizer CycloneDxJustification justification = CycloneDxJustification.None, string? detail = null, Principal? principal = null, - TrustLabel? trustLabel = null) + TrustLabel? trustLabel = null, + DateTimeOffset? issuedAt = null) { var assertions = new List(); @@ -312,7 +314,7 @@ public sealed class CycloneDxVexNormalizer : IVexNormalizer Issuer = principal ?? Principal.Unknown, Assertions = assertions, TrustLabel = trustLabel, - Time = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow }, + Time = new ClaimTimeInfo { IssuedAt = issuedAt ?? DateTimeOffset.UtcNow }, }; } }