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.Infrastructure; using StellaOps.Policy.Gateway.Options; using StellaOps.Policy.Gateway.Services; 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(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() .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>().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.AddStellaOpsResourceServerAuthentication( builder.Configuration, configurationSection: $"{PolicyGatewayOptions.SectionName}:ResourceServer"); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddTransient(); if (bootstrap.Options.PolicyEngine.ClientCredentials.Enabled) { builder.Services.AddOptions() .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(); builder.Services.AddHttpClient((provider, client) => { var authOptions = provider.GetRequiredService>().CurrentValue; client.Timeout = authOptions.HttpTimeout; }).AddPolicyHandler(static (provider, _) => CreateAuthorityRetryPolicy(provider)); builder.Services.AddHttpClient((provider, client) => { var authOptions = provider.GetRequiredService>().CurrentValue; client.Timeout = authOptions.HttpTimeout; }).AddPolicyHandler(static (provider, _) => CreateAuthorityRetryPolicy(provider)); builder.Services.AddHttpClient((provider, client) => { var authOptions = provider.GetRequiredService>().CurrentValue; client.Timeout = authOptions.HttpTimeout; }) .AddPolicyHandler(static (provider, _) => CreateAuthorityRetryPolicy(provider)) .AddHttpMessageHandler(); } builder.Services.AddHttpClient((serviceProvider, client) => { var gatewayOptions = serviceProvider.GetRequiredService>().Value; var egressPolicy = serviceProvider.GetService(); 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 ( 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 ( 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 ( 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 ( 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( 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( 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( 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( 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( 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)); app.Run(); static IAsyncPolicy CreateAuthorityRetryPolicy(IServiceProvider provider) { var authOptions = provider.GetRequiredService>().CurrentValue; var delays = authOptions.NormalizedRetryDelays; if (delays.Count == 0) { return Policy.NoOpAsync(); } var loggerFactory = provider.GetService(); 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 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 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 { }