Files
git.stella-ops.org/src/Policy/StellaOps.Policy.Gateway/Program.cs
StellaOps Bot 7e384ab610 feat: Implement IsolatedReplayContext for deterministic audit replay
- Added IsolatedReplayContext class to provide an isolated environment for replaying audit bundles without external calls.
- Introduced methods for initializing the context, verifying input digests, and extracting inputs for policy evaluation.
- Created supporting interfaces and options for context configuration.

feat: Create ReplayExecutor for executing policy re-evaluation and verdict comparison

- Developed ReplayExecutor class to handle the execution of replay processes, including input verification and verdict comparison.
- Implemented detailed drift detection and error handling during replay execution.
- Added interfaces for policy evaluation and replay execution options.

feat: Add ScanSnapshotFetcher for fetching scan data and snapshots

- Introduced ScanSnapshotFetcher class to retrieve necessary scan data and snapshots for audit bundle creation.
- Implemented methods to fetch scan metadata, advisory feeds, policy snapshots, and VEX statements.
- Created supporting interfaces for scan data, feed snapshots, and policy snapshots.
2025-12-23 07:46:40 +02:00

576 lines
21 KiB
C#

using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NetEscapades.Configuration.Yaml;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.Client;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Configuration;
using StellaOps.Policy.Gateway.Clients;
using StellaOps.Policy.Gateway.Contracts;
using StellaOps.Policy.Gateway.Endpoints;
using StellaOps.Policy.Gateway.Infrastructure;
using StellaOps.Policy.Gateway.Options;
using StellaOps.Policy.Gateway.Services;
using StellaOps.Policy.Deltas;
using StellaOps.Policy.Snapshots;
using StellaOps.Policy.Storage.Postgres;
using Polly;
using Polly.Extensions.Http;
using StellaOps.AirGap.Policy;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddJsonConsole();
builder.Configuration.AddStellaOpsDefaults(options =>
{
options.BasePath = builder.Environment.ContentRootPath;
options.EnvironmentPrefix = "STELLAOPS_POLICY_GATEWAY_";
options.ConfigureBuilder = configurationBuilder =>
{
var contentRoot = builder.Environment.ContentRootPath;
foreach (var relative in new[]
{
"../etc/policy-gateway.yaml",
"../etc/policy-gateway.local.yaml",
"policy-gateway.yaml",
"policy-gateway.local.yaml"
})
{
var path = Path.Combine(contentRoot, relative);
configurationBuilder.AddYamlFile(path, optional: true);
}
};
});
var bootstrap = StellaOpsConfigurationBootstrapper.Build<PolicyGatewayOptions>(options =>
{
options.BasePath = builder.Environment.ContentRootPath;
options.EnvironmentPrefix = "STELLAOPS_POLICY_GATEWAY_";
options.BindingSection = PolicyGatewayOptions.SectionName;
options.ConfigureBuilder = configurationBuilder =>
{
foreach (var relative in new[]
{
"../etc/policy-gateway.yaml",
"../etc/policy-gateway.local.yaml",
"policy-gateway.yaml",
"policy-gateway.local.yaml"
})
{
var path = Path.Combine(builder.Environment.ContentRootPath, relative);
configurationBuilder.AddYamlFile(path, optional: true);
}
};
options.PostBind = static (value, _) => value.Validate();
});
builder.Configuration.AddConfiguration(bootstrap.Configuration);
builder.Services.AddAirGapEgressPolicy(builder.Configuration, sectionName: "AirGap");
builder.Logging.SetMinimumLevel(bootstrap.Options.Telemetry.MinimumLogLevel);
builder.Services.AddOptions<PolicyGatewayOptions>()
.Bind(builder.Configuration.GetSection(PolicyGatewayOptions.SectionName))
.Validate(options =>
{
try
{
options.Validate();
return true;
}
catch (Exception ex)
{
throw new OptionsValidationException(
PolicyGatewayOptions.SectionName,
typeof(PolicyGatewayOptions),
new[] { ex.Message });
}
})
.ValidateOnStart();
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<PolicyGatewayOptions>>().Value);
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddRouting(options => options.LowercaseUrls = true);
builder.Services.AddProblemDetails();
builder.Services.AddHealthChecks();
builder.Services.AddAuthentication();
builder.Services.AddAuthorization();
builder.Services.AddStellaOpsScopeHandler();
builder.Services.AddPolicyPostgresStorage(builder.Configuration);
builder.Services.AddMemoryCache();
// Exception services
builder.Services.Configure<ApprovalWorkflowOptions>(
builder.Configuration.GetSection(ApprovalWorkflowOptions.SectionName));
builder.Services.Configure<ExceptionExpiryOptions>(
builder.Configuration.GetSection(ExceptionExpiryOptions.SectionName));
builder.Services.AddScoped<IExceptionService, ExceptionService>();
builder.Services.AddScoped<IExceptionQueryService, ExceptionQueryService>();
builder.Services.AddScoped<IApprovalWorkflowService, ApprovalWorkflowService>();
builder.Services.AddSingleton<IExceptionNotificationService, NoOpExceptionNotificationService>();
builder.Services.AddHostedService<ExceptionExpiryWorker>();
// Delta services
builder.Services.AddScoped<IDeltaComputer, DeltaComputer>();
builder.Services.AddScoped<IBaselineSelector, BaselineSelector>();
builder.Services.AddScoped<ISnapshotStore, InMemorySnapshotStore>();
builder.Services.AddScoped<StellaOps.Policy.Deltas.ISnapshotService, DeltaSnapshotServiceAdapter>();
builder.Services.AddStellaOpsResourceServerAuthentication(
builder.Configuration,
configurationSection: $"{PolicyGatewayOptions.SectionName}:ResourceServer");
builder.Services.AddSingleton<PolicyGatewayMetrics>();
builder.Services.AddSingleton<PolicyGatewayDpopProofGenerator>();
builder.Services.AddSingleton<PolicyEngineTokenProvider>();
builder.Services.AddTransient<PolicyGatewayDpopHandler>();
if (bootstrap.Options.PolicyEngine.ClientCredentials.Enabled)
{
builder.Services.AddOptions<StellaOpsAuthClientOptions>()
.Configure(options =>
{
options.Authority = bootstrap.Options.ResourceServer.Authority;
options.ClientId = bootstrap.Options.PolicyEngine.ClientCredentials.ClientId;
options.ClientSecret = bootstrap.Options.PolicyEngine.ClientCredentials.ClientSecret;
options.HttpTimeout = TimeSpan.FromSeconds(bootstrap.Options.PolicyEngine.ClientCredentials.BackchannelTimeoutSeconds);
foreach (var scope in bootstrap.Options.PolicyEngine.ClientCredentials.Scopes)
{
options.DefaultScopes.Add(scope);
}
})
.PostConfigure(static opt => opt.Validate());
builder.Services.TryAddSingleton<IStellaOpsTokenCache, InMemoryTokenCache>();
builder.Services.AddHttpClient<StellaOpsDiscoveryCache>((provider, client) =>
{
var authOptions = provider.GetRequiredService<IOptionsMonitor<StellaOpsAuthClientOptions>>().CurrentValue;
client.Timeout = authOptions.HttpTimeout;
}).AddPolicyHandler(static (provider, _) => CreateAuthorityRetryPolicy(provider));
builder.Services.AddHttpClient<StellaOpsJwksCache>((provider, client) =>
{
var authOptions = provider.GetRequiredService<IOptionsMonitor<StellaOpsAuthClientOptions>>().CurrentValue;
client.Timeout = authOptions.HttpTimeout;
}).AddPolicyHandler(static (provider, _) => CreateAuthorityRetryPolicy(provider));
builder.Services.AddHttpClient<IStellaOpsTokenClient, StellaOpsTokenClient>((provider, client) =>
{
var authOptions = provider.GetRequiredService<IOptionsMonitor<StellaOpsAuthClientOptions>>().CurrentValue;
client.Timeout = authOptions.HttpTimeout;
})
.AddPolicyHandler(static (provider, _) => CreateAuthorityRetryPolicy(provider))
.AddHttpMessageHandler<PolicyGatewayDpopHandler>();
}
builder.Services.AddHttpClient<IPolicyEngineClient, PolicyEngineClient>((serviceProvider, client) =>
{
var gatewayOptions = serviceProvider.GetRequiredService<IOptions<PolicyGatewayOptions>>().Value;
var egressPolicy = serviceProvider.GetService<IEgressPolicy>();
if (egressPolicy is not null)
{
egressPolicy.EnsureAllowed(new EgressRequest("PolicyGateway", gatewayOptions.PolicyEngine.BaseUri, "policy-engine-client"));
}
client.BaseAddress = gatewayOptions.PolicyEngine.BaseUri;
client.Timeout = TimeSpan.FromSeconds(gatewayOptions.PolicyEngine.ClientCredentials.BackchannelTimeoutSeconds);
})
.AddPolicyHandler(static (provider, _) => CreatePolicyEngineRetryPolicy(provider));
var app = builder.Build();
app.UseExceptionHandler(static appBuilder => appBuilder.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(new { error = "Unexpected gateway error." });
}));
app.UseStatusCodePages();
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz");
app.MapGet("/readyz", () => Results.Ok(new { status = "ready" }))
.WithName("Readiness");
app.MapGet("/", () => Results.Redirect("/healthz"));
var policyPacks = app.MapGroup("/api/policy/packs")
.WithTags("Policy Packs");
policyPacks.MapGet(string.Empty, async Task<IResult> (
HttpContext context,
IPolicyEngineClient client,
PolicyEngineTokenProvider tokenProvider,
CancellationToken cancellationToken) =>
{
GatewayForwardingContext? forwardingContext = null;
if (GatewayForwardingContext.TryCreate(context, out var callerContext))
{
forwardingContext = callerContext;
}
else if (!tokenProvider.IsEnabled)
{
return Results.Unauthorized();
}
var response = await client.ListPolicyPacksAsync(forwardingContext, cancellationToken).ConfigureAwait(false);
return response.ToMinimalResult();
})
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
policyPacks.MapPost(string.Empty, async Task<IResult> (
HttpContext context,
CreatePolicyPackRequest request,
IPolicyEngineClient client,
PolicyEngineTokenProvider tokenProvider,
CancellationToken cancellationToken) =>
{
if (request is null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Request body required.",
Status = StatusCodes.Status400BadRequest
});
}
GatewayForwardingContext? forwardingContext = null;
if (GatewayForwardingContext.TryCreate(context, out var callerContext))
{
forwardingContext = callerContext;
}
else if (!tokenProvider.IsEnabled)
{
return Results.Unauthorized();
}
var response = await client.CreatePolicyPackAsync(forwardingContext, request, cancellationToken).ConfigureAwait(false);
return response.ToMinimalResult();
})
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor));
policyPacks.MapPost("/{packId}/revisions", async Task<IResult> (
HttpContext context,
string packId,
CreatePolicyRevisionRequest request,
IPolicyEngineClient client,
PolicyEngineTokenProvider tokenProvider,
CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(packId))
{
return Results.BadRequest(new ProblemDetails
{
Title = "packId is required.",
Status = StatusCodes.Status400BadRequest
});
}
if (request is null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Request body required.",
Status = StatusCodes.Status400BadRequest
});
}
GatewayForwardingContext? forwardingContext = null;
if (GatewayForwardingContext.TryCreate(context, out var callerContext))
{
forwardingContext = callerContext;
}
else if (!tokenProvider.IsEnabled)
{
return Results.Unauthorized();
}
var response = await client.CreatePolicyRevisionAsync(forwardingContext, packId, request, cancellationToken).ConfigureAwait(false);
return response.ToMinimalResult();
})
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor));
policyPacks.MapPost("/{packId}/revisions/{version:int}:activate", async Task<IResult> (
HttpContext context,
string packId,
int version,
ActivatePolicyRevisionRequest request,
IPolicyEngineClient client,
PolicyEngineTokenProvider tokenProvider,
PolicyGatewayMetrics metrics,
ILoggerFactory loggerFactory,
CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(packId))
{
return Results.BadRequest(new ProblemDetails
{
Title = "packId is required.",
Status = StatusCodes.Status400BadRequest
});
}
if (request is null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Request body required.",
Status = StatusCodes.Status400BadRequest
});
}
GatewayForwardingContext? forwardingContext = null;
var source = "service";
if (GatewayForwardingContext.TryCreate(context, out var callerContext))
{
forwardingContext = callerContext;
source = "caller";
}
else if (!tokenProvider.IsEnabled)
{
return Results.Unauthorized();
}
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.ActivatePolicyRevisionAsync(forwardingContext, packId, version, request, cancellationToken).ConfigureAwait(false);
stopwatch.Stop();
var outcome = DetermineActivationOutcome(response);
metrics.RecordActivation(outcome, source, stopwatch.Elapsed.TotalMilliseconds);
var logger = loggerFactory.CreateLogger("StellaOps.Policy.Gateway.Activation");
LogActivation(logger, packId, version, outcome, source, response.StatusCode);
return response.ToMinimalResult();
})
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(
StellaOpsScopes.PolicyOperate,
StellaOpsScopes.PolicyActivate));
var cvss = app.MapGroup("/api/cvss")
.WithTags("CVSS Receipts");
cvss.MapPost("/receipts", async Task<IResult>(
HttpContext context,
CreateCvssReceiptRequest request,
IPolicyEngineClient client,
PolicyEngineTokenProvider tokenProvider,
CancellationToken cancellationToken) =>
{
if (request is null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Request body required.",
Status = StatusCodes.Status400BadRequest
});
}
GatewayForwardingContext? forwardingContext = null;
if (GatewayForwardingContext.TryCreate(context, out var callerContext))
{
forwardingContext = callerContext;
}
else if (!tokenProvider.IsEnabled)
{
return Results.Unauthorized();
}
var response = await client.CreateCvssReceiptAsync(forwardingContext, request, cancellationToken).ConfigureAwait(false);
return response.ToMinimalResult();
})
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun));
cvss.MapGet("/receipts/{receiptId}", async Task<IResult>(
HttpContext context,
string receiptId,
IPolicyEngineClient client,
PolicyEngineTokenProvider tokenProvider,
CancellationToken cancellationToken) =>
{
GatewayForwardingContext? forwardingContext = null;
if (GatewayForwardingContext.TryCreate(context, out var callerContext))
{
forwardingContext = callerContext;
}
else if (!tokenProvider.IsEnabled)
{
return Results.Unauthorized();
}
var response = await client.GetCvssReceiptAsync(forwardingContext, receiptId, cancellationToken).ConfigureAwait(false);
return response.ToMinimalResult();
})
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.FindingsRead));
cvss.MapPut("/receipts/{receiptId}/amend", async Task<IResult>(
HttpContext context,
string receiptId,
AmendCvssReceiptRequest request,
IPolicyEngineClient client,
PolicyEngineTokenProvider tokenProvider,
CancellationToken cancellationToken) =>
{
if (request is null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Request body required.",
Status = StatusCodes.Status400BadRequest
});
}
GatewayForwardingContext? forwardingContext = null;
if (GatewayForwardingContext.TryCreate(context, out var callerContext))
{
forwardingContext = callerContext;
}
else if (!tokenProvider.IsEnabled)
{
return Results.Unauthorized();
}
var response = await client.AmendCvssReceiptAsync(forwardingContext, receiptId, request, cancellationToken).ConfigureAwait(false);
return response.ToMinimalResult();
})
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun));
cvss.MapGet("/receipts/{receiptId}/history", async Task<IResult>(
HttpContext context,
string receiptId,
IPolicyEngineClient client,
PolicyEngineTokenProvider tokenProvider,
CancellationToken cancellationToken) =>
{
GatewayForwardingContext? forwardingContext = null;
if (GatewayForwardingContext.TryCreate(context, out var callerContext))
{
forwardingContext = callerContext;
}
else if (!tokenProvider.IsEnabled)
{
return Results.Unauthorized();
}
var response = await client.GetCvssReceiptHistoryAsync(forwardingContext, receiptId, cancellationToken).ConfigureAwait(false);
return response.ToMinimalResult();
})
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.FindingsRead));
cvss.MapGet("/policies", async Task<IResult>(
HttpContext context,
IPolicyEngineClient client,
PolicyEngineTokenProvider tokenProvider,
CancellationToken cancellationToken) =>
{
GatewayForwardingContext? forwardingContext = null;
if (GatewayForwardingContext.TryCreate(context, out var callerContext))
{
forwardingContext = callerContext;
}
else if (!tokenProvider.IsEnabled)
{
return Results.Unauthorized();
}
var response = await client.ListCvssPoliciesAsync(forwardingContext, cancellationToken).ConfigureAwait(false);
return response.ToMinimalResult();
})
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.FindingsRead));
// Exception management endpoints
app.MapExceptionEndpoints();
// Delta management endpoints
app.MapDeltasEndpoints();
app.Run();
static IAsyncPolicy<HttpResponseMessage> CreateAuthorityRetryPolicy(IServiceProvider provider)
{
var authOptions = provider.GetRequiredService<IOptionsMonitor<StellaOpsAuthClientOptions>>().CurrentValue;
var delays = authOptions.NormalizedRetryDelays;
if (delays.Count == 0)
{
return Policy.NoOpAsync<HttpResponseMessage>();
}
var loggerFactory = provider.GetService<ILoggerFactory>();
var logger = loggerFactory?.CreateLogger("PolicyGateway.AuthorityHttp");
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(static message => message.StatusCode == HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(
delays.Count,
attempt => delays[attempt - 1],
(outcome, delay, attempt, _) =>
{
logger?.LogWarning(
outcome.Exception,
"Retrying Authority HTTP call ({Attempt}/{Total}) after {Reason}; waiting {Delay}.",
attempt,
delays.Count,
outcome.Exception?.Message ?? outcome.Result?.StatusCode.ToString(),
delay);
});
}
static IAsyncPolicy<HttpResponseMessage> CreatePolicyEngineRetryPolicy(IServiceProvider provider)
=> HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(static response => response.StatusCode is HttpStatusCode.TooManyRequests or HttpStatusCode.BadGateway or HttpStatusCode.ServiceUnavailable or HttpStatusCode.GatewayTimeout)
.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));
static string DetermineActivationOutcome(PolicyEngineResponse<PolicyRevisionActivationDto> response)
{
if (response.IsSuccess)
{
return response.Value?.Status switch
{
"activated" => "activated",
"already_active" => "already_active",
"pending_second_approval" => "pending_second_approval",
_ => "success"
};
}
return response.StatusCode switch
{
HttpStatusCode.BadRequest => "bad_request",
HttpStatusCode.NotFound => "not_found",
HttpStatusCode.Unauthorized => "unauthorized",
HttpStatusCode.Forbidden => "forbidden",
_ => "error"
};
}
static void LogActivation(ILogger logger, string packId, int version, string outcome, string source, HttpStatusCode statusCode)
{
if (logger is null)
{
return;
}
var message = "Policy activation forwarded.";
var logLevel = outcome is "activated" or "already_active" or "pending_second_approval" ? LogLevel.Information : LogLevel.Warning;
logger.Log(logLevel, message + " Outcome={Outcome}; Source={Source}; PackId={PackId}; Version={Version}; StatusCode={StatusCode}.", outcome, source, packId, version, (int)statusCode);
}
public partial class Program
{
}