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:
StellaOps Bot
2026-01-04 15:44:49 +02:00
parent 1f33143bd1
commit f7d27c6fda
44 changed files with 2406 additions and 1107 deletions

View File

@@ -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)
{

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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}";
}

View File

@@ -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();

View File

@@ -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)

View File

@@ -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,

View File

@@ -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" />