feat(secrets): Implement secret leak policies and signal binding
- Added `spl-secret-block@1.json` to block deployments with critical or high severity secret findings. - Introduced `spl-secret-warn@1.json` to warn on secret findings without blocking deployments. - Created `SecretSignalBinder.cs` to bind secret evidence to policy evaluation signals. - Developed unit tests for `SecretEvidenceContext` and `SecretSignalBinder` to ensure correct functionality. - Enhanced `SecretSignalContextExtensions` to integrate secret evidence into signal contexts.
This commit is contained in:
@@ -7,9 +7,15 @@ namespace StellaOps.Scheduler.WebService.EventWebhooks;
|
||||
internal sealed class InMemoryWebhookRateLimiter : IWebhookRateLimiter, IDisposable
|
||||
{
|
||||
private readonly MemoryCache _cache = new(new MemoryCacheOptions());
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private readonly object _mutex = new();
|
||||
|
||||
public InMemoryWebhookRateLimiter(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public bool TryAcquire(string key, int limit, TimeSpan window, out TimeSpan retryAfter)
|
||||
{
|
||||
if (limit <= 0)
|
||||
@@ -19,7 +25,7 @@ internal sealed class InMemoryWebhookRateLimiter : IWebhookRateLimiter, IDisposa
|
||||
}
|
||||
|
||||
retryAfter = TimeSpan.Zero;
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
lock (_mutex)
|
||||
{
|
||||
|
||||
@@ -8,18 +8,18 @@ namespace StellaOps.Scheduler.WebService.GraphJobs;
|
||||
internal sealed class GraphJobService : IGraphJobService
|
||||
{
|
||||
private readonly IGraphJobStore _store;
|
||||
private readonly ISystemClock _clock;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGraphJobCompletionPublisher _completionPublisher;
|
||||
private readonly ICartographerWebhookClient _cartographerWebhook;
|
||||
|
||||
public GraphJobService(
|
||||
IGraphJobStore store,
|
||||
ISystemClock clock,
|
||||
TimeProvider timeProvider,
|
||||
IGraphJobCompletionPublisher completionPublisher,
|
||||
ICartographerWebhookClient cartographerWebhook)
|
||||
{
|
||||
_store = store ?? throw new ArgumentNullException(nameof(store));
|
||||
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_completionPublisher = completionPublisher ?? throw new ArgumentNullException(nameof(completionPublisher));
|
||||
_cartographerWebhook = cartographerWebhook ?? throw new ArgumentNullException(nameof(cartographerWebhook));
|
||||
}
|
||||
@@ -31,7 +31,7 @@ internal sealed class GraphJobService : IGraphJobService
|
||||
var trigger = request.Trigger ?? GraphBuildJobTrigger.SbomVersion;
|
||||
var metadata = request.Metadata ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
var now = _clock.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var id = GenerateIdentifier("gbj");
|
||||
var job = new GraphBuildJob(
|
||||
id,
|
||||
@@ -65,7 +65,7 @@ internal sealed class GraphJobService : IGraphJobService
|
||||
var metadata = request.Metadata ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
var trigger = request.Trigger ?? GraphOverlayJobTrigger.Policy;
|
||||
|
||||
var now = _clock.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var id = GenerateIdentifier("goj");
|
||||
|
||||
var job = new GraphOverlayJob(
|
||||
@@ -98,7 +98,7 @@ internal sealed class GraphJobService : IGraphJobService
|
||||
throw new ValidationException("Completion requires status completed, failed, or cancelled.");
|
||||
}
|
||||
|
||||
var occurredAt = request.OccurredAt == default ? _clock.UtcNow : request.OccurredAt.ToUniversalTime();
|
||||
var occurredAt = request.OccurredAt == default ? _timeProvider.GetUtcNow() : request.OccurredAt.ToUniversalTime();
|
||||
var graphSnapshotId = Normalize(request.GraphSnapshotId);
|
||||
var correlationId = Normalize(request.CorrelationId);
|
||||
var resultUri = Normalize(request.ResultUri);
|
||||
@@ -369,7 +369,7 @@ internal sealed class GraphJobService : IGraphJobService
|
||||
|
||||
public async Task<OverlayLagMetricsResponse> GetOverlayLagMetricsAsync(string tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var now = _clock.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var overlayJobs = await _store.GetOverlayJobsAsync(tenantId, cancellationToken);
|
||||
|
||||
var pending = overlayJobs.Count(job => job.Status == GraphJobStatus.Pending);
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
namespace StellaOps.Scheduler.WebService;
|
||||
|
||||
/// <summary>
|
||||
/// Legacy system clock interface. Prefer using TimeProvider instead.
|
||||
/// </summary>
|
||||
[Obsolete("Use TimeProvider instead. This interface is retained for backward compatibility.")]
|
||||
public interface ISystemClock
|
||||
{
|
||||
DateTimeOffset UtcNow { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legacy system clock implementation. Prefer using TimeProvider instead.
|
||||
/// </summary>
|
||||
[Obsolete("Use TimeProvider instead. This class is retained for backward compatibility.")]
|
||||
public sealed class SystemClock : ISystemClock
|
||||
{
|
||||
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public SystemClock(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public DateTimeOffset UtcNow => _timeProvider.GetUtcNow();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.PolicyRuns;
|
||||
@@ -10,6 +11,14 @@ internal sealed class InMemoryPolicyRunService : IPolicyRunService
|
||||
private readonly ConcurrentDictionary<string, PolicyRunStatus> _runs = new(StringComparer.Ordinal);
|
||||
private readonly List<PolicyRunStatus> _orderedRuns = new();
|
||||
private readonly object _gate = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public InMemoryPolicyRunService(TimeProvider? timeProvider = null, IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
public Task<PolicyRunStatus> EnqueueAsync(string tenantId, PolicyRunRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -17,11 +26,12 @@ internal sealed class InMemoryPolicyRunService : IPolicyRunService
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var runId = string.IsNullOrWhiteSpace(request.RunId)
|
||||
? GenerateRunId(request.PolicyId, request.QueuedAt ?? DateTimeOffset.UtcNow)
|
||||
? GenerateRunId(request.PolicyId, request.QueuedAt ?? now)
|
||||
: request.RunId;
|
||||
|
||||
var queuedAt = request.QueuedAt ?? DateTimeOffset.UtcNow;
|
||||
var queuedAt = request.QueuedAt ?? now;
|
||||
|
||||
var status = new PolicyRunStatus(
|
||||
runId,
|
||||
@@ -152,7 +162,7 @@ internal sealed class InMemoryPolicyRunService : IPolicyRunService
|
||||
}
|
||||
|
||||
var cancellationReason = NormalizeCancellationReason(reason);
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
updated = existing with
|
||||
{
|
||||
Status = PolicyRunExecutionStatus.Cancelled,
|
||||
@@ -206,17 +216,17 @@ internal sealed class InMemoryPolicyRunService : IPolicyRunService
|
||||
runId: null,
|
||||
policyVersion: existing.PolicyVersion,
|
||||
requestedBy: NormalizeActor(requestedBy),
|
||||
queuedAt: DateTimeOffset.UtcNow,
|
||||
queuedAt: _timeProvider.GetUtcNow(),
|
||||
correlationId: null,
|
||||
metadata: metadataBuilder.ToImmutable());
|
||||
|
||||
return await EnqueueAsync(tenantId, request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string GenerateRunId(string policyId, DateTimeOffset timestamp)
|
||||
private string GenerateRunId(string policyId, DateTimeOffset timestamp)
|
||||
{
|
||||
var normalizedPolicyId = string.IsNullOrWhiteSpace(policyId) ? "policy" : policyId.Trim();
|
||||
var suffix = Guid.NewGuid().ToString("N")[..8];
|
||||
var suffix = _guidProvider.NewGuid().ToString("N")[..8];
|
||||
return $"run:{normalizedPolicyId}:{timestamp:yyyyMMddTHHmmssZ}:{suffix}";
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ using StellaOps.Router.AspNet;
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddRouting(options => options.LowercaseUrls = true);
|
||||
builder.Services.AddSingleton<StellaOps.Scheduler.WebService.ISystemClock, StellaOps.Scheduler.WebService.SystemClock>();
|
||||
// TimeProvider.System is registered here for deterministic time support
|
||||
builder.Services.TryAddSingleton(TimeProvider.System);
|
||||
|
||||
var authorityOptions = new SchedulerAuthorityOptions();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Persistence.Postgres.Repositories;
|
||||
|
||||
@@ -13,14 +14,15 @@ internal static class SchedulerEndpointHelpers
|
||||
private const string ActorKindHeader = "X-Actor-Kind";
|
||||
private const string TenantHeader = "X-Tenant-Id";
|
||||
|
||||
public static string GenerateIdentifier(string prefix)
|
||||
public static string GenerateIdentifier(string prefix, IGuidProvider? guidProvider = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prefix))
|
||||
{
|
||||
throw new ArgumentException("Prefix must be provided.", nameof(prefix));
|
||||
}
|
||||
|
||||
return $"{prefix.Trim()}_{Guid.NewGuid():N}";
|
||||
var guid = (guidProvider ?? SystemGuidProvider.Instance).NewGuid();
|
||||
return $"{prefix.Trim()}_{guid:N}";
|
||||
}
|
||||
|
||||
public static string ResolveActorId(HttpContext context)
|
||||
|
||||
@@ -124,11 +124,22 @@ internal sealed class InMemoryRunSummaryService : IRunSummaryService
|
||||
|
||||
internal sealed class InMemorySchedulerAuditService : ISchedulerAuditService
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly StellaOps.Determinism.IGuidProvider _guidProvider;
|
||||
|
||||
public InMemorySchedulerAuditService(
|
||||
TimeProvider? timeProvider = null,
|
||||
StellaOps.Determinism.IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? StellaOps.Determinism.SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
public Task<AuditRecord> WriteAsync(SchedulerAuditEvent auditEvent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var occurredAt = auditEvent.OccurredAt ?? DateTimeOffset.UtcNow;
|
||||
var occurredAt = auditEvent.OccurredAt ?? _timeProvider.GetUtcNow();
|
||||
var record = new AuditRecord(
|
||||
auditEvent.AuditId ?? $"audit_{Guid.NewGuid():N}",
|
||||
auditEvent.AuditId ?? $"audit_{_guidProvider.NewGuid():N}",
|
||||
auditEvent.TenantId,
|
||||
auditEvent.Category,
|
||||
auditEvent.Action,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scheduler.Queue/StellaOps.Scheduler.Queue.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scheduler.Persistence/StellaOps.Scheduler.Persistence.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Determinism.Abstractions/StellaOps.Determinism.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Persistence.Postgres.Models;
|
||||
|
||||
@@ -10,12 +11,21 @@ namespace StellaOps.Scheduler.Persistence.Postgres.Repositories;
|
||||
/// </summary>
|
||||
public sealed class FailureSignatureRepository : RepositoryBase<SchedulerDataSource>, IFailureSignatureRepository
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new failure signature repository.
|
||||
/// </summary>
|
||||
public FailureSignatureRepository(SchedulerDataSource dataSource, ILogger<FailureSignatureRepository> logger)
|
||||
public FailureSignatureRepository(
|
||||
SchedulerDataSource dataSource,
|
||||
ILogger<FailureSignatureRepository> logger,
|
||||
TimeProvider? timeProvider = null,
|
||||
IGuidProvider? guidProvider = null)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -332,7 +342,7 @@ public sealed class FailureSignatureRepository : RepositoryBase<SchedulerDataSou
|
||||
AND resolved_at < @cutoff
|
||||
""";
|
||||
|
||||
var cutoff = DateTimeOffset.UtcNow.Subtract(olderThan);
|
||||
var cutoff = _timeProvider.GetUtcNow().Subtract(olderThan);
|
||||
|
||||
await using var connection = await DataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
@@ -389,7 +399,7 @@ public sealed class FailureSignatureRepository : RepositoryBase<SchedulerDataSou
|
||||
|
||||
private void AddSignatureParameters(NpgsqlCommand command, FailureSignatureEntity signature)
|
||||
{
|
||||
AddParameter(command, "signature_id", signature.SignatureId == Guid.Empty ? Guid.NewGuid() : signature.SignatureId);
|
||||
AddParameter(command, "signature_id", signature.SignatureId == Guid.Empty ? _guidProvider.NewGuid() : signature.SignatureId);
|
||||
AddParameter(command, "tenant_id", signature.TenantId);
|
||||
AddParameter(command, "scope_type", signature.ScopeType.ToString().ToLowerInvariant());
|
||||
AddParameter(command, "scope_id", signature.ScopeId);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using Dapper;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Connections;
|
||||
|
||||
@@ -8,11 +9,13 @@ namespace StellaOps.Scheduler.Persistence.Postgres.Repositories;
|
||||
public sealed class ImpactSnapshotRepository : IImpactSnapshotRepository
|
||||
{
|
||||
private readonly SchedulerDataSource _dataSource;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly JsonSerializerOptions _serializer = CanonicalJsonSerializer.Settings;
|
||||
|
||||
public ImpactSnapshotRepository(SchedulerDataSource dataSource)
|
||||
public ImpactSnapshotRepository(SchedulerDataSource dataSource, IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
public async Task UpsertAsync(ImpactSet snapshot, CancellationToken cancellationToken = default)
|
||||
@@ -29,7 +32,7 @@ ON CONFLICT (snapshot_id) DO UPDATE SET impact = EXCLUDED.impact;
|
||||
|
||||
await conn.ExecuteAsync(sql, new
|
||||
{
|
||||
SnapshotId = snapshot.SnapshotId ?? $"impact::{Guid.NewGuid():N}",
|
||||
SnapshotId = snapshot.SnapshotId ?? $"impact::{_guidProvider.NewGuid():N}",
|
||||
TenantId = tenantId,
|
||||
Impact = JsonSerializer.Serialize(snapshot, _serializer)
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Persistence.Postgres.Models;
|
||||
|
||||
@@ -10,12 +11,21 @@ namespace StellaOps.Scheduler.Persistence.Postgres.Repositories;
|
||||
/// </summary>
|
||||
public sealed class JobRepository : RepositoryBase<SchedulerDataSource>, IJobRepository
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new job repository.
|
||||
/// </summary>
|
||||
public JobRepository(SchedulerDataSource dataSource, ILogger<JobRepository> logger)
|
||||
public JobRepository(
|
||||
SchedulerDataSource dataSource,
|
||||
ILogger<JobRepository> logger,
|
||||
TimeProvider? timeProvider = null,
|
||||
IGuidProvider? guidProvider = null)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -123,8 +133,8 @@ public sealed class JobRepository : RepositoryBase<SchedulerDataSource>, IJobRep
|
||||
TimeSpan leaseDuration,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var leaseId = Guid.NewGuid();
|
||||
var leaseUntil = DateTimeOffset.UtcNow.Add(leaseDuration);
|
||||
var leaseId = _guidProvider.NewGuid();
|
||||
var leaseUntil = _timeProvider.GetUtcNow().Add(leaseDuration);
|
||||
|
||||
const string sql = """
|
||||
UPDATE scheduler.jobs
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Persistence.Postgres.Models;
|
||||
|
||||
@@ -10,12 +11,18 @@ namespace StellaOps.Scheduler.Persistence.Postgres.Repositories;
|
||||
/// </summary>
|
||||
public sealed class TriggerRepository : RepositoryBase<SchedulerDataSource>, ITriggerRepository
|
||||
{
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new trigger repository.
|
||||
/// </summary>
|
||||
public TriggerRepository(SchedulerDataSource dataSource, ILogger<TriggerRepository> logger)
|
||||
public TriggerRepository(
|
||||
SchedulerDataSource dataSource,
|
||||
ILogger<TriggerRepository> logger,
|
||||
IGuidProvider? guidProvider = null)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -125,7 +132,7 @@ public sealed class TriggerRepository : RepositoryBase<SchedulerDataSource>, ITr
|
||||
RETURNING *
|
||||
""";
|
||||
|
||||
var id = trigger.Id == Guid.Empty ? Guid.NewGuid() : trigger.Id;
|
||||
var id = trigger.Id == Guid.Empty ? _guidProvider.NewGuid() : trigger.Id;
|
||||
await using var connection = await DataSource.OpenConnectionAsync(trigger.TenantId, "writer", cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Scheduler.Models\StellaOps.Scheduler.Models.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.EfCore\StellaOps.Infrastructure.EfCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -227,15 +227,18 @@ public sealed class BundleRotationJob : IBundleRotationScheduler
|
||||
private readonly IAttestorBundleClient _bundleClient;
|
||||
private readonly BundleRotationOptions _options;
|
||||
private readonly ILogger<BundleRotationJob> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public BundleRotationJob(
|
||||
IAttestorBundleClient bundleClient,
|
||||
IOptions<BundleRotationOptions> options,
|
||||
ILogger<BundleRotationJob> logger)
|
||||
ILogger<BundleRotationJob> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_bundleClient = bundleClient ?? throw new ArgumentNullException(nameof(bundleClient));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -275,7 +278,7 @@ public sealed class BundleRotationJob : IBundleRotationScheduler
|
||||
string triggeredBy,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var startedAt = DateTimeOffset.UtcNow;
|
||||
var startedAt = _timeProvider.GetUtcNow();
|
||||
var results = new List<BundleRotationResult>();
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
@@ -330,7 +333,7 @@ public sealed class BundleRotationJob : IBundleRotationScheduler
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
var completedAt = DateTimeOffset.UtcNow;
|
||||
var completedAt = _timeProvider.GetUtcNow();
|
||||
|
||||
var summary = new BundleRotationSummary(
|
||||
StartedAt: startedAt,
|
||||
@@ -417,7 +420,7 @@ public sealed class BundleRotationJob : IBundleRotationScheduler
|
||||
/// <inheritdoc/>
|
||||
public async Task<int> ApplyRetentionPolicyAsync(CancellationToken ct = default)
|
||||
{
|
||||
var cutoffDate = DateTimeOffset.UtcNow.AddMonths(-_options.RetentionMonths);
|
||||
var cutoffDate = _timeProvider.GetUtcNow().AddMonths(-_options.RetentionMonths);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Applying retention policy. Deleting bundles created before {Cutoff}",
|
||||
@@ -456,7 +459,7 @@ public sealed class BundleRotationJob : IBundleRotationScheduler
|
||||
|
||||
private (DateTimeOffset start, DateTimeOffset end) GetCurrentBundlePeriod()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
return _options.Cadence switch
|
||||
{
|
||||
|
||||
@@ -472,10 +472,17 @@ public sealed class InMemoryEvidenceBundleJobQueue : IEvidenceBundleJobQueue
|
||||
public sealed class InMemoryEvidenceBundleStore : IEvidenceBundleStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, StoredBundle> _bundles = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryEvidenceBundleStore(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public ValueTask StoreBundleAsync(string tenantId, string idempotencyKey, GeneratedBundle bundle, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = $"{tenantId}:{idempotencyKey}";
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var stored = new StoredBundle(
|
||||
bundle.BundleId,
|
||||
tenantId,
|
||||
@@ -483,8 +490,8 @@ public sealed class InMemoryEvidenceBundleStore : IEvidenceBundleStore
|
||||
bundle.StorageUri,
|
||||
bundle.SizeBytes,
|
||||
BundleStatus.Completed,
|
||||
DateTimeOffset.UtcNow,
|
||||
DateTimeOffset.UtcNow.AddDays(7));
|
||||
now,
|
||||
now.AddDays(7));
|
||||
|
||||
_bundles[key] = stored;
|
||||
return ValueTask.CompletedTask;
|
||||
@@ -498,7 +505,7 @@ public sealed class InMemoryEvidenceBundleStore : IEvidenceBundleStore
|
||||
|
||||
public ValueTask<int> CleanupExpiredAsync(TimeSpan maxAge, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var cutoff = DateTimeOffset.UtcNow - maxAge;
|
||||
var cutoff = _timeProvider.GetUtcNow() - maxAge;
|
||||
var toRemove = _bundles
|
||||
.Where(kvp => kvp.Value.CreatedAt < cutoff)
|
||||
.Select(kvp => kvp.Key)
|
||||
@@ -551,8 +558,15 @@ public sealed class InMemoryJobManifestProvider : IJobManifestProvider
|
||||
/// </summary>
|
||||
public sealed class NullEvidenceBundleGenerator : IEvidenceBundleGenerator
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public static NullEvidenceBundleGenerator Instance { get; } = new();
|
||||
|
||||
public NullEvidenceBundleGenerator(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public ValueTask<GeneratedBundle> GenerateAsync(EvidenceBundleJob job, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ValueTask.FromResult(new GeneratedBundle(
|
||||
@@ -563,6 +577,6 @@ public sealed class NullEvidenceBundleGenerator : IEvidenceBundleGenerator
|
||||
ChecksumAlgorithm: "SHA256",
|
||||
BundleType: job.BundleType,
|
||||
ArtifactCount: job.ArtifactIds.Length,
|
||||
GeneratedAt: DateTimeOffset.UtcNow));
|
||||
GeneratedAt: _timeProvider.GetUtcNow()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,15 +345,17 @@ public sealed class InMemoryProgressEventDeduplicator : IProgressEventDeduplicat
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, DateTimeOffset> _processed = new();
|
||||
private readonly TimeSpan _retentionPeriod;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryProgressEventDeduplicator(TimeSpan? retentionPeriod = null)
|
||||
public InMemoryProgressEventDeduplicator(TimeSpan? retentionPeriod = null, TimeProvider? timeProvider = null)
|
||||
{
|
||||
_retentionPeriod = retentionPeriod ?? TimeSpan.FromMinutes(30);
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public ValueTask<bool> TryMarkAsProcessedAsync(string eventId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
// Clean up old entries periodically
|
||||
if (_processed.Count > 10000)
|
||||
|
||||
@@ -37,6 +37,7 @@ internal sealed class SchedulerEventPublisher : ISchedulerEventPublisher
|
||||
private readonly INotifyEventQueue _queue;
|
||||
private readonly NotifyEventQueueOptions _queueOptions;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly StellaOps.Determinism.IGuidProvider _guidProvider;
|
||||
private readonly ILogger<SchedulerEventPublisher> _logger;
|
||||
private readonly string _stream;
|
||||
|
||||
@@ -44,11 +45,13 @@ internal sealed class SchedulerEventPublisher : ISchedulerEventPublisher
|
||||
INotifyEventQueue queue,
|
||||
NotifyEventQueueOptions queueOptions,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<SchedulerEventPublisher> logger)
|
||||
ILogger<SchedulerEventPublisher> logger,
|
||||
StellaOps.Determinism.IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_queue = queue ?? throw new ArgumentNullException(nameof(queue));
|
||||
_queueOptions = queueOptions ?? throw new ArgumentNullException(nameof(queueOptions));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? StellaOps.Determinism.SystemGuidProvider.Instance;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_stream = ResolveStream(queueOptions);
|
||||
}
|
||||
@@ -76,7 +79,7 @@ internal sealed class SchedulerEventPublisher : ISchedulerEventPublisher
|
||||
var attributes = BuildReportAttributes(run, message, result, impactImage);
|
||||
|
||||
var notifyEvent = NotifyEvent.Create(
|
||||
eventId: Guid.NewGuid(),
|
||||
eventId: _guidProvider.NewGuid(),
|
||||
kind: NotifyEventKinds.ScannerReportReady,
|
||||
tenant: run.TenantId,
|
||||
ts: occurredAt,
|
||||
@@ -110,7 +113,7 @@ internal sealed class SchedulerEventPublisher : ISchedulerEventPublisher
|
||||
var attributes = BuildRescanAttributes(run, message, deltas, impactLookup);
|
||||
|
||||
var notifyEvent = NotifyEvent.Create(
|
||||
eventId: Guid.NewGuid(),
|
||||
eventId: _guidProvider.NewGuid(),
|
||||
kind: NotifyEventKinds.SchedulerRescanDelta,
|
||||
tenant: run.TenantId,
|
||||
ts: now,
|
||||
|
||||
@@ -276,8 +276,17 @@ public sealed record ExpiringDigestEntry(
|
||||
/// </summary>
|
||||
public sealed class NullExpiringDigestService : IExpiringDigestService
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly StellaOps.Determinism.IGuidProvider _guidProvider;
|
||||
|
||||
public static NullExpiringDigestService Instance { get; } = new();
|
||||
|
||||
public NullExpiringDigestService(TimeProvider? timeProvider = null, StellaOps.Determinism.IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? StellaOps.Determinism.SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
public ValueTask<ExpiringDigest> GenerateDigestAsync(
|
||||
string tenantId,
|
||||
IReadOnlyList<ExceptionRecord> expiringExceptions,
|
||||
@@ -285,9 +294,9 @@ public sealed class NullExpiringDigestService : IExpiringDigestService
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var digest = new ExpiringDigest(
|
||||
DigestId: Guid.NewGuid().ToString("N"),
|
||||
DigestId: _guidProvider.NewGuid().ToString("N"),
|
||||
TenantId: tenantId,
|
||||
GeneratedAt: DateTimeOffset.UtcNow,
|
||||
GeneratedAt: _timeProvider.GetUtcNow(),
|
||||
WindowEnd: windowEnd,
|
||||
TotalCount: expiringExceptions.Count,
|
||||
CriticalCount: 0,
|
||||
|
||||
@@ -21,15 +21,18 @@ internal sealed class HttpScannerReportClient : IScannerReportClient
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IOptions<SchedulerWorkerOptions> _options;
|
||||
private readonly ILogger<HttpScannerReportClient> _logger;
|
||||
private readonly StellaOps.Determinism.IGuidProvider _guidProvider;
|
||||
|
||||
public HttpScannerReportClient(
|
||||
HttpClient httpClient,
|
||||
IOptions<SchedulerWorkerOptions> options,
|
||||
ILogger<HttpScannerReportClient> logger)
|
||||
ILogger<HttpScannerReportClient> logger,
|
||||
StellaOps.Determinism.IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_guidProvider = guidProvider ?? StellaOps.Determinism.SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
public async Task<RunnerImageResult> ExecuteAsync(
|
||||
@@ -151,13 +154,13 @@ internal sealed class HttpScannerReportClient : IScannerReportClient
|
||||
private static bool IsTransient(Exception exception)
|
||||
=> exception is HttpRequestException or TaskCanceledException;
|
||||
|
||||
private static RunnerReportSnapshot BuildReportSnapshot(ReportResponse report, string fallbackDigest)
|
||||
private RunnerReportSnapshot BuildReportSnapshot(ReportResponse report, string fallbackDigest)
|
||||
{
|
||||
var document = report.Report ?? new ReportDocument();
|
||||
var summary = document.Summary ?? new ReportSummary();
|
||||
|
||||
return new RunnerReportSnapshot(
|
||||
string.IsNullOrWhiteSpace(document.ReportId) ? Guid.NewGuid().ToString("N") : document.ReportId,
|
||||
string.IsNullOrWhiteSpace(document.ReportId) ? _guidProvider.NewGuid().ToString("N") : document.ReportId,
|
||||
string.IsNullOrWhiteSpace(document.ImageDigest) ? fallbackDigest : document.ImageDigest,
|
||||
string.IsNullOrWhiteSpace(document.Verdict) ? "warn" : document.Verdict,
|
||||
document.GeneratedAt,
|
||||
|
||||
@@ -48,14 +48,17 @@ public sealed class PartitionHealthMonitor
|
||||
/// <param name="connection">PostgreSQL connection.</param>
|
||||
/// <param name="alertThreshold">Days threshold for warning alert.</param>
|
||||
/// <param name="criticalThreshold">Days threshold for critical alert.</param>
|
||||
/// <param name="timeProvider">Optional time provider for deterministic testing.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>List of partition health status for each table.</returns>
|
||||
public async Task<List<PartitionHealthStatus>> CheckHealthAsync(
|
||||
NpgsqlConnection connection,
|
||||
int alertThreshold = 30,
|
||||
int criticalThreshold = 7,
|
||||
TimeProvider? timeProvider = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var time = timeProvider ?? TimeProvider.System;
|
||||
using var activity = ActivitySource.StartActivity("partitions.health_check", ActivityKind.Internal);
|
||||
|
||||
var results = new List<PartitionHealthStatus>();
|
||||
@@ -82,6 +85,7 @@ public sealed class PartitionHealthMonitor
|
||||
{
|
||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
||||
|
||||
var now = time.GetUtcNow();
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
var schema = reader.GetString(0);
|
||||
@@ -91,7 +95,7 @@ public sealed class PartitionHealthMonitor
|
||||
|
||||
var tableKey = $"{schema}.{table}";
|
||||
var daysUntilExhaustion = lastPartitionStart.HasValue
|
||||
? Math.Max(0, (int)(lastPartitionStart.Value - DateTimeOffset.UtcNow).TotalDays)
|
||||
? Math.Max(0, (int)(lastPartitionStart.Value - now).TotalDays)
|
||||
: 0;
|
||||
|
||||
futureCounts[tableKey] = futureCount;
|
||||
|
||||
@@ -134,17 +134,20 @@ public sealed class ScoreReplaySchedulerJob : IScoreReplayScheduler
|
||||
{
|
||||
private readonly IScannerReplayClient _scannerClient;
|
||||
private readonly ScoreReplaySchedulerOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<ScoreReplaySchedulerJob> _logger;
|
||||
private string? _lastFeedSnapshotHash;
|
||||
|
||||
public ScoreReplaySchedulerJob(
|
||||
IScannerReplayClient scannerClient,
|
||||
IOptions<ScoreReplaySchedulerOptions> options,
|
||||
ILogger<ScoreReplaySchedulerJob> logger)
|
||||
ILogger<ScoreReplaySchedulerJob> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_scannerClient = scannerClient ?? throw new ArgumentNullException(nameof(scannerClient));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -191,7 +194,7 @@ public sealed class ScoreReplaySchedulerJob : IScoreReplayScheduler
|
||||
string? feedSnapshotHash = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var startedAt = DateTimeOffset.UtcNow;
|
||||
var startedAt = _timeProvider.GetUtcNow();
|
||||
var results = new List<ScoreReplayResult>();
|
||||
var successCount = 0;
|
||||
var failureCount = 0;
|
||||
@@ -252,7 +255,7 @@ public sealed class ScoreReplaySchedulerJob : IScoreReplayScheduler
|
||||
_logger.LogError(ex, "Error during batch score replay");
|
||||
}
|
||||
|
||||
var completedAt = DateTimeOffset.UtcNow;
|
||||
var completedAt = _timeProvider.GetUtcNow();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Score replay batch completed. Success={Success}, Failed={Failed}, SignificantDeltas={Deltas}, Duration={Duration}ms",
|
||||
|
||||
@@ -200,6 +200,7 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler
|
||||
private readonly IPolicyGatewayClient _gatewayClient;
|
||||
private readonly GateEvaluationOptions _options;
|
||||
private readonly ILogger<GateEvaluationJob> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
// In-memory queue for pending jobs (replace with persistent store in production)
|
||||
private readonly Queue<GateEvaluationRequest> _pendingJobs = new();
|
||||
@@ -209,11 +210,13 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler
|
||||
public GateEvaluationJob(
|
||||
IPolicyGatewayClient gatewayClient,
|
||||
IOptions<GateEvaluationOptions> options,
|
||||
ILogger<GateEvaluationJob> logger)
|
||||
ILogger<GateEvaluationJob> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_gatewayClient = gatewayClient ?? throw new ArgumentNullException(nameof(gatewayClient));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -233,7 +236,7 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler
|
||||
DeltaCount: null,
|
||||
CriticalCount: null,
|
||||
HighCount: null,
|
||||
StartedAt: DateTimeOffset.UtcNow,
|
||||
StartedAt: _timeProvider.GetUtcNow(),
|
||||
CompletedAt: default,
|
||||
Duration: TimeSpan.Zero);
|
||||
}
|
||||
@@ -262,13 +265,13 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler
|
||||
{
|
||||
_logger.LogDebug("Gate evaluation jobs are disabled");
|
||||
return new GateEvaluationBatchSummary(
|
||||
DateTimeOffset.UtcNow,
|
||||
DateTimeOffset.UtcNow,
|
||||
_timeProvider.GetUtcNow(),
|
||||
_timeProvider.GetUtcNow(),
|
||||
0, 0, 0, 0, 0,
|
||||
TimeSpan.Zero);
|
||||
}
|
||||
|
||||
var startedAt = DateTimeOffset.UtcNow;
|
||||
var startedAt = _timeProvider.GetUtcNow();
|
||||
var sw = Stopwatch.StartNew();
|
||||
var results = new List<GateEvaluationResult>();
|
||||
|
||||
@@ -292,7 +295,7 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler
|
||||
results.AddRange(completedResults);
|
||||
|
||||
sw.Stop();
|
||||
var completedAt = DateTimeOffset.UtcNow;
|
||||
var completedAt = _timeProvider.GetUtcNow();
|
||||
|
||||
var summary = new GateEvaluationBatchSummary(
|
||||
StartedAt: startedAt,
|
||||
@@ -320,7 +323,7 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler
|
||||
GateEvaluationRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var startedAt = DateTimeOffset.UtcNow;
|
||||
var startedAt = _timeProvider.GetUtcNow();
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
// Update status to in-progress
|
||||
@@ -347,7 +350,7 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler
|
||||
timeoutCts.Token);
|
||||
|
||||
sw.Stop();
|
||||
var completedAt = DateTimeOffset.UtcNow;
|
||||
var completedAt = _timeProvider.GetUtcNow();
|
||||
|
||||
var result = new GateEvaluationResult(
|
||||
JobId: request.JobId,
|
||||
@@ -453,7 +456,7 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler
|
||||
CriticalCount: null,
|
||||
HighCount: null,
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: DateTimeOffset.UtcNow,
|
||||
CompletedAt: _timeProvider.GetUtcNow(),
|
||||
Duration: duration,
|
||||
ErrorMessage: errorMessage);
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ public sealed class ReachabilityJoinerWorker : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ReachabilityFact> JoinSnapshotWithSignals(
|
||||
private IReadOnlyList<ReachabilityFact> JoinSnapshotWithSignals(
|
||||
SbomSnapshot snapshot,
|
||||
IReadOnlyDictionary<string, ComponentSignal> signals)
|
||||
{
|
||||
@@ -207,7 +207,7 @@ public sealed class ReachabilityJoinerWorker : BackgroundService
|
||||
IsReachable: signal.IsReachable,
|
||||
Confidence: signal.Confidence,
|
||||
Evidence: signal.Evidence,
|
||||
ProducedAt: DateTimeOffset.UtcNow);
|
||||
ProducedAt: _timeProvider.GetUtcNow());
|
||||
|
||||
facts.Add(fact);
|
||||
}
|
||||
@@ -384,6 +384,12 @@ public sealed class InMemoryReachabilityFactCache : IReachabilityFactCache
|
||||
{
|
||||
private readonly Dictionary<string, (IReadOnlyList<ReachabilityFact> Facts, DateTimeOffset ExpiresAt)> _cache = new();
|
||||
private readonly object _lock = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryReachabilityFactCache(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public ValueTask WriteFactsAsync(
|
||||
string tenantId,
|
||||
@@ -396,7 +402,7 @@ public sealed class InMemoryReachabilityFactCache : IReachabilityFactCache
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_cache[key] = (facts, DateTimeOffset.UtcNow.Add(ttl));
|
||||
_cache[key] = (facts, _timeProvider.GetUtcNow().Add(ttl));
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
@@ -411,7 +417,7 @@ public sealed class InMemoryReachabilityFactCache : IReachabilityFactCache
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_cache.TryGetValue(key, out var entry) && entry.ExpiresAt > DateTimeOffset.UtcNow)
|
||||
if (_cache.TryGetValue(key, out var entry) && entry.ExpiresAt > _timeProvider.GetUtcNow())
|
||||
{
|
||||
return ValueTask.FromResult(entry.Facts);
|
||||
}
|
||||
|
||||
@@ -291,6 +291,12 @@ public sealed class InMemoryReachabilityFactStore : IReachabilityFactStore
|
||||
{
|
||||
private readonly Dictionary<string, List<StoredFact>> _facts = new();
|
||||
private readonly object _lock = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryReachabilityFactStore(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public ValueTask<IReadOnlyList<string>> GetTenantsWithFactsAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -343,7 +349,7 @@ public sealed class InMemoryReachabilityFactStore : IReachabilityFactStore
|
||||
int maxCount,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
|
||||
@@ -422,14 +422,24 @@ public sealed class InMemoryFindingsLedgerProjector : IFindingsLedgerProjector
|
||||
/// </summary>
|
||||
public sealed class NullPolicyEngineEvaluator : IPolicyEngineEvaluator
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly StellaOps.Determinism.IGuidProvider _guidProvider;
|
||||
|
||||
public static NullPolicyEngineEvaluator Instance { get; } = new();
|
||||
|
||||
public NullPolicyEngineEvaluator(TimeProvider? timeProvider = null, StellaOps.Determinism.IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? StellaOps.Determinism.SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
public ValueTask<BatchEvaluationResult> EvaluateBatchAsync(
|
||||
string tenantId,
|
||||
string artifactId,
|
||||
IReadOnlyList<CandidateFinding> candidates,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var evaluatedFindings = candidates
|
||||
.Select(c => new EvaluatedFinding(
|
||||
c.FindingId,
|
||||
@@ -440,13 +450,13 @@ public sealed class NullPolicyEngineEvaluator : IPolicyEngineEvaluator
|
||||
"default-policy",
|
||||
null,
|
||||
null,
|
||||
DateTimeOffset.UtcNow))
|
||||
now))
|
||||
.ToImmutableArray();
|
||||
|
||||
return ValueTask.FromResult(new BatchEvaluationResult(
|
||||
BatchId: Guid.NewGuid().ToString("N"),
|
||||
BatchId: _guidProvider.NewGuid().ToString("N"),
|
||||
EvaluatedFindings: evaluatedFindings,
|
||||
SkippedCount: 0,
|
||||
EvaluatedAt: DateTimeOffset.UtcNow));
|
||||
EvaluatedAt: now));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,6 +446,12 @@ public sealed class InMemorySimulationManifestWriter : ISimulationManifestWriter
|
||||
{
|
||||
private readonly Dictionary<string, (SimulationManifest Manifest, ManifestStorageResult Result)> _manifests = new();
|
||||
private readonly object _lock = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemorySimulationManifestWriter(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public ValueTask<ManifestStorageResult> WriteManifestAsync(
|
||||
string tenantId,
|
||||
@@ -462,7 +468,7 @@ public sealed class InMemorySimulationManifestWriter : ISimulationManifestWriter
|
||||
Checksum: checksum,
|
||||
ChecksumAlgorithm: "SHA256",
|
||||
SizeBytes: bytes.Length,
|
||||
StoredAt: DateTimeOffset.UtcNow);
|
||||
StoredAt: _timeProvider.GetUtcNow());
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
|
||||
@@ -17,19 +17,22 @@ public sealed class SimulationSecurityEnforcer : ISimulationSecurityEnforcer
|
||||
private readonly ISecretScanner _secretScanner;
|
||||
private readonly SchedulerWorkerOptions _options;
|
||||
private readonly ILogger<SimulationSecurityEnforcer> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public SimulationSecurityEnforcer(
|
||||
ITenantScopeValidator scopeValidator,
|
||||
IAttestationVerifier attestationVerifier,
|
||||
ISecretScanner secretScanner,
|
||||
SchedulerWorkerOptions options,
|
||||
ILogger<SimulationSecurityEnforcer> logger)
|
||||
ILogger<SimulationSecurityEnforcer> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_scopeValidator = scopeValidator ?? throw new ArgumentNullException(nameof(scopeValidator));
|
||||
_attestationVerifier = attestationVerifier ?? throw new ArgumentNullException(nameof(attestationVerifier));
|
||||
_secretScanner = secretScanner ?? throw new ArgumentNullException(nameof(secretScanner));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -77,7 +80,7 @@ public sealed class SimulationSecurityEnforcer : ISimulationSecurityEnforcer
|
||||
return new SecurityValidationResult(
|
||||
IsValid: isValid,
|
||||
Violations: [.. violations],
|
||||
ValidatedAt: DateTimeOffset.UtcNow,
|
||||
ValidatedAt: _timeProvider.GetUtcNow(),
|
||||
ValidatorVersion: "1.0.0");
|
||||
}
|
||||
|
||||
@@ -112,7 +115,7 @@ public sealed class SimulationSecurityEnforcer : ISimulationSecurityEnforcer
|
||||
return new SecurityValidationResult(
|
||||
IsValid: violations.Count == 0,
|
||||
Violations: [.. violations],
|
||||
ValidatedAt: DateTimeOffset.UtcNow,
|
||||
ValidatedAt: _timeProvider.GetUtcNow(),
|
||||
ValidatorVersion: "1.0.0");
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<ProjectReference Include="../StellaOps.Scheduler.Models/StellaOps.Scheduler.Models.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Scheduler.Persistence/StellaOps.Scheduler.Persistence.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Scheduler.Queue/StellaOps.Scheduler.Queue.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Determinism.Abstractions/StellaOps.Determinism.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Models/StellaOps.Notify.Models.csproj" />
|
||||
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Queue/StellaOps.Notify.Queue.csproj" />
|
||||
<ProjectReference Include="../../../Scanner/__Libraries/StellaOps.Scanner.Surface.Env/StellaOps.Scanner.Surface.Env.csproj" />
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.WebService.GraphJobs;
|
||||
using Xunit;
|
||||
@@ -21,10 +22,10 @@ public sealed class GraphJobServiceTests
|
||||
var initial = CreateBuildJob();
|
||||
await store.AddAsync(initial, CancellationToken.None);
|
||||
|
||||
var clock = new FixedClock(FixedTime);
|
||||
var timeProvider = new FakeTimeProvider(FixedTime);
|
||||
var publisher = new RecordingPublisher();
|
||||
var webhook = new RecordingWebhookClient();
|
||||
var service = new GraphJobService(store, clock, publisher, webhook);
|
||||
var service = new GraphJobService(store, timeProvider, publisher, webhook);
|
||||
|
||||
var request = new GraphJobCompletionRequest
|
||||
{
|
||||
@@ -60,10 +61,10 @@ public sealed class GraphJobServiceTests
|
||||
var initial = CreateBuildJob();
|
||||
await store.AddAsync(initial, CancellationToken.None);
|
||||
|
||||
var clock = new FixedClock(FixedTime);
|
||||
var timeProvider = new FakeTimeProvider(FixedTime);
|
||||
var publisher = new RecordingPublisher();
|
||||
var webhook = new RecordingWebhookClient();
|
||||
var service = new GraphJobService(store, clock, publisher, webhook);
|
||||
var service = new GraphJobService(store, timeProvider, publisher, webhook);
|
||||
|
||||
var request = new GraphJobCompletionRequest
|
||||
{
|
||||
@@ -95,10 +96,10 @@ public sealed class GraphJobServiceTests
|
||||
var initial = CreateBuildJob();
|
||||
await store.AddAsync(initial, CancellationToken.None);
|
||||
|
||||
var clock = new FixedClock(FixedTime);
|
||||
var timeProvider = new FakeTimeProvider(FixedTime);
|
||||
var publisher = new RecordingPublisher();
|
||||
var webhook = new RecordingWebhookClient();
|
||||
var service = new GraphJobService(store, clock, publisher, webhook);
|
||||
var service = new GraphJobService(store, timeProvider, publisher, webhook);
|
||||
|
||||
var firstRequest = new GraphJobCompletionRequest
|
||||
{
|
||||
@@ -140,10 +141,10 @@ public sealed class GraphJobServiceTests
|
||||
public async Task CreateBuildJob_NormalizesSbomDigest()
|
||||
{
|
||||
var store = new TrackingGraphJobStore();
|
||||
var clock = new FixedClock(FixedTime);
|
||||
var timeProvider = new FakeTimeProvider(FixedTime);
|
||||
var publisher = new RecordingPublisher();
|
||||
var webhook = new RecordingWebhookClient();
|
||||
var service = new GraphJobService(store, clock, publisher, webhook);
|
||||
var service = new GraphJobService(store, timeProvider, publisher, webhook);
|
||||
|
||||
var request = new GraphBuildJobRequest
|
||||
{
|
||||
@@ -161,10 +162,10 @@ public sealed class GraphJobServiceTests
|
||||
public async Task CreateBuildJob_RejectsDigestWithoutPrefix()
|
||||
{
|
||||
var store = new TrackingGraphJobStore();
|
||||
var clock = new FixedClock(FixedTime);
|
||||
var timeProvider = new FakeTimeProvider(FixedTime);
|
||||
var publisher = new RecordingPublisher();
|
||||
var webhook = new RecordingWebhookClient();
|
||||
var service = new GraphJobService(store, clock, publisher, webhook);
|
||||
var service = new GraphJobService(store, timeProvider, publisher, webhook);
|
||||
|
||||
var request = new GraphBuildJobRequest
|
||||
{
|
||||
@@ -253,14 +254,4 @@ public sealed class GraphJobServiceTests
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FixedClock : ISystemClock
|
||||
{
|
||||
public FixedClock(DateTimeOffset utcNow)
|
||||
{
|
||||
UtcNow = utcNow;
|
||||
}
|
||||
|
||||
public DateTimeOffset UtcNow { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user