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" />
|
||||
|
||||
Reference in New Issue
Block a user