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
This commit is contained in:
StellaOps Bot
2026-01-04 13:33:21 +02:00
parent ae78af4692
commit f5f12acbf0
14 changed files with 91 additions and 35 deletions

View File

@@ -104,6 +104,7 @@ public sealed record VexProofGateContext
public sealed class VexProofGate : IPolicyGate public sealed class VexProofGate : IPolicyGate
{ {
private readonly VexProofGateOptions _options; private readonly VexProofGateOptions _options;
private readonly TimeProvider _timeProvider;
// Confidence tier ordering for comparison // Confidence tier ordering for comparison
private static readonly IReadOnlyDictionary<string, int> ConfidenceTierOrder = private static readonly IReadOnlyDictionary<string, int> ConfidenceTierOrder =
@@ -114,9 +115,10 @@ public sealed class VexProofGate : IPolicyGate
["high"] = 3, ["high"] = 3,
}; };
public VexProofGate(VexProofGateOptions? options = null) public VexProofGate(VexProofGateOptions? options = null, TimeProvider? timeProvider = null)
{ {
_options = options ?? new VexProofGateOptions(); _options = options ?? new VexProofGateOptions();
_timeProvider = timeProvider ?? TimeProvider.System;
} }
public Task<GateResult> EvaluateAsync( public Task<GateResult> EvaluateAsync(
@@ -207,7 +209,7 @@ public sealed class VexProofGate : IPolicyGate
// Validate proof age // Validate proof age
if (_options.MaxProofAgeHours >= 0 && proofContext.ProofComputedAt.HasValue) 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["proofAgeHours"] = proofAge.TotalHours;
details["maxProofAgeHours"] = _options.MaxProofAgeHours; details["maxProofAgeHours"] = _options.MaxProofAgeHours;

View File

@@ -61,6 +61,16 @@ public sealed record PolicyExplanation(
/// <summary> /// <summary>
/// Creates an explanation with full context for persistence. /// Creates an explanation with full context for persistence.
/// </summary> /// </summary>
/// <param name="findingId">The finding ID.</param>
/// <param name="decision">The policy decision.</param>
/// <param name="ruleName">The rule name.</param>
/// <param name="reason">The reason for the decision.</param>
/// <param name="nodes">The explanation nodes.</param>
/// <param name="ruleHits">Optional rule hits.</param>
/// <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>
public static PolicyExplanation Create( public static PolicyExplanation Create(
string findingId, string findingId,
PolicyVerdictStatus decision, PolicyVerdictStatus decision,
@@ -70,12 +80,13 @@ public sealed record PolicyExplanation(
IEnumerable<RuleHit>? ruleHits = null, IEnumerable<RuleHit>? ruleHits = null,
IDictionary<string, object?>? inputs = null, IDictionary<string, object?>? inputs = null,
string? policyVersion = null, string? policyVersion = null,
string? correlationId = null) => string? correlationId = null,
DateTimeOffset? evaluatedAt = null) =>
new(findingId, decision, ruleName, reason, nodes.ToImmutableArray()) new(findingId, decision, ruleName, reason, nodes.ToImmutableArray())
{ {
RuleHits = ruleHits?.ToImmutableArray() ?? ImmutableArray<RuleHit>.Empty, RuleHits = ruleHits?.ToImmutableArray() ?? ImmutableArray<RuleHit>.Empty,
EvaluatedInputs = inputs?.ToImmutableDictionary() ?? ImmutableDictionary<string, object?>.Empty, EvaluatedInputs = inputs?.ToImmutableDictionary() ?? ImmutableDictionary<string, object?>.Empty,
EvaluatedAt = DateTimeOffset.UtcNow, EvaluatedAt = evaluatedAt ?? DateTimeOffset.UtcNow,
PolicyVersion = policyVersion, PolicyVersion = policyVersion,
CorrelationId = correlationId CorrelationId = correlationId
}; };
@@ -214,13 +225,21 @@ public sealed record PolicyExplanationRecord(
/// <summary> /// <summary>
/// Creates a persistence record from an explanation. /// Creates a persistence record from an explanation.
/// </summary> /// </summary>
/// <param name="explanation">The explanation to convert.</param>
/// <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>
public static PolicyExplanationRecord FromExplanation( public static PolicyExplanationRecord FromExplanation(
PolicyExplanation explanation, PolicyExplanation explanation,
string policyId, string policyId,
string? tenantId = null, 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 ruleHitsJson = System.Text.Json.JsonSerializer.Serialize(explanation.RuleHits);
var inputsJson = System.Text.Json.JsonSerializer.Serialize(explanation.EvaluatedInputs); var inputsJson = System.Text.Json.JsonSerializer.Serialize(explanation.EvaluatedInputs);
var treeJson = System.Text.Json.JsonSerializer.Serialize(explanation.Nodes); var treeJson = System.Text.Json.JsonSerializer.Serialize(explanation.Nodes);
@@ -235,7 +254,7 @@ public sealed record PolicyExplanationRecord(
RuleHitsJson: ruleHitsJson, RuleHitsJson: ruleHitsJson,
InputsJson: inputsJson, InputsJson: inputsJson,
ExplanationTreeJson: treeJson, ExplanationTreeJson: treeJson,
EvaluatedAt: explanation.EvaluatedAt ?? DateTimeOffset.UtcNow, EvaluatedAt: explanation.EvaluatedAt ?? evaluatedAt ?? DateTimeOffset.UtcNow,
CorrelationId: explanation.CorrelationId, CorrelationId: explanation.CorrelationId,
TenantId: tenantId, TenantId: tenantId,
Actor: actor); Actor: actor);

View File

@@ -13,11 +13,16 @@ public sealed class PolicyPreviewService
{ {
private readonly PolicySnapshotStore _snapshotStore; private readonly PolicySnapshotStore _snapshotStore;
private readonly ILogger<PolicyPreviewService> _logger; private readonly ILogger<PolicyPreviewService> _logger;
private readonly TimeProvider _timeProvider;
public PolicyPreviewService(PolicySnapshotStore snapshotStore, ILogger<PolicyPreviewService> logger) public PolicyPreviewService(
PolicySnapshotStore snapshotStore,
ILogger<PolicyPreviewService> logger,
TimeProvider? timeProvider = null)
{ {
_snapshotStore = snapshotStore ?? throw new ArgumentNullException(nameof(snapshotStore)); _snapshotStore = snapshotStore ?? throw new ArgumentNullException(nameof(snapshotStore));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
} }
public async Task<PolicyPreviewResponse> PreviewAsync(PolicyPreviewRequest request, CancellationToken cancellationToken = default) public async Task<PolicyPreviewResponse> PreviewAsync(PolicyPreviewRequest request, CancellationToken cancellationToken = default)
@@ -59,7 +64,7 @@ public sealed class PolicyPreviewService
request.SnapshotOverride?.RevisionNumber + 1 ?? 0, request.SnapshotOverride?.RevisionNumber + 1 ?? 0,
request.SnapshotOverride?.RevisionId ?? "preview", request.SnapshotOverride?.RevisionId ?? "preview",
digest, digest,
DateTimeOffset.UtcNow, _timeProvider.GetUtcNow(),
request.ProposedPolicy.Actor, request.ProposedPolicy.Actor,
request.ProposedPolicy.Format, request.ProposedPolicy.Format,
binding.Document, binding.Document,

View File

@@ -2,6 +2,7 @@ using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using StellaOps.Determinism;
namespace StellaOps.Policy; namespace StellaOps.Policy;
@@ -10,6 +11,7 @@ public sealed class PolicySnapshotStore
private readonly IPolicySnapshotRepository _snapshotRepository; private readonly IPolicySnapshotRepository _snapshotRepository;
private readonly IPolicyAuditRepository _auditRepository; private readonly IPolicyAuditRepository _auditRepository;
private readonly TimeProvider _timeProvider; private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
private readonly ILogger<PolicySnapshotStore> _logger; private readonly ILogger<PolicySnapshotStore> _logger;
private readonly SemaphoreSlim _mutex = new(1, 1); private readonly SemaphoreSlim _mutex = new(1, 1);
@@ -17,11 +19,13 @@ public sealed class PolicySnapshotStore
IPolicySnapshotRepository snapshotRepository, IPolicySnapshotRepository snapshotRepository,
IPolicyAuditRepository auditRepository, IPolicyAuditRepository auditRepository,
TimeProvider? timeProvider, TimeProvider? timeProvider,
IGuidProvider? guidProvider,
ILogger<PolicySnapshotStore> logger) ILogger<PolicySnapshotStore> logger)
{ {
_snapshotRepository = snapshotRepository ?? throw new ArgumentNullException(nameof(snapshotRepository)); _snapshotRepository = snapshotRepository ?? throw new ArgumentNullException(nameof(snapshotRepository));
_auditRepository = auditRepository ?? throw new ArgumentNullException(nameof(auditRepository)); _auditRepository = auditRepository ?? throw new ArgumentNullException(nameof(auditRepository));
_timeProvider = timeProvider ?? TimeProvider.System; _timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
@@ -72,7 +76,7 @@ public sealed class PolicySnapshotStore
var auditMessage = content.Description ?? "Policy snapshot created"; var auditMessage = content.Description ?? "Policy snapshot created";
var auditEntry = new PolicyAuditEntry( var auditEntry = new PolicyAuditEntry(
Guid.NewGuid(), _guidProvider.NewGuid(),
createdAt, createdAt,
"snapshot.created", "snapshot.created",
revisionId, revisionId,

View File

@@ -118,15 +118,16 @@ public sealed class ProofLedger
/// Serialize the ledger to JSON. /// Serialize the ledger to JSON.
/// </summary> /// </summary>
/// <param name="options">Optional JSON serializer options.</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> /// <returns>The JSON representation of the ledger.</returns>
public string ToJson(JsonSerializerOptions? options = null) public string ToJson(JsonSerializerOptions? options = null, DateTimeOffset? createdAtUtc = null)
{ {
lock (_lock) lock (_lock)
{ {
var payload = new ProofLedgerPayload( var payload = new ProofLedgerPayload(
Nodes: [.. _nodes], Nodes: [.. _nodes],
RootHash: RootHash(), RootHash: RootHash(),
CreatedAtUtc: DateTimeOffset.UtcNow); CreatedAtUtc: createdAtUtc ?? DateTimeOffset.UtcNow);
return JsonSerializer.Serialize(payload, options ?? DefaultJsonOptions); return JsonSerializer.Serialize(payload, options ?? DefaultJsonOptions);
} }

View File

@@ -320,17 +320,25 @@ public sealed class ScoreAttestationBuilder
/// <summary> /// <summary>
/// Creates a new builder. /// Creates a new builder.
/// </summary> /// </summary>
/// <param name="subjectDigest">The subject digest.</param>
/// <param name="overallScore">The overall score.</param>
/// <param name="confidence">The confidence value.</param>
/// <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>
public static ScoreAttestationBuilder Create( public static ScoreAttestationBuilder Create(
string subjectDigest, string subjectDigest,
int overallScore, int overallScore,
double confidence, double confidence,
ScoreBreakdown breakdown, ScoreBreakdown breakdown,
ScoringPolicyRef policy, ScoringPolicyRef policy,
ScoringInputs inputs) ScoringInputs inputs,
DateTimeOffset? scoredAt = null)
{ {
return new ScoreAttestationBuilder(new ScoreAttestationStatement return new ScoreAttestationBuilder(new ScoreAttestationStatement
{ {
ScoredAt = DateTimeOffset.UtcNow, ScoredAt = scoredAt ?? DateTimeOffset.UtcNow,
SubjectDigest = subjectDigest, SubjectDigest = subjectDigest,
OverallScore = overallScore, OverallScore = overallScore,
Confidence = confidence, Confidence = confidence,

View File

@@ -346,13 +346,16 @@ public sealed class ScoringRulesSnapshotBuilder
/// <summary> /// <summary>
/// Creates a new builder with defaults. /// Creates a new builder with defaults.
/// </summary> /// </summary>
public static ScoringRulesSnapshotBuilder Create(string id, int version) /// <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)
{ {
return new ScoringRulesSnapshotBuilder(new ScoringRulesSnapshot return new ScoringRulesSnapshotBuilder(new ScoringRulesSnapshot
{ {
Id = id, Id = id,
Version = version, Version = version,
CreatedAt = DateTimeOffset.UtcNow, CreatedAt = createdAt ?? DateTimeOffset.UtcNow,
Digest = "", // Will be computed on build Digest = "", // Will be computed on build
Weights = new ScoringWeights(), Weights = new ScoringWeights(),
Thresholds = new GradeThresholds(), Thresholds = new GradeThresholds(),

View File

@@ -281,10 +281,12 @@ public sealed record WeightedMergeResult
public sealed class TrustSourceWeightService public sealed class TrustSourceWeightService
{ {
private readonly TrustSourceWeightConfig _config; 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(); _config = config ?? new TrustSourceWeightConfig();
_timeProvider = timeProvider ?? TimeProvider.System;
} }
/// <summary> /// <summary>
@@ -320,7 +322,7 @@ public sealed class TrustSourceWeightService
// Penalty for stale data (>7 days old) // Penalty for stale data (>7 days old)
if (source.FetchedAt.HasValue) if (source.FetchedAt.HasValue)
{ {
var age = DateTimeOffset.UtcNow - source.FetchedAt.Value; var age = _timeProvider.GetUtcNow() - source.FetchedAt.Value;
if (age.TotalDays > 7) if (age.TotalDays > 7)
{ {
weight *= 0.95; weight *= 0.95;

View File

@@ -18,10 +18,12 @@ public sealed class SnapshotBuilder
private TrustBundleRef? _trust; private TrustBundleRef? _trust;
private DeterminismProfile? _environment; private DeterminismProfile? _environment;
private readonly ICryptoHash _cryptoHash; 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)); _cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
_timeProvider = timeProvider ?? TimeProvider.System;
} }
public SnapshotBuilder WithEngine(string name, string version, string commit) public SnapshotBuilder WithEngine(string name, string version, string commit)
@@ -81,7 +83,7 @@ public sealed class SnapshotBuilder
{ {
Name = name, Name = name,
Type = KnowledgeSourceTypes.Vex, Type = KnowledgeSourceTypes.Vex,
Epoch = DateTimeOffset.UtcNow.ToString("o", CultureInfo.InvariantCulture), Epoch = _timeProvider.GetUtcNow().ToString("o", CultureInfo.InvariantCulture),
Digest = digest, Digest = digest,
Origin = origin Origin = origin
}); });
@@ -94,7 +96,7 @@ public sealed class SnapshotBuilder
{ {
Name = name, Name = name,
Type = KnowledgeSourceTypes.Sbom, Type = KnowledgeSourceTypes.Sbom,
Epoch = DateTimeOffset.UtcNow.ToString("o", CultureInfo.InvariantCulture), Epoch = _timeProvider.GetUtcNow().ToString("o", CultureInfo.InvariantCulture),
Digest = digest, Digest = digest,
Origin = origin Origin = origin
}); });
@@ -107,7 +109,7 @@ public sealed class SnapshotBuilder
{ {
Name = name, Name = name,
Type = KnowledgeSourceTypes.Reachability, Type = KnowledgeSourceTypes.Reachability,
Epoch = DateTimeOffset.UtcNow.ToString("o", CultureInfo.InvariantCulture), Epoch = _timeProvider.GetUtcNow().ToString("o", CultureInfo.InvariantCulture),
Digest = digest, Digest = digest,
Origin = origin Origin = origin
}); });
@@ -148,7 +150,7 @@ public sealed class SnapshotBuilder
var manifest = new KnowledgeSnapshotManifest var manifest = new KnowledgeSnapshotManifest
{ {
SnapshotId = "", // Placeholder SnapshotId = "", // Placeholder
CreatedAt = DateTimeOffset.UtcNow, CreatedAt = _timeProvider.GetUtcNow(),
Engine = _engine, Engine = _engine,
Plugins = _plugins.ToList(), Plugins = _plugins.ToList(),
Policy = _policy, Policy = _policy,

View File

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

View File

@@ -149,7 +149,8 @@ public sealed class OpenVexNormalizer : IVexNormalizer
string? actionStatement = null, string? actionStatement = null,
string? impactStatement = null, string? impactStatement = null,
Principal? principal = null, Principal? principal = null,
TrustLabel? trustLabel = null) TrustLabel? trustLabel = null,
DateTimeOffset? issuedAt = null)
{ {
var assertions = new List<AtomAssertion>(); var assertions = new List<AtomAssertion>();
@@ -191,7 +192,7 @@ public sealed class OpenVexNormalizer : IVexNormalizer
Issuer = principal ?? Principal.Unknown, Issuer = principal ?? Principal.Unknown,
Assertions = assertions, Assertions = assertions,
TrustLabel = trustLabel, TrustLabel = trustLabel,
Time = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow }, Time = new ClaimTimeInfo { IssuedAt = issuedAt ?? DateTimeOffset.UtcNow },
}; };
} }
} }

View File

@@ -235,9 +235,12 @@ public sealed record PolicyBundle
/// <summary> /// <summary>
/// Checks if a principal is trusted for a given scope. /// Checks if a principal is trusted for a given scope.
/// </summary> /// </summary>
public bool IsTrusted(Principal principal, AuthorityScope? requiredScope = null) /// <param name="principal">The principal to check.</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)
{ {
var now = DateTimeOffset.UtcNow; var now = asOf ?? DateTimeOffset.UtcNow;
foreach (var root in TrustRoots) foreach (var root in TrustRoots)
{ {
@@ -257,9 +260,11 @@ public sealed record PolicyBundle
/// <summary> /// <summary>
/// Gets the maximum assurance level for a principal. /// Gets the maximum assurance level for a principal.
/// </summary> /// </summary>
public AssuranceLevel? GetMaxAssurance(Principal principal) /// <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)
{ {
var now = DateTimeOffset.UtcNow; var now = asOf ?? DateTimeOffset.UtcNow;
foreach (var root in TrustRoots) foreach (var root in TrustRoots)
{ {

View File

@@ -397,7 +397,8 @@ public sealed class TrustLatticeEngine
/// <summary> /// <summary>
/// Builds and ingests the claim. /// Builds and ingests the claim.
/// </summary> /// </summary>
public Claim Build() /// <param name="issuedAt">Optional timestamp for deterministic testing. If null, uses current time.</param>
public Claim Build(DateTimeOffset? issuedAt = null)
{ {
if (_subject is null) if (_subject is null)
throw new InvalidOperationException("Subject is required."); throw new InvalidOperationException("Subject is required.");
@@ -409,7 +410,7 @@ public sealed class TrustLatticeEngine
TrustLabel = _trustLabel, TrustLabel = _trustLabel,
Assertions = _assertions, Assertions = _assertions,
EvidenceRefs = _evidenceRefs, EvidenceRefs = _evidenceRefs,
Time = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow }, Time = new ClaimTimeInfo { IssuedAt = issuedAt ?? DateTimeOffset.UtcNow },
}; };
return _engine.IngestClaim(claim); return _engine.IngestClaim(claim);

View File

@@ -270,6 +270,7 @@ public sealed class CycloneDxVexNormalizer : IVexNormalizer
/// <param name="detail">Optional detail text.</param> /// <param name="detail">Optional detail text.</param>
/// <param name="principal">The principal making the assertion.</param> /// <param name="principal">The principal making the assertion.</param>
/// <param name="trustLabel">Optional trust label.</param> /// <param name="trustLabel">Optional trust label.</param>
/// <param name="issuedAt">Optional timestamp for deterministic testing.</param>
/// <returns>A normalized claim.</returns> /// <returns>A normalized claim.</returns>
public Claim NormalizeStatement( public Claim NormalizeStatement(
Subject subject, Subject subject,
@@ -277,7 +278,8 @@ public sealed class CycloneDxVexNormalizer : IVexNormalizer
CycloneDxJustification justification = CycloneDxJustification.None, CycloneDxJustification justification = CycloneDxJustification.None,
string? detail = null, string? detail = null,
Principal? principal = null, Principal? principal = null,
TrustLabel? trustLabel = null) TrustLabel? trustLabel = null,
DateTimeOffset? issuedAt = null)
{ {
var assertions = new List<AtomAssertion>(); var assertions = new List<AtomAssertion>();
@@ -312,7 +314,7 @@ public sealed class CycloneDxVexNormalizer : IVexNormalizer
Issuer = principal ?? Principal.Unknown, Issuer = principal ?? Principal.Unknown,
Assertions = assertions, Assertions = assertions,
TrustLabel = trustLabel, TrustLabel = trustLabel,
Time = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow }, Time = new ClaimTimeInfo { IssuedAt = issuedAt ?? DateTimeOffset.UtcNow },
}; };
} }
} }