- 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.
179 lines
5.6 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|