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

@@ -1,19 +1,26 @@
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json.Nodes;
using StellaOps.TaskRunner.Core.Expressions;
using StellaOps.TaskRunner.Core.TaskPacks;
using System;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text.Json.Nodes;
using StellaOps.AirGap.Policy;
using StellaOps.TaskRunner.Core.Expressions;
using StellaOps.TaskRunner.Core.TaskPacks;
namespace StellaOps.TaskRunner.Core.Planning;
public sealed class TaskPackPlanner
{
private readonly TaskPackManifestValidator validator;
public TaskPackPlanner()
{
validator = new TaskPackManifestValidator();
}
private static readonly string[] NetworkParameterHints = { "url", "uri", "endpoint", "host", "registry", "mirror", "address" };
private readonly TaskPackManifestValidator validator;
private readonly IEgressPolicy? egressPolicy;
public TaskPackPlanner(IEgressPolicy? egressPolicy = null)
{
validator = new TaskPackManifestValidator();
this.egressPolicy = egressPolicy;
}
public TaskPackPlanResult Plan(TaskPackManifest manifest, IDictionary<string, JsonNode?>? providedInputs = null)
{
@@ -50,14 +57,17 @@ public sealed class TaskPackPlanner
var context = TaskPackExpressionContext.Create(effectiveInputs, stepTracker, secretTracker);
var planSteps = new List<TaskPackPlanStep>();
var steps = manifest.Spec.Steps;
for (var i = 0; i < steps.Count; i++)
{
var step = steps[i];
var planStep = BuildStep(step, context, $"spec.steps[{i}]", errors);
planSteps.Add(planStep);
}
var packName = manifest.Metadata.Name;
var packVersion = manifest.Metadata.Version;
var planSteps = new List<TaskPackPlanStep>();
var steps = manifest.Spec.Steps;
for (var i = 0; i < steps.Count; i++)
{
var step = steps[i];
var planStep = BuildStep(packName, packVersion, step, context, $"spec.steps[{i}]", errors);
planSteps.Add(planStep);
}
if (errors.Count > 0)
{
@@ -70,13 +80,13 @@ public sealed class TaskPackPlanner
manifest.Metadata.Description,
manifest.Metadata.Tags?.ToList() ?? new List<string>());
var planApprovals = manifest.Spec.Approvals?
.Select(approval => new TaskPackPlanApproval(
approval.Id,
approval.Grants?.ToList() ?? new List<string>(),
approval.ExpiresAfter,
approval.ReasonTemplate))
.ToList() ?? new List<TaskPackPlanApproval>();
var planApprovals = manifest.Spec.Approvals?
.Select(approval => new TaskPackPlanApproval(
approval.Id,
NormalizeGrants(approval.Grants),
approval.ExpiresAfter,
approval.ReasonTemplate))
.ToList() ?? new List<TaskPackPlanApproval>();
var planSecrets = manifest.Spec.Secrets?
.Select(secret => new TaskPackPlanSecret(secret.Name, secret.Scope, secret.Description))
@@ -134,11 +144,13 @@ public sealed class TaskPackPlanner
return effective;
}
private TaskPackPlanStep BuildStep(
TaskPackStep step,
TaskPackExpressionContext context,
string path,
ImmutableArray<TaskPackPlanError>.Builder errors)
private TaskPackPlanStep BuildStep(
string packName,
string packVersion,
TaskPackStep step,
TaskPackExpressionContext context,
string path,
ImmutableArray<TaskPackPlanError>.Builder errors)
{
if (!TaskPackExpressions.TryEvaluateBoolean(step.When, context, out var enabled, out var whenError))
{
@@ -146,23 +158,23 @@ public sealed class TaskPackPlanner
enabled = false;
}
TaskPackPlanStep planStep;
if (step.Run is not null)
{
planStep = BuildRunStep(step, step.Run, context, path, enabled, errors);
TaskPackPlanStep planStep;
if (step.Run is not null)
{
planStep = BuildRunStep(packName, packVersion, step, step.Run, context, path, enabled, errors);
}
else if (step.Gate is not null)
{
planStep = BuildGateStep(step, step.Gate, context, path, enabled, errors);
}
else if (step.Gate is not null)
{
planStep = BuildGateStep(step, step.Gate, context, path, enabled, errors);
}
else if (step.Parallel is not null)
{
planStep = BuildParallelStep(step, step.Parallel, context, path, enabled, errors);
}
else if (step.Map is not null)
{
planStep = BuildMapStep(step, step.Map, context, path, enabled, errors);
else if (step.Parallel is not null)
{
planStep = BuildParallelStep(packName, packVersion, step, step.Parallel, context, path, enabled, errors);
}
else if (step.Map is not null)
{
planStep = BuildMapStep(packName, packVersion, step, step.Map, context, path, enabled, errors);
}
else
{
@@ -174,28 +186,235 @@ public sealed class TaskPackPlanner
return planStep;
}
private TaskPackPlanStep BuildRunStep(
TaskPackStep step,
TaskPackRunStep run,
TaskPackExpressionContext context,
string path,
bool enabled,
private TaskPackPlanStep BuildRunStep(
string packName,
string packVersion,
TaskPackStep step,
TaskPackRunStep run,
TaskPackExpressionContext context,
string path,
bool enabled,
ImmutableArray<TaskPackPlanError>.Builder errors)
{
var parameters = ResolveParameters(run.With, context, $"{path}.run", errors);
return new TaskPackPlanStep(
step.Id,
step.Id,
step.Name,
"run",
enabled,
run.Uses,
parameters,
ApprovalId: null,
GateMessage: null,
Children: null);
}
var parameters = ResolveParameters(run.With, context, $"{path}.run", errors);
if (egressPolicy?.IsSealed == true)
{
ValidateRunStepEgress(packName, packVersion, step, run, parameters, path, errors);
}
return new TaskPackPlanStep(
step.Id,
step.Id,
step.Name,
"run",
enabled,
run.Uses,
parameters,
ApprovalId: null,
GateMessage: null,
Children: null);
}
private void ValidateRunStepEgress(
string packName,
string packVersion,
TaskPackStep step,
TaskPackRunStep run,
IReadOnlyDictionary<string, TaskPackPlanParameterValue>? parameters,
string path,
ImmutableArray<TaskPackPlanError>.Builder errors)
{
if (egressPolicy is null || !egressPolicy.IsSealed)
{
return;
}
var destinations = new List<Uri>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
void AddDestination(Uri uri)
{
if (seen.Add(uri.ToString()))
{
destinations.Add(uri);
}
}
if (run.Egress is not null)
{
for (var i = 0; i < run.Egress.Count; i++)
{
var entry = run.Egress[i];
var entryPath = $"{path}.egress[{i}]";
if (entry is null)
{
continue;
}
if (TryParseNetworkUri(entry.Url, out var uri))
{
AddDestination(uri);
}
else
{
errors.Add(new TaskPackPlanError($"{entryPath}.url", "Egress URL must be an absolute HTTP or HTTPS address."));
}
}
}
var requiresRuntimeNetwork = false;
if (parameters is not null)
{
foreach (var parameter in parameters)
{
var value = parameter.Value;
if (value.Value is JsonValue jsonValue && jsonValue.TryGetValue<string>(out var literal) && TryParseNetworkUri(literal, out var uri))
{
AddDestination(uri);
}
else if (value.RequiresRuntimeValue && MightBeNetworkParameter(parameter.Key))
{
requiresRuntimeNetwork = true;
}
}
}
if (destinations.Count == 0)
{
if (requiresRuntimeNetwork && (run.Egress is null || run.Egress.Count == 0))
{
errors.Add(new TaskPackPlanError(path, $"Step '{step.Id}' references runtime network parameters while sealed mode is enabled. Declare explicit run.egress URLs or remove external calls."));
}
return;
}
foreach (var destination in destinations)
{
try
{
var request = new EgressRequest(
component: "TaskRunner",
destination: destination,
intent: $"taskpack:{packName}@{packVersion}:{step.Id}",
transport: DetermineTransport(destination),
operation: run.Uses);
egressPolicy.EnsureAllowed(request);
}
catch (AirGapEgressBlockedException blocked)
{
var remediation = blocked.Remediation;
errors.Add(new TaskPackPlanError(
path,
$"Step '{step.Id}' attempted to reach '{destination}' in sealed mode and was blocked. Reason: {blocked.Reason}. Remediation: {remediation}"));
}
}
}
private static bool TryParseNetworkUri(string? value, out Uri uri)
{
uri = default!;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
if (!Uri.TryCreate(value, UriKind.Absolute, out var parsed))
{
return false;
}
if (!IsNetworkScheme(parsed))
{
return false;
}
uri = parsed;
return true;
}
private static bool IsNetworkScheme(Uri uri)
=> string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase)
|| string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase);
private static bool MightBeNetworkParameter(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return false;
}
foreach (var hint in NetworkParameterHints)
{
if (name.Contains(hint, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private static EgressTransport DetermineTransport(Uri destination)
=> string.Equals(destination.Scheme, "https", StringComparison.OrdinalIgnoreCase)
? EgressTransport.Https
: string.Equals(destination.Scheme, "http", StringComparison.OrdinalIgnoreCase)
? EgressTransport.Http
: EgressTransport.Any;
private static IReadOnlyList<string> NormalizeGrants(IReadOnlyList<string>? grants)
{
if (grants is null || grants.Count == 0)
{
return Array.Empty<string>();
}
var normalized = new List<string>(grants.Count);
foreach (var grant in grants)
{
if (string.IsNullOrWhiteSpace(grant))
{
continue;
}
var segments = grant
.Split('.', StringSplitOptions.RemoveEmptyEntries)
.Select(segment =>
{
var trimmed = segment.Trim();
if (trimmed.Length == 0)
{
return string.Empty;
}
if (trimmed.Length == 1)
{
return trimmed.ToUpperInvariant();
}
var first = char.ToUpperInvariant(trimmed[0]);
var rest = trimmed[1..].ToLowerInvariant();
return string.Concat(first, rest);
})
.Where(segment => segment.Length > 0)
.ToArray();
if (segments.Length == 0)
{
continue;
}
normalized.Add(string.Join('.', segments));
}
return normalized.Count == 0
? Array.Empty<string>()
: normalized;
}
private TaskPackPlanStep BuildGateStep(
TaskPackStep step,
@@ -238,20 +457,22 @@ public sealed class TaskPackPlanner
Children: null);
}
private TaskPackPlanStep BuildParallelStep(
TaskPackStep step,
TaskPackParallelStep parallel,
TaskPackExpressionContext context,
string path,
bool enabled,
ImmutableArray<TaskPackPlanError>.Builder errors)
{
var children = new List<TaskPackPlanStep>();
for (var i = 0; i < parallel.Steps.Count; i++)
{
var child = BuildStep(parallel.Steps[i], context, $"{path}.parallel.steps[{i}]", errors);
children.Add(child);
}
private TaskPackPlanStep BuildParallelStep(
string packName,
string packVersion,
TaskPackStep step,
TaskPackParallelStep parallel,
TaskPackExpressionContext context,
string path,
bool enabled,
ImmutableArray<TaskPackPlanError>.Builder errors)
{
var children = new List<TaskPackPlanStep>();
for (var i = 0; i < parallel.Steps.Count; i++)
{
var child = BuildStep(packName, packVersion, parallel.Steps[i], context, $"{path}.parallel.steps[{i}]", errors);
children.Add(child);
}
var parameters = new Dictionary<string, TaskPackPlanParameterValue>(StringComparer.Ordinal);
if (parallel.MaxParallel.HasValue)
@@ -274,12 +495,14 @@ public sealed class TaskPackPlanner
Children: children);
}
private TaskPackPlanStep BuildMapStep(
TaskPackStep step,
TaskPackMapStep map,
TaskPackExpressionContext context,
string path,
bool enabled,
private TaskPackPlanStep BuildMapStep(
string packName,
string packVersion,
TaskPackStep step,
TaskPackMapStep map,
TaskPackExpressionContext context,
string path,
bool enabled,
ImmutableArray<TaskPackPlanError>.Builder errors)
{
var parameters = new Dictionary<string, TaskPackPlanParameterValue>(StringComparer.Ordinal);
@@ -324,7 +547,7 @@ public sealed class TaskPackPlanner
var item = itemsArray[i];
var iterationContext = context.WithItem(item);
var iterationPath = $"{path}.map.step[{i}]";
var templateStep = BuildStep(map.Step, iterationContext, iterationPath, errors);
var templateStep = BuildStep(packName, packVersion, map.Step, iterationContext, iterationPath, errors);
var childId = $"{step.Id}[{i}]::{map.Step.Id}";
var iterationParameters = templateStep.Parameters is null

View File

@@ -15,8 +15,12 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="YamlDotNet" Version="13.7.1" />
</ItemGroup>
</Project>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="YamlDotNet" Version="13.7.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\AirGap\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy.csproj" />
</ItemGroup>
</Project>

View File

@@ -150,18 +150,33 @@ public sealed class TaskPackStep
public TaskPackMapStep? Map { get; init; }
}
public sealed class TaskPackRunStep
{
[JsonPropertyName("uses")]
public required string Uses { get; init; }
[JsonPropertyName("with")]
public IDictionary<string, JsonNode?>? With { get; init; }
}
public sealed class TaskPackGateStep
{
[JsonPropertyName("approval")]
public sealed class TaskPackRunStep
{
[JsonPropertyName("uses")]
public required string Uses { get; init; }
[JsonPropertyName("with")]
public IDictionary<string, JsonNode?>? With { get; init; }
[JsonPropertyName("egress")]
public IReadOnlyList<TaskPackRunEgress>? Egress { get; init; }
}
public sealed class TaskPackRunEgress
{
[JsonPropertyName("url")]
public required string Url { get; init; }
[JsonPropertyName("intent")]
public string? Intent { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
}
public sealed class TaskPackGateStep
{
[JsonPropertyName("approval")]
public TaskPackApprovalGate? Approval { get; init; }
[JsonPropertyName("policy")]

View File

@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System;
using System.Collections.Immutable;
using System.Text.RegularExpressions;
using System.Linq;
@@ -134,10 +135,10 @@ public sealed class TaskPackManifestValidator
errors.Add(new TaskPackManifestValidationError(path, "Step may define only one of run, gate, parallel, or map."));
}
if (step.Run is not null)
{
ValidateRunStep(step.Run, $"{path}.run", errors);
}
if (step.Run is not null)
{
ValidateRunStep(step.Run, $"{path}.run", errors);
}
if (step.Gate is not null)
{
@@ -156,13 +157,44 @@ public sealed class TaskPackManifestValidator
}
}
private static void ValidateRunStep(TaskPackRunStep run, string path, ICollection<TaskPackManifestValidationError> errors)
{
if (string.IsNullOrWhiteSpace(run.Uses))
{
errors.Add(new TaskPackManifestValidationError($"{path}.uses", "Run step requires 'uses'."));
}
}
private static void ValidateRunStep(TaskPackRunStep run, string path, ICollection<TaskPackManifestValidationError> errors)
{
if (string.IsNullOrWhiteSpace(run.Uses))
{
errors.Add(new TaskPackManifestValidationError($"{path}.uses", "Run step requires 'uses'."));
}
if (run.Egress is not null)
{
for (var i = 0; i < run.Egress.Count; i++)
{
var entry = run.Egress[i];
var entryPath = $"{path}.egress[{i}]";
if (entry is null)
{
errors.Add(new TaskPackManifestValidationError(entryPath, "Egress entry must be specified."));
continue;
}
if (string.IsNullOrWhiteSpace(entry.Url))
{
errors.Add(new TaskPackManifestValidationError($"{entryPath}.url", "Egress entry requires an absolute URL."));
}
else if (!Uri.TryCreate(entry.Url, UriKind.Absolute, out var uri) ||
(!string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
{
errors.Add(new TaskPackManifestValidationError($"{entryPath}.url", "Egress URL must be an absolute HTTP or HTTPS address."));
}
if (entry.Intent is not null && string.IsNullOrWhiteSpace(entry.Intent))
{
errors.Add(new TaskPackManifestValidationError($"{entryPath}.intent", "Intent must be omitted or non-empty."));
}
}
}
}
private static void ValidateGateStep(TaskPackGateStep gate, HashSet<string> approvalIds, string path, ICollection<TaskPackManifestValidationError> errors)
{