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

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

View File

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

View File

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

View File

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

View File

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