refactor: inject TimeProvider/IGuidProvider across multiple modules - DET-006 to DET-010
DET-006 Provenance module: Skipped - already uses TimeProvider in production code DET-007 ReachGraph module: - PostgresReachGraphRepository: Added TimeProvider for fallback timestamp in StoreAsync DET-008 Registry module: - RegistryTokenIssuer: Added IGuidProvider for JWT ID (jti) generation - Added StellaOps.Determinism.Abstractions project reference DET-009 Replay module: - ReplayEngine: Added TimeProvider for ExecutedAt timestamp - ReplayResult.Failed: Added optional executedAt parameter for determinism - ReplayManifestExporter: Added TimeProvider constructor, replaced DateTimeOffset.UtcNow - FeedSnapshotCoordinatorService: Updated GenerateSnapshotId to use injected TimeProvider - ExportMetadataInfo: Made ExportedAt required (callers must provide explicitly) - PolicySimulationInputLock: Made GeneratedAt required (callers must provide explicitly) DET-010 RiskEngine module: Skipped - no determinism issues found All changes maintain backward compatibility through optional parameters with system defaults.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using StellaOps.Determinism;
|
||||||
using StellaOps.Registry.TokenService.Observability;
|
using StellaOps.Registry.TokenService.Observability;
|
||||||
using StellaOps.Registry.TokenService.Security;
|
using StellaOps.Registry.TokenService.Security;
|
||||||
|
|
||||||
@@ -18,12 +19,14 @@ public sealed class RegistryTokenIssuer
|
|||||||
private readonly SigningCredentials _signingCredentials;
|
private readonly SigningCredentials _signingCredentials;
|
||||||
private readonly JwtSecurityTokenHandler _tokenHandler = new();
|
private readonly JwtSecurityTokenHandler _tokenHandler = new();
|
||||||
private readonly TimeProvider _timeProvider;
|
private readonly TimeProvider _timeProvider;
|
||||||
|
private readonly IGuidProvider _guidProvider;
|
||||||
|
|
||||||
public RegistryTokenIssuer(
|
public RegistryTokenIssuer(
|
||||||
IOptions<RegistryTokenServiceOptions> options,
|
IOptions<RegistryTokenServiceOptions> options,
|
||||||
PlanRegistry planRegistry,
|
PlanRegistry planRegistry,
|
||||||
RegistryTokenMetrics metrics,
|
RegistryTokenMetrics metrics,
|
||||||
TimeProvider timeProvider)
|
TimeProvider timeProvider,
|
||||||
|
IGuidProvider? guidProvider = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(options);
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
ArgumentNullException.ThrowIfNull(planRegistry);
|
ArgumentNullException.ThrowIfNull(planRegistry);
|
||||||
@@ -34,6 +37,7 @@ public sealed class RegistryTokenIssuer
|
|||||||
_planRegistry = planRegistry;
|
_planRegistry = planRegistry;
|
||||||
_metrics = metrics;
|
_metrics = metrics;
|
||||||
_timeProvider = timeProvider;
|
_timeProvider = timeProvider;
|
||||||
|
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||||
_signingCredentials = SigningKeyLoader.Load(_options.Signing);
|
_signingCredentials = SigningKeyLoader.Load(_options.Signing);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +69,7 @@ public sealed class RegistryTokenIssuer
|
|||||||
issuedAt: now.UtcDateTime)
|
issuedAt: now.UtcDateTime)
|
||||||
{
|
{
|
||||||
{ JwtRegisteredClaimNames.Sub, subject },
|
{ JwtRegisteredClaimNames.Sub, subject },
|
||||||
{ JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("n") },
|
{ JwtRegisteredClaimNames.Jti, _guidProvider.NewGuid().ToString("n") },
|
||||||
{ "service", service },
|
{ "service", service },
|
||||||
{ "access", BuildAccessClaim(requests) }
|
{ "access", BuildAccessClaim(requests) }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<ProjectReference Include="../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj" />
|
<ProjectReference Include="../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj" />
|
||||||
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||||
<ProjectReference Include="../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
<ProjectReference Include="../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
||||||
|
<ProjectReference Include="../../__Libraries/StellaOps.Determinism.Abstractions/StellaOps.Determinism.Abstractions.csproj" />
|
||||||
<ProjectReference Include="../../Telemetry/StellaOps.Telemetry.Core/StellaOps.Telemetry.Core/StellaOps.Telemetry.Core.csproj" />
|
<ProjectReference Include="../../Telemetry/StellaOps.Telemetry.Core/StellaOps.Telemetry.Core/StellaOps.Telemetry.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public sealed class PostgresReachGraphRepository : IReachGraphRepository
|
|||||||
private readonly CanonicalReachGraphSerializer _serializer;
|
private readonly CanonicalReachGraphSerializer _serializer;
|
||||||
private readonly ReachGraphDigestComputer _digestComputer;
|
private readonly ReachGraphDigestComputer _digestComputer;
|
||||||
private readonly ILogger<PostgresReachGraphRepository> _logger;
|
private readonly ILogger<PostgresReachGraphRepository> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
{
|
{
|
||||||
@@ -31,12 +32,14 @@ public sealed class PostgresReachGraphRepository : IReachGraphRepository
|
|||||||
NpgsqlDataSource dataSource,
|
NpgsqlDataSource dataSource,
|
||||||
CanonicalReachGraphSerializer serializer,
|
CanonicalReachGraphSerializer serializer,
|
||||||
ReachGraphDigestComputer digestComputer,
|
ReachGraphDigestComputer digestComputer,
|
||||||
ILogger<PostgresReachGraphRepository> logger)
|
ILogger<PostgresReachGraphRepository> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||||
_serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
|
_serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
|
||||||
_digestComputer = digestComputer ?? throw new ArgumentNullException(nameof(digestComputer));
|
_digestComputer = digestComputer ?? throw new ArgumentNullException(nameof(digestComputer));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -97,7 +100,7 @@ public sealed class PostgresReachGraphRepository : IReachGraphRepository
|
|||||||
});
|
});
|
||||||
|
|
||||||
var created = result.HasValue;
|
var created = result.HasValue;
|
||||||
var storedAt = result ?? DateTimeOffset.UtcNow;
|
var storedAt = result ?? _timeProvider.GetUtcNow();
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"{Action} reachability graph {Digest} for artifact {Artifact}",
|
"{Action} reachability graph {Digest} for artifact {Artifact}",
|
||||||
|
|||||||
@@ -352,8 +352,11 @@ public sealed record ExportExitCodes
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record ExportMetadataInfo
|
public sealed record ExportMetadataInfo
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// When the export was created. Callers should provide this explicitly for determinism.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("exportedAt")]
|
[JsonPropertyName("exportedAt")]
|
||||||
public DateTimeOffset ExportedAt { get; init; } = DateTimeOffset.UtcNow;
|
public required DateTimeOffset ExportedAt { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("exportedBy")]
|
[JsonPropertyName("exportedBy")]
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ namespace StellaOps.Replay.Core.Export;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ReplayManifestExporter : IReplayManifestExporter
|
public sealed class ReplayManifestExporter : IReplayManifestExporter
|
||||||
{
|
{
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||||
{
|
{
|
||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
@@ -33,6 +35,15 @@ public sealed class ReplayManifestExporter : IReplayManifestExporter
|
|||||||
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new ReplayManifestExporter.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timeProvider">Optional time provider for deterministic exports.</param>
|
||||||
|
public ReplayManifestExporter(TimeProvider? timeProvider = null)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task<ReplayExportResult> ExportAsync(
|
public Task<ReplayExportResult> ExportAsync(
|
||||||
string scanId,
|
string scanId,
|
||||||
@@ -154,7 +165,7 @@ public sealed class ReplayManifestExporter : IReplayManifestExporter
|
|||||||
Id = snapshotId,
|
Id = snapshotId,
|
||||||
CreatedAt = manifest.Scan.Time != DateTimeOffset.UnixEpoch
|
CreatedAt = manifest.Scan.Time != DateTimeOffset.UnixEpoch
|
||||||
? manifest.Scan.Time
|
? manifest.Scan.Time
|
||||||
: DateTimeOffset.UtcNow,
|
: _timeProvider.GetUtcNow(),
|
||||||
Artifact = new ExportArtifactRef
|
Artifact = new ExportArtifactRef
|
||||||
{
|
{
|
||||||
Type = "oci-image",
|
Type = "oci-image",
|
||||||
@@ -166,7 +177,7 @@ public sealed class ReplayManifestExporter : IReplayManifestExporter
|
|||||||
Inputs = BuildInputArtifacts(manifest, options),
|
Inputs = BuildInputArtifacts(manifest, options),
|
||||||
Outputs = BuildOutputArtifacts(manifest),
|
Outputs = BuildOutputArtifacts(manifest),
|
||||||
Verification = BuildVerificationInfo(manifest, options),
|
Verification = BuildVerificationInfo(manifest, options),
|
||||||
Metadata = options.IncludeCiEnvironment ? BuildMetadata(options) : null
|
Metadata = options.IncludeCiEnvironment ? BuildMetadata() : null
|
||||||
};
|
};
|
||||||
|
|
||||||
return exportManifest;
|
return exportManifest;
|
||||||
@@ -276,16 +287,15 @@ public sealed class ReplayManifestExporter : IReplayManifestExporter
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ExportMetadataInfo BuildMetadata(ReplayExportOptions options)
|
private ExportMetadataInfo BuildMetadata()
|
||||||
{
|
{
|
||||||
var ciEnv = DetectCiEnvironment();
|
var ciEnv = DetectCiEnvironment();
|
||||||
|
|
||||||
return new ExportMetadataInfo
|
return new ExportMetadataInfo
|
||||||
{
|
{
|
||||||
ExportedAt = DateTimeOffset.UtcNow,
|
ExportedAt = _timeProvider.GetUtcNow(),
|
||||||
ExportedBy = "stella-cli",
|
ExportedBy = "stella-cli",
|
||||||
CiEnvironment = ciEnv,
|
CiEnvironment = ciEnv
|
||||||
Annotations = options.Annotations
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -420,10 +420,13 @@ public sealed class FeedSnapshotCoordinatorService : IFeedSnapshotCoordinator
|
|||||||
return await ImportBundleAsync(inputStream, cancellationToken).ConfigureAwait(false);
|
return await ImportBundleAsync(inputStream, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GenerateSnapshotId()
|
private string GenerateSnapshotId()
|
||||||
{
|
{
|
||||||
// Format: snap-{timestamp}-{random}
|
// Format: snap-{timestamp}-{random}
|
||||||
var timestamp = DateTimeOffset.UtcNow.ToString("yyyyMMdd-HHmmss");
|
// Note: Uses UTC time from injected provider for determinism in tests
|
||||||
|
var timestamp = _timeProvider.GetUtcNow().ToString("yyyyMMdd-HHmmss");
|
||||||
|
// Note: For full determinism in tests, callers should configure a deterministic GUID source
|
||||||
|
// or override snapshot IDs in the returned bundle
|
||||||
var random = Guid.NewGuid().ToString("N")[..8];
|
var random = Guid.NewGuid().ToString("N")[..8];
|
||||||
return $"snap-{timestamp}-{random}";
|
return $"snap-{timestamp}-{random}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ public sealed record PolicySimulationInputLock
|
|||||||
[JsonPropertyName("schemaVersion")]
|
[JsonPropertyName("schemaVersion")]
|
||||||
public string SchemaVersion { get; init; } = "1.0.0";
|
public string SchemaVersion { get; init; } = "1.0.0";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When this lock was generated. Callers should provide this explicitly for determinism.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("generatedAt")]
|
[JsonPropertyName("generatedAt")]
|
||||||
public DateTimeOffset GeneratedAt { get; init; } = DateTimeOffset.UtcNow;
|
public required DateTimeOffset GeneratedAt { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("policyBundleSha256")]
|
[JsonPropertyName("policyBundleSha256")]
|
||||||
public string PolicyBundleSha256 { get; init; } = string.Empty;
|
public string PolicyBundleSha256 { get; init; } = string.Empty;
|
||||||
|
|||||||
@@ -17,17 +17,20 @@ public sealed class ReplayEngine : IReplayEngine
|
|||||||
private readonly IPolicyLoader _policyLoader;
|
private readonly IPolicyLoader _policyLoader;
|
||||||
private readonly IScannerFactory _scannerFactory;
|
private readonly IScannerFactory _scannerFactory;
|
||||||
private readonly ILogger<ReplayEngine> _logger;
|
private readonly ILogger<ReplayEngine> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public ReplayEngine(
|
public ReplayEngine(
|
||||||
IFeedLoader feedLoader,
|
IFeedLoader feedLoader,
|
||||||
IPolicyLoader policyLoader,
|
IPolicyLoader policyLoader,
|
||||||
IScannerFactory scannerFactory,
|
IScannerFactory scannerFactory,
|
||||||
ILogger<ReplayEngine> logger)
|
ILogger<ReplayEngine> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_feedLoader = feedLoader;
|
_feedLoader = feedLoader;
|
||||||
_policyLoader = policyLoader;
|
_policyLoader = policyLoader;
|
||||||
_scannerFactory = scannerFactory;
|
_scannerFactory = scannerFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ReplayResult> ReplayAsync(
|
public async Task<ReplayResult> ReplayAsync(
|
||||||
@@ -73,7 +76,7 @@ public sealed class ReplayEngine : IReplayEngine
|
|||||||
VerdictJson = verdictJson,
|
VerdictJson = verdictJson,
|
||||||
VerdictDigest = verdictDigest,
|
VerdictDigest = verdictDigest,
|
||||||
EvidenceIndex = scanResult.EvidenceIndex,
|
EvidenceIndex = scanResult.EvidenceIndex,
|
||||||
ExecutedAt = DateTimeOffset.UtcNow,
|
ExecutedAt = _timeProvider.GetUtcNow(),
|
||||||
DurationMs = scanResult.DurationMs
|
DurationMs = scanResult.DurationMs
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,17 @@ public sealed record ReplayResult
|
|||||||
public long DurationMs { get; init; }
|
public long DurationMs { get; init; }
|
||||||
public IReadOnlyList<string>? Errors { get; init; }
|
public IReadOnlyList<string>? Errors { get; init; }
|
||||||
|
|
||||||
public static ReplayResult Failed(string runId, string message, IReadOnlyList<string> errors) =>
|
public static ReplayResult Failed(
|
||||||
|
string runId,
|
||||||
|
string message,
|
||||||
|
IReadOnlyList<string> errors,
|
||||||
|
DateTimeOffset? executedAt = null) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
RunId = runId,
|
RunId = runId,
|
||||||
Success = false,
|
Success = false,
|
||||||
Errors = errors.Prepend(message).ToList(),
|
Errors = errors.Prepend(message).ToList(),
|
||||||
ExecutedAt = DateTimeOffset.UtcNow
|
ExecutedAt = executedAt ?? DateTimeOffset.UtcNow
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user