feat: Implement vulnerability token signing and verification utilities
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user