feat: Implement vulnerability token signing and verification utilities
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added VulnTokenSigner for signing JWT tokens with specified algorithms and keys.
- Introduced VulnTokenUtilities for resolving tenant and subject claims, and sanitizing context dictionaries.
- Created VulnTokenVerificationUtilities for parsing tokens, verifying signatures, and deserializing payloads.
- Developed VulnWorkflowAntiForgeryTokenIssuer for issuing anti-forgery tokens with configurable options.
- Implemented VulnWorkflowAntiForgeryTokenVerifier for verifying anti-forgery tokens and validating payloads.
- Added AuthorityVulnerabilityExplorerOptions to manage configuration for vulnerability explorer features.
- Included tests for FilesystemPackRunDispatcher to ensure proper job handling under egress policy restrictions.
This commit is contained in:
master
2025-11-03 10:02:29 +02:00
parent bf2bf4b395
commit b1e78fe412
215 changed files with 19441 additions and 12185 deletions

View File

@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Configuration;
/// <summary>
/// Options controlling vulnerability explorer specific features exposed by Authority.
/// </summary>
public sealed class AuthorityVulnerabilityExplorerOptions
{
/// <summary>
/// Workflow-oriented configuration (anti-forgery tokens, CSRF enforcement helpers).
/// </summary>
public AuthorityVulnWorkflowOptions Workflow { get; } = new();
/// <summary>
/// Attachment handling configuration (signed access tokens).
/// </summary>
public AuthorityVulnAttachmentOptions Attachments { get; } = new();
internal void Validate()
{
Workflow.Validate();
Attachments.Validate();
}
}
/// <summary>
/// Workflow specific configuration for Vuln Explorer clients.
/// </summary>
public sealed class AuthorityVulnWorkflowOptions
{
/// <summary>
/// Anti-forgery token configuration.
/// </summary>
public AuthorityVulnAntiForgeryOptions AntiForgery { get; } = new();
internal void Validate()
{
AntiForgery.Validate();
}
}
/// <summary>
/// Anti-forgery token configuration used to protect workflow submissions.
/// </summary>
public sealed class AuthorityVulnAntiForgeryOptions
{
/// <summary>
/// Determines whether anti-forgery token issuance/verification is enabled.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Audience claim value embedded in issued tokens.
/// </summary>
public string Audience { get; set; } = "stellaops:vuln-workflow";
/// <summary>
/// Default lifetime applied when callers omit an explicit expiration.
/// </summary>
public TimeSpan DefaultLifetime { get; set; } = TimeSpan.FromMinutes(10);
/// <summary>
/// Maximum lifetime permitted for anti-forgery tokens.
/// </summary>
public TimeSpan MaxLifetime { get; set; } = TimeSpan.FromMinutes(30);
/// <summary>
/// Optional maximum size for the context dictionary payload.
/// </summary>
public int MaxContextEntries { get; set; } = 16;
/// <summary>
/// Maximum length permitted per context value entry.
/// </summary>
public int MaxContextValueLength { get; set; } = 256;
internal void Validate()
{
if (!Enabled)
{
return;
}
if (string.IsNullOrWhiteSpace(Audience))
{
throw new InvalidOperationException("vulnerabilityExplorer.workflow.antiForgery.audience must be specified when anti-forgery tokens are enabled.");
}
if (DefaultLifetime <= TimeSpan.Zero)
{
throw new InvalidOperationException("vulnerabilityExplorer.workflow.antiForgery.defaultLifetime must be greater than zero.");
}
if (MaxLifetime <= TimeSpan.Zero || MaxLifetime < DefaultLifetime)
{
throw new InvalidOperationException("vulnerabilityExplorer.workflow.antiForgery.maxLifetime must be greater than zero and greater than or equal to defaultLifetime.");
}
if (MaxContextEntries < 0)
{
throw new InvalidOperationException("vulnerabilityExplorer.workflow.antiForgery.maxContextEntries must be non-negative.");
}
if (MaxContextValueLength <= 0)
{
throw new InvalidOperationException("vulnerabilityExplorer.workflow.antiForgery.maxContextValueLength must be greater than zero.");
}
}
}
/// <summary>
/// Attachment token configuration used to protect ledger attachments.
/// </summary>
public sealed class AuthorityVulnAttachmentOptions
{
/// <summary>
/// Determines whether attachment token issuance/verification is enabled.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Default lifetime for attachment access tokens.
/// </summary>
public TimeSpan DefaultLifetime { get; set; } = TimeSpan.FromMinutes(30);
/// <summary>
/// Maximum lifetime permitted for attachment access tokens.
/// </summary>
public TimeSpan MaxLifetime { get; set; } = TimeSpan.FromHours(4);
/// <summary>
/// Payload type identifier emitted in audit and downstream validation.
/// </summary>
public string PayloadType { get; set; } = "application/vnd.stellaops.vuln-attachment-token+json";
/// <summary>
/// Optional limit on attachment metadata entries.
/// </summary>
public int MaxMetadataEntries { get; set; } = 16;
/// <summary>
/// Optional maximum length for metadata values.
/// </summary>
public int MaxMetadataValueLength { get; set; } = 512;
internal void Validate()
{
if (!Enabled)
{
return;
}
if (DefaultLifetime <= TimeSpan.Zero)
{
throw new InvalidOperationException("vulnerabilityExplorer.attachments.defaultLifetime must be greater than zero when attachment tokens are enabled.");
}
if (MaxLifetime <= TimeSpan.Zero || MaxLifetime < DefaultLifetime)
{
throw new InvalidOperationException("vulnerabilityExplorer.attachments.maxLifetime must be greater than zero and greater than or equal to defaultLifetime.");
}
if (string.IsNullOrWhiteSpace(PayloadType))
{
throw new InvalidOperationException("vulnerabilityExplorer.attachments.payloadType must be specified when attachment tokens are enabled.");
}
if (MaxMetadataEntries < 0)
{
throw new InvalidOperationException("vulnerabilityExplorer.attachments.maxMetadataEntries must be non-negative.");
}
if (MaxMetadataValueLength <= 0)
{
throw new InvalidOperationException("vulnerabilityExplorer.attachments.maxMetadataValueLength must be greater than zero.");
}
}
}

View File

@@ -99,6 +99,11 @@ public sealed class StellaOpsAuthorityOptions
/// </summary>
public AuthorityNotificationsOptions Notifications { get; } = new();
/// <summary>
/// Vulnerability explorer integration configuration (workflow CSRF tokens, attachments).
/// </summary>
public AuthorityVulnerabilityExplorerOptions VulnerabilityExplorer { get; } = new();
/// <summary>
/// Exception governance configuration (routing templates, MFA requirements).
/// </summary>
@@ -158,6 +163,7 @@ public sealed class StellaOpsAuthorityOptions
AdvisoryAi.Normalize();
AdvisoryAi.Validate();
Notifications.Validate();
VulnerabilityExplorer.Validate();
ApiLifecycle.Validate();
Signing.Validate();
Delegation.NormalizeAndValidate(tenants);
@@ -772,6 +778,9 @@ public sealed class AuthorityTenantOptions
public IList<string> DefaultRoles { get; } = new List<string>();
public IList<string> Projects { get; } = new List<string>();
public IDictionary<string, AuthorityTenantRoleOptions> Roles { get; } =
new Dictionary<string, AuthorityTenantRoleOptions>(StringComparer.OrdinalIgnoreCase);
public AuthorityTenantAdvisoryAiOptions AdvisoryAi { get; } = new();
public AuthorityTenantDelegationOptions Delegation { get; } = new();
@@ -820,6 +829,28 @@ public sealed class AuthorityTenantOptions
AdvisoryAi.Normalize(advisoryAiOptions);
Delegation.Normalize(delegationOptions);
if (Roles.Count > 0)
{
var normalizedRoles = new Dictionary<string, AuthorityTenantRoleOptions>(StringComparer.Ordinal);
foreach (var (roleName, roleOptions) in Roles)
{
if (string.IsNullOrWhiteSpace(roleName) || roleOptions is null)
{
continue;
}
var normalizedName = roleName.Trim().ToLowerInvariant();
roleOptions.Normalize(normalizedName);
normalizedRoles[normalizedName] = roleOptions;
}
Roles.Clear();
foreach (var entry in normalizedRoles)
{
Roles.Add(entry.Key, entry.Value);
}
}
}
internal void Validate(AuthorityAdvisoryAiOptions? advisoryAiOptions, AuthorityDelegationOptions delegationOptions)
@@ -852,12 +883,149 @@ public sealed class AuthorityTenantOptions
AdvisoryAi.Validate(advisoryAiOptions);
Delegation.Validate(delegationOptions, Id);
if (Roles.Count > 0)
{
foreach (var (roleName, roleOptions) in Roles)
{
if (roleOptions is null)
{
throw new InvalidOperationException($"Tenant '{Id}' defines role '{roleName}' without configuration.");
}
roleOptions.Validate(Id, roleName);
}
}
}
private static readonly Regex TenantSlugRegex = new("^[a-z0-9-]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex ProjectSlugRegex = new("^[a-z0-9-]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
}
public sealed class AuthorityTenantRoleOptions
{
public IList<string> Scopes { get; } = new List<string>();
public IDictionary<string, IList<string>> Attributes { get; } =
new Dictionary<string, IList<string>>(StringComparer.OrdinalIgnoreCase);
internal void Normalize(string roleName)
{
if (Scopes.Count > 0)
{
var seenScopes = new HashSet<string>(StringComparer.Ordinal);
for (var index = Scopes.Count - 1; index >= 0; index--)
{
var current = Scopes[index];
var normalized = StellaOpsScopes.Normalize(current);
if (string.IsNullOrWhiteSpace(normalized))
{
Scopes.RemoveAt(index);
continue;
}
if (!seenScopes.Add(normalized))
{
Scopes.RemoveAt(index);
continue;
}
Scopes[index] = normalized;
}
}
if (Attributes.Count > 0)
{
var normalizedAttributes = new Dictionary<string, IList<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var (attributeName, values) in Attributes)
{
var normalizedName = attributeName?.Trim();
if (string.IsNullOrWhiteSpace(normalizedName))
{
continue;
}
normalizedName = normalizedName.ToLowerInvariant();
var normalizedValues = new List<string>();
var seenValues = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var wildcard = false;
if (values is not null)
{
foreach (var value in values)
{
if (string.IsNullOrWhiteSpace(value))
{
continue;
}
var trimmed = value.Trim();
if (trimmed.Equals("*", StringComparison.Ordinal))
{
normalizedValues.Clear();
normalizedValues.Add("*");
wildcard = true;
break;
}
var lower = trimmed.ToLowerInvariant();
if (seenValues.Add(lower))
{
normalizedValues.Add(lower);
}
}
}
if (wildcard || normalizedValues.Count > 0)
{
normalizedAttributes[normalizedName] = normalizedValues;
}
}
Attributes.Clear();
foreach (var pair in normalizedAttributes)
{
Attributes[pair.Key] = pair.Value;
}
}
}
internal void Validate(string tenantId, string roleName)
{
if (Scopes.Count == 0)
{
throw new InvalidOperationException($"Tenant '{tenantId}' role '{roleName}' must specify at least one scope.");
}
foreach (var scope in Scopes)
{
if (!StellaOpsScopes.IsKnown(scope))
{
throw new InvalidOperationException($"Tenant '{tenantId}' role '{roleName}' references unknown scope '{scope}'.");
}
}
if (Attributes.Count > 0)
{
foreach (var attributeName in Attributes.Keys)
{
if (!AllowedAttributeKeys.Contains(attributeName))
{
throw new InvalidOperationException($"Tenant '{tenantId}' role '{roleName}' defines unsupported attribute '{attributeName}'. Allowed attributes: env, owner, business_tier.");
}
}
}
}
private static readonly HashSet<string> AllowedAttributeKeys = new(new[]
{
"env",
"owner",
"business_tier"
}, StringComparer.OrdinalIgnoreCase);
}
public sealed class AuthorityDelegationOptions
{
private readonly IList<AuthorityServiceAccountSeedOptions> serviceAccounts = new List<AuthorityServiceAccountSeedOptions>();
@@ -977,6 +1145,9 @@ public sealed class AuthorityServiceAccountSeedOptions
public IList<string> AllowedScopes { get; } = new List<string>();
public IDictionary<string, IList<string>> Attributes { get; } =
new Dictionary<string, IList<string>>(StringComparer.OrdinalIgnoreCase);
internal void Normalize()
{
AccountId = (AccountId ?? string.Empty).Trim();
@@ -990,6 +1161,8 @@ public sealed class AuthorityServiceAccountSeedOptions
var normalized = StellaOpsScopes.Normalize(scope);
return normalized ?? scope.Trim().ToLowerInvariant();
}, StringComparer.Ordinal);
NormalizeAttributes(Attributes);
}
internal void Validate(ISet<string> tenantIds)
@@ -1018,6 +1191,17 @@ public sealed class AuthorityServiceAccountSeedOptions
{
throw new InvalidOperationException($"Service account '{AccountId}' must specify at least one allowed scope.");
}
if (Attributes.Count > 0)
{
foreach (var attributeName in Attributes.Keys)
{
if (!AllowedAttributeKeys.Contains(attributeName))
{
throw new InvalidOperationException($"Service account '{AccountId}' defines unsupported attribute '{attributeName}'. Allowed attributes: env, owner, business_tier.");
}
}
}
}
private static void NormalizeList(IList<string> values, Func<string, string> normalize, IEqualityComparer<string> comparer)
@@ -1057,6 +1241,74 @@ public sealed class AuthorityServiceAccountSeedOptions
values[index] = normalized;
}
}
private static void NormalizeAttributes(IDictionary<string, IList<string>> attributes)
{
ArgumentNullException.ThrowIfNull(attributes);
if (attributes.Count == 0)
{
return;
}
var normalized = new Dictionary<string, IList<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var (name, values) in attributes)
{
var key = string.IsNullOrWhiteSpace(name) ? null : name.Trim().ToLowerInvariant();
if (string.IsNullOrWhiteSpace(key))
{
continue;
}
var normalizedValues = new List<string>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var wildcard = false;
if (values is not null)
{
foreach (var value in values)
{
if (string.IsNullOrWhiteSpace(value))
{
continue;
}
var trimmed = value.Trim();
if (trimmed.Equals("*", StringComparison.Ordinal))
{
normalizedValues.Clear();
normalizedValues.Add("*");
wildcard = true;
break;
}
var lower = trimmed.ToLowerInvariant();
if (seen.Add(lower))
{
normalizedValues.Add(lower);
}
}
}
if (wildcard || normalizedValues.Count > 0)
{
normalized[key] = normalizedValues;
}
}
attributes.Clear();
foreach (var pair in normalized)
{
attributes[pair.Key] = pair.Value;
}
}
private static readonly HashSet<string> AllowedAttributeKeys = new(new[]
{
"env",
"owner",
"business_tier"
}, StringComparer.OrdinalIgnoreCase);
}