Files
git.stella-ops.org/src/Scheduler/StellaOps.Scheduler.WebService/SchedulerEndpointHelpers.cs
StellaOps Bot f7d27c6fda 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.
2026-01-04 15:44:49 +02:00

179 lines
5.6 KiB
C#

using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Text;
using StellaOps.Determinism;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Persistence.Postgres.Repositories;
namespace StellaOps.Scheduler.WebService;
internal static class SchedulerEndpointHelpers
{
private const string ActorHeader = "X-Actor-Id";
private const string ActorNameHeader = "X-Actor-Name";
private const string ActorKindHeader = "X-Actor-Kind";
private const string TenantHeader = "X-Tenant-Id";
public static string GenerateIdentifier(string prefix, IGuidProvider? guidProvider = null)
{
if (string.IsNullOrWhiteSpace(prefix))
{
throw new ArgumentException("Prefix must be provided.", nameof(prefix));
}
var guid = (guidProvider ?? SystemGuidProvider.Instance).NewGuid();
return $"{prefix.Trim()}_{guid:N}";
}
public static string ResolveActorId(HttpContext context)
{
ArgumentNullException.ThrowIfNull(context);
if (context.Request.Headers.TryGetValue(ActorHeader, out var values))
{
var actor = values.ToString().Trim();
if (!string.IsNullOrEmpty(actor))
{
return actor;
}
}
if (context.Request.Headers.TryGetValue(TenantHeader, out var tenant))
{
var tenantId = tenant.ToString().Trim();
if (!string.IsNullOrEmpty(tenantId))
{
return tenantId;
}
}
return "system";
}
public static AuditActor ResolveAuditActor(HttpContext context)
{
ArgumentNullException.ThrowIfNull(context);
var actorId = context.Request.Headers.TryGetValue(ActorHeader, out var idHeader)
? idHeader.ToString().Trim()
: null;
var displayName = context.Request.Headers.TryGetValue(ActorNameHeader, out var nameHeader)
? nameHeader.ToString().Trim()
: null;
var kind = context.Request.Headers.TryGetValue(ActorKindHeader, out var kindHeader)
? kindHeader.ToString().Trim()
: null;
if (string.IsNullOrWhiteSpace(actorId))
{
actorId = context.Request.Headers.TryGetValue(TenantHeader, out var tenantHeader)
? tenantHeader.ToString().Trim()
: "system";
}
displayName = string.IsNullOrWhiteSpace(displayName) ? actorId : displayName;
kind = string.IsNullOrWhiteSpace(kind) ? "user" : kind;
return new AuditActor(actorId!, displayName!, kind!);
}
public static bool TryParseBoolean(string? value)
=> !string.IsNullOrWhiteSpace(value) &&
(string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
string.Equals(value, "1", StringComparison.OrdinalIgnoreCase));
public static int? TryParsePositiveInt(string? value)
{
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) && parsed > 0)
{
return parsed;
}
return null;
}
public static DateTimeOffset? TryParseDateTimeOffset(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed))
{
return parsed.ToUniversalTime();
}
throw new ValidationException($"Value '{value}' is not a valid ISO-8601 timestamp.");
}
public static Selector NormalizeSelector(Selector selection, string tenantId)
{
ArgumentNullException.ThrowIfNull(selection);
if (string.IsNullOrWhiteSpace(tenantId))
{
throw new ArgumentException("Tenant identifier must be provided.", nameof(tenantId));
}
return new Selector(
selection.Scope,
tenantId,
selection.Namespaces,
selection.Repositories,
selection.Digests,
selection.IncludeTags,
selection.Labels,
selection.ResolvesTags);
}
public static string CreateRunCursor(Run run)
{
ArgumentNullException.ThrowIfNull(run);
var payload = $"{run.CreatedAt.ToUniversalTime():O}|{run.Id}";
return Convert.ToBase64String(Encoding.UTF8.GetBytes(payload));
}
public static RunListCursor? TryParseRunCursor(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var trimmed = value.Trim();
if (trimmed.Length == 0)
{
return null;
}
try
{
var bytes = Convert.FromBase64String(trimmed);
var decoded = Encoding.UTF8.GetString(bytes);
var parts = decoded.Split('|', 2, StringSplitOptions.TrimEntries);
if (parts.Length != 2)
{
throw new ValidationException($"Cursor '{value}' is not valid.");
}
if (!DateTimeOffset.TryParse(parts[0], CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var timestamp))
{
throw new ValidationException($"Cursor '{value}' is not valid.");
}
if (string.IsNullOrWhiteSpace(parts[1]))
{
throw new ValidationException($"Cursor '{value}' is not valid.");
}
return new RunListCursor(timestamp.ToUniversalTime(), parts[1]);
}
catch (FormatException ex)
{
throw new ValidationException($"Cursor '{value}' is not valid.", ex);
}
}
}