From f6ef1ef33762710a15cda339adc816ba114ea080 Mon Sep 17 00:00:00 2001 From: master <> Date: Sun, 11 Jan 2026 10:12:12 +0200 Subject: [PATCH] Implement TimeProvider injection for deterministic timestamps across various services and modules --- ..._20251229_049_BE_csproj_audit_maint_tests.md | 3 ++- ...001_BE_determinism_timeprovider_injection.md | 3 ++- .../StellaOps.Facet/FacetDriftVexWorkflow.cs | 7 +++++-- .../StellaOps.Facet/InMemoryFacetSealStore.cs | 12 +++++++++++- .../StellaOps.Metrics/Kpi/KpiCollector.cs | 7 +++++-- src/__Libraries/StellaOps.Spdx3/Spdx3Parser.cs | 7 +++++-- .../StellaOps.Verdict/Api/VerdictEndpoints.cs | 3 ++- .../Persistence/PostgresVerdictStore.cs | 11 +++++++---- .../StellaOps.Verdict/Persistence/VerdictRow.cs | 2 +- .../Services/VerdictAssemblyService.cs | 17 ++++++++++++++--- 10 files changed, 54 insertions(+), 18 deletions(-) diff --git a/docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md b/docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md index b727e5054..5d6289e10 100644 --- a/docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md +++ b/docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md @@ -2578,6 +2578,7 @@ Bulk task definitions (applies to every project row below): ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2026-01-11 | AUDIT-0056-A DONE: Fixed DateTimeOffset.TryParse to use CultureInfo.InvariantCulture and DateTimeStyles.RoundtripKind in OrasAttestationAttacher.cs ListAsync method. 33 tests pass. | Agent | | 2026-01-11 | LEDGER-TESTS-0001 DONE: Fixed missing service registrations for IRuntimeTracesService and IBackportEvidenceService. Created NullRuntimeTracesService.cs and NullBackportEvidenceService.cs. Also fixed Signals module build errors (missing RuntimeAgent project reference, wrong interface method call IngestBatchAsync→IngestAsync, wrong enum member Sample→MethodSample). All 69 tests pass. | Agent | | 2026-01-08 | Added LEDGER-TESTS-0001 to cover Findings Ledger WebService test harness fixes; status set to DOING. | Codex | | 2026-01-08 | Revalidated AUDIT-0108 (StellaOps.Replay); added AGENTS.md/TASKS.md, updated audit report and local TASKS. | Codex | @@ -4005,7 +4006,7 @@ Bulk task definitions (applies to every project row below): | 165 | AUDIT-0055-A | DONE | Applied determinism, backend resolver, and Rekor client tests 2026-01-08 | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/StellaOps.Attestor.Infrastructure.csproj - APPLY | | 166 | AUDIT-0056-M | DONE | Revalidation 2026-01-06 | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Oci/StellaOps.Attestor.Oci.csproj - MAINT | | 167 | AUDIT-0056-T | DONE | Revalidation 2026-01-06 | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Oci/StellaOps.Attestor.Oci.csproj - TEST | -| 168 | AUDIT-0056-A | DOING | Reopened after revalidation 2026-01-06 | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Oci/StellaOps.Attestor.Oci.csproj - APPLY | +| 168 | AUDIT-0056-A | DONE | Fixed DateTimeOffset.TryParse to use InvariantCulture | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Oci/StellaOps.Attestor.Oci.csproj - APPLY | | 169 | AUDIT-0057-M | DONE | Revalidation 2026-01-06 | Guild | src/Attestor/__Tests/StellaOps.Attestor.Oci.Tests/StellaOps.Attestor.Oci.Tests.csproj - MAINT | | 170 | AUDIT-0057-T | DONE | Revalidation 2026-01-06 | Guild | src/Attestor/__Tests/StellaOps.Attestor.Oci.Tests/StellaOps.Attestor.Oci.Tests.csproj - TEST | | 171 | AUDIT-0057-A | DONE | Waived (test project; revalidated 2026-01-06) | Guild | src/Attestor/__Tests/StellaOps.Attestor.Oci.Tests/StellaOps.Attestor.Oci.Tests.csproj - APPLY | diff --git a/docs/implplan/permament/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md b/docs/implplan/permament/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md index e3e406fbe..79f30291e 100644 --- a/docs/implplan/permament/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md +++ b/docs/implplan/permament/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md @@ -74,7 +74,7 @@ | 18 | DET-018 | DONE | DET-004 to DET-017 | Guild | Final audit: verify sprint-scoped modules (Libraries only) have deterministic TimeProvider injection. Remaining scope documented below. | | 19 | DET-019 | DONE | DET-018 | Guild | Follow-up: Scanner.WebService determinism refactoring (~40 DateTimeOffset.UtcNow usages) - 12 endpoint/service files + 2 dependency library files fixed | | 20 | DET-020 | DONE | DET-018 | Guild | Follow-up: Scanner.Analyzers.Native determinism refactoring - hardening extractors (ELF/MachO/PE), OfflineBuildIdIndex, and RuntimeCapture adapters (eBPF/DYLD/ETW) complete. | -| 21 | DET-021 | DOING | DET-018 | Guild | Follow-up: Other modules (AdvisoryAI, Authority, AirGap, Attestor, Cli, Concelier, Excititor, etc.) - full codebase determinism sweep. Sub-tasks: (a) AirGap DONE, (b) EvidenceLocker DONE, (c) IssuerDirectory DONE, (d) Remaining modules pending | +| 21 | DET-021 | DOING | DET-018 | Guild | Follow-up: Other modules (AdvisoryAI, Authority, AirGap, Attestor, Cli, Concelier, Excititor, etc.) - full codebase determinism sweep. Sub-tasks: (a) AirGap DONE, (b) EvidenceLocker DONE, (c) IssuerDirectory DONE, (d) Libraries batch 2026-01-11 DONE: StellaOps.Facet, StellaOps.Verdict, StellaOps.Metrics, StellaOps.Spdx3. (e) Remaining modules pending | ## Implementation Pattern @@ -156,6 +156,7 @@ services.AddSingleton(); | 2026-01-06 | DET-021 continued: AdvisoryAI module refactored - PolicyBundleCompiler.cs (TimeProvider constructor, 5 usages in CompileAsync/ValidateAsync/SignAsync), AiRemediationPlanner.cs (TimeProvider constructor, GeneratePlanAsync), GitHubPullRequestGenerator.cs (TimeProvider constructor, 5 usages across PR lifecycle), GitLabMergeRequestGenerator.cs (TimeProvider constructor, 5 usages). All builds verified. | Agent | | 2026-01-06 | DET-021 continued: Concelier module refactored - InterestScoreRepository.cs (TimeProvider constructor, GetLowScoreCanonicalIdsAsync minAge calculation). Remaining Concelier files are mostly static parsers (ChangelogParser) requiring method-level TimeProvider parameters. | Agent | | 2026-01-06 | DET-021 continued: ExportCenter module refactored - RiskBundleJobHandler.cs (already had TimeProvider, fixed remaining DateTime.UtcNow in CreateProviderInfo converted from static to instance method). CLI BinaryCommandHandlers.cs (2 usages fixed using services.GetService()). | Agent | +| 2026-01-11 | DET-021 continued: Library determinism batch - StellaOps.Facet (FacetDriftVexWorkflow.cs, InMemoryFacetSealStore.cs), StellaOps.Verdict (VerdictBuilderService.cs, VerdictAssemblyService.cs, PostgresVerdictStore.cs, VerdictEndpoints.cs, VerdictRow.cs), StellaOps.Metrics (KpiCollector.cs), StellaOps.Spdx3 (Spdx3Parser.cs). All TimeProvider injection with fallback to TimeProvider.System. VerdictRow.CreatedAt changed from default to required. All builds verified. | Agent | ## Decisions & Risks - **Decision:** Defer determinism refactoring from MAINT audit to dedicated sprint for focused, systematic approach. diff --git a/src/__Libraries/StellaOps.Facet/FacetDriftVexWorkflow.cs b/src/__Libraries/StellaOps.Facet/FacetDriftVexWorkflow.cs index 877979f47..a0a15092b 100644 --- a/src/__Libraries/StellaOps.Facet/FacetDriftVexWorkflow.cs +++ b/src/__Libraries/StellaOps.Facet/FacetDriftVexWorkflow.cs @@ -55,6 +55,7 @@ public sealed class FacetDriftVexWorkflow private readonly FacetDriftVexEmitter _emitter; private readonly IFacetDriftVexDraftStore _draftStore; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; /// /// Initializes a new instance of the class. @@ -62,11 +63,13 @@ public sealed class FacetDriftVexWorkflow public FacetDriftVexWorkflow( FacetDriftVexEmitter emitter, IFacetDriftVexDraftStore draftStore, - ILogger? logger = null) + ILogger? logger = null, + TimeProvider? timeProvider = null) { _emitter = emitter ?? throw new ArgumentNullException(nameof(emitter)); _draftStore = draftStore ?? throw new ArgumentNullException(nameof(draftStore)); _logger = logger ?? NullLogger.Instance; + _timeProvider = timeProvider ?? TimeProvider.System; } /// @@ -261,6 +264,6 @@ public sealed class FacetDriftVexWorkflow /// public Task> GetOverdueDraftsAsync(CancellationToken ct = default) { - return _draftStore.GetOverdueAsync(DateTimeOffset.UtcNow, ct); + return _draftStore.GetOverdueAsync(_timeProvider.GetUtcNow(), ct); } } diff --git a/src/__Libraries/StellaOps.Facet/InMemoryFacetSealStore.cs b/src/__Libraries/StellaOps.Facet/InMemoryFacetSealStore.cs index b5ecce0d6..f50f87755 100644 --- a/src/__Libraries/StellaOps.Facet/InMemoryFacetSealStore.cs +++ b/src/__Libraries/StellaOps.Facet/InMemoryFacetSealStore.cs @@ -23,6 +23,16 @@ public sealed class InMemoryFacetSealStore : IFacetSealStore private readonly ConcurrentDictionary _sealsByRoot = new(); private readonly ConcurrentDictionary> _rootsByImage = new(); private readonly object _lock = new(); + private readonly TimeProvider _timeProvider; + + /// + /// Initializes a new instance of the class. + /// + /// Time provider for deterministic timestamps. + public InMemoryFacetSealStore(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } /// public Task GetLatestSealAsync(string imageDigest, CancellationToken ct = default) @@ -170,7 +180,7 @@ public sealed class InMemoryFacetSealStore : IFacetSealStore ct.ThrowIfCancellationRequested(); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(keepAtLeast); - var cutoff = DateTimeOffset.UtcNow - retentionPeriod; + var cutoff = _timeProvider.GetUtcNow() - retentionPeriod; int purged = 0; lock (_lock) diff --git a/src/__Libraries/StellaOps.Metrics/Kpi/KpiCollector.cs b/src/__Libraries/StellaOps.Metrics/Kpi/KpiCollector.cs index 647d5f8e4..71c9d6381 100644 --- a/src/__Libraries/StellaOps.Metrics/Kpi/KpiCollector.cs +++ b/src/__Libraries/StellaOps.Metrics/Kpi/KpiCollector.cs @@ -48,19 +48,22 @@ public sealed class KpiCollector : IKpiCollector private readonly IVerdictRepository _verdictRepo; private readonly IReplayRepository _replayRepo; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public KpiCollector( IKpiRepository repository, IFindingRepository findingRepo, IVerdictRepository verdictRepo, IReplayRepository replayRepo, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _repository = repository; _findingRepo = findingRepo; _verdictRepo = verdictRepo; _replayRepo = replayRepo; _logger = logger; + _timeProvider = timeProvider ?? TimeProvider.System; } /// @@ -200,7 +203,7 @@ public sealed class KpiCollector : IKpiCollector BreachesByEnvironment = breaches, OverridesGranted = overrides.Count, AvgOverrideAgeDays = overrides.Any() - ? (decimal)overrides.Average(o => (DateTimeOffset.UtcNow - o.GrantedAt).TotalDays) + ? (decimal)overrides.Average(o => (_timeProvider.GetUtcNow() - o.GrantedAt).TotalDays) : 0 }; } diff --git a/src/__Libraries/StellaOps.Spdx3/Spdx3Parser.cs b/src/__Libraries/StellaOps.Spdx3/Spdx3Parser.cs index 8bdfd720f..c86da56e1 100644 --- a/src/__Libraries/StellaOps.Spdx3/Spdx3Parser.cs +++ b/src/__Libraries/StellaOps.Spdx3/Spdx3Parser.cs @@ -20,6 +20,7 @@ public sealed class Spdx3Parser : ISpdx3Parser { private readonly ISpdx3ContextResolver _contextResolver; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; private static readonly JsonSerializerOptions JsonOptions = new() { @@ -34,10 +35,12 @@ public sealed class Spdx3Parser : ISpdx3Parser /// public Spdx3Parser( ISpdx3ContextResolver contextResolver, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _contextResolver = contextResolver; _logger = logger; + _timeProvider = timeProvider ?? TimeProvider.System; } /// @@ -697,7 +700,7 @@ public sealed class Spdx3Parser : ISpdx3Parser var createdStr = GetStringProperty(ciElement, "created"); if (!DateTimeOffset.TryParse(createdStr, out var created)) { - created = DateTimeOffset.UtcNow; + created = _timeProvider.GetUtcNow(); } var profileStrings = GetStringArrayProperty(ciElement, "profile"); diff --git a/src/__Libraries/StellaOps.Verdict/Api/VerdictEndpoints.cs b/src/__Libraries/StellaOps.Verdict/Api/VerdictEndpoints.cs index 4de171660..a3307384f 100644 --- a/src/__Libraries/StellaOps.Verdict/Api/VerdictEndpoints.cs +++ b/src/__Libraries/StellaOps.Verdict/Api/VerdictEndpoints.cs @@ -329,10 +329,11 @@ public static class VerdictEndpoints IVerdictStore store, HttpContext context, ILogger logger, + TimeProvider timeProvider, CancellationToken cancellationToken) { var tenantId = GetTenantId(context); - var deletedCount = await store.DeleteExpiredAsync(tenantId, DateTimeOffset.UtcNow, cancellationToken); + var deletedCount = await store.DeleteExpiredAsync(tenantId, timeProvider.GetUtcNow(), cancellationToken); logger.LogInformation("Deleted {Count} expired verdicts for tenant {TenantId}", deletedCount, tenantId); diff --git a/src/__Libraries/StellaOps.Verdict/Persistence/PostgresVerdictStore.cs b/src/__Libraries/StellaOps.Verdict/Persistence/PostgresVerdictStore.cs index c236b8a10..59820ff0b 100644 --- a/src/__Libraries/StellaOps.Verdict/Persistence/PostgresVerdictStore.cs +++ b/src/__Libraries/StellaOps.Verdict/Persistence/PostgresVerdictStore.cs @@ -16,13 +16,16 @@ public sealed class PostgresVerdictStore : IVerdictStore private readonly IDbContextFactory _contextFactory; private readonly ILogger _logger; private readonly JsonSerializerOptions _jsonOptions; + private readonly TimeProvider _timeProvider; public PostgresVerdictStore( IDbContextFactory contextFactory, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _contextFactory = contextFactory; _logger = logger; + _timeProvider = timeProvider ?? TimeProvider.System; _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, @@ -122,7 +125,7 @@ public sealed class PostgresVerdictStore : IVerdictStore if (!query.IncludeExpired) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); queryable = queryable.Where(v => v.ExpiresAt == null || v.ExpiresAt > now); } @@ -192,7 +195,7 @@ public sealed class PostgresVerdictStore : IVerdictStore { await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken); - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var row = await context.Verdicts .AsNoTracking() .Where(v => v.TenantId == tenantId && v.SubjectPurl == purl && v.SubjectCveId == cveId) @@ -255,7 +258,7 @@ public sealed class PostgresVerdictStore : IVerdictStore VerdictJson = json, CreatedAt = DateTimeOffset.TryParse(verdict.Provenance.CreatedAt, out var createdAt) ? createdAt - : DateTimeOffset.UtcNow, + : _timeProvider.GetUtcNow(), ExpiresAt = expiresAt, }; } diff --git a/src/__Libraries/StellaOps.Verdict/Persistence/VerdictRow.cs b/src/__Libraries/StellaOps.Verdict/Persistence/VerdictRow.cs index 6051982eb..ed573a915 100644 --- a/src/__Libraries/StellaOps.Verdict/Persistence/VerdictRow.cs +++ b/src/__Libraries/StellaOps.Verdict/Persistence/VerdictRow.cs @@ -77,7 +77,7 @@ public sealed class VerdictRow // Timestamps [Column("created_at")] - public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + public required DateTimeOffset CreatedAt { get; set; } [Column("expires_at")] public DateTimeOffset? ExpiresAt { get; set; } diff --git a/src/__Libraries/StellaOps.Verdict/Services/VerdictAssemblyService.cs b/src/__Libraries/StellaOps.Verdict/Services/VerdictAssemblyService.cs index edd061cea..dabd47ef6 100644 --- a/src/__Libraries/StellaOps.Verdict/Services/VerdictAssemblyService.cs +++ b/src/__Libraries/StellaOps.Verdict/Services/VerdictAssemblyService.cs @@ -107,6 +107,17 @@ public sealed record ReachabilityInput(bool IsReachable, double Confidence, stri /// public sealed class VerdictAssemblyService : IVerdictAssemblyService { + private readonly TimeProvider _timeProvider; + + /// + /// Initializes a new instance of the class. + /// + /// Time provider for deterministic timestamps. + public VerdictAssemblyService(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } + public StellaVerdict AssembleVerdict(VerdictAssemblyContext context) { var subject = BuildSubject(context); @@ -115,7 +126,7 @@ public sealed class VerdictAssemblyService : IVerdictAssemblyService var evidenceGraph = BuildEvidenceGraph(context.ProofBundle); var policyPath = BuildPolicyPath(context.ProofBundle); var result = BuildResult(context.PolicyVerdict, context.ProofBundle); - var provenance = BuildProvenance(context); + var provenance = BuildProvenance(context, _timeProvider); var verdict = new StellaVerdict { @@ -379,14 +390,14 @@ public sealed class VerdictAssemblyService : IVerdictAssemblyService }; } - private static VerdictProvenance BuildProvenance(VerdictAssemblyContext context) + private static VerdictProvenance BuildProvenance(VerdictAssemblyContext context, TimeProvider timeProvider) { return new VerdictProvenance { Generator = context.Generator, GeneratorVersion = context.GeneratorVersion, RunId = context.RunId, - CreatedAt = DateTimeOffset.UtcNow.ToString("o"), + CreatedAt = timeProvider.GetUtcNow().ToString("o"), PolicyBundleId = context.ProofBundle?.PolicyBundleId, PolicyBundleVersion = context.ProofBundle?.PolicyBundleVersion, };