up
This commit is contained in:
245
src/StellaOps.Scanner.WebService/Program.cs
Normal file
245
src/StellaOps.Scanner.WebService/Program.cs
Normal file
@@ -0,0 +1,245 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using StellaOps.Auth.Client;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Plugin.DependencyInjection;
|
||||
using StellaOps.Scanner.WebService.Diagnostics;
|
||||
using StellaOps.Scanner.WebService.Endpoints;
|
||||
using StellaOps.Scanner.WebService.Extensions;
|
||||
using StellaOps.Scanner.WebService.Hosting;
|
||||
using StellaOps.Scanner.WebService.Options;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
using StellaOps.Scanner.WebService.Security;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Configuration.AddStellaOpsDefaults(options =>
|
||||
{
|
||||
options.BasePath = builder.Environment.ContentRootPath;
|
||||
options.EnvironmentPrefix = "SCANNER_";
|
||||
options.ConfigureBuilder = configurationBuilder =>
|
||||
{
|
||||
configurationBuilder.AddScannerYaml(Path.Combine(builder.Environment.ContentRootPath, "../etc/scanner.yaml"));
|
||||
};
|
||||
});
|
||||
|
||||
var contentRoot = builder.Environment.ContentRootPath;
|
||||
|
||||
var bootstrapOptions = builder.Configuration.BindOptions<ScannerWebServiceOptions>(
|
||||
ScannerWebServiceOptions.SectionName,
|
||||
(opts, _) =>
|
||||
{
|
||||
ScannerWebServiceOptionsPostConfigure.Apply(opts, contentRoot);
|
||||
ScannerWebServiceOptionsValidator.Validate(opts);
|
||||
});
|
||||
|
||||
builder.Services.AddOptions<ScannerWebServiceOptions>()
|
||||
.Bind(builder.Configuration.GetSection(ScannerWebServiceOptions.SectionName))
|
||||
.PostConfigure(options =>
|
||||
{
|
||||
ScannerWebServiceOptionsPostConfigure.Apply(options, contentRoot);
|
||||
ScannerWebServiceOptionsValidator.Validate(options);
|
||||
})
|
||||
.ValidateOnStart();
|
||||
|
||||
builder.Host.UseSerilog((context, services, loggerConfiguration) =>
|
||||
{
|
||||
loggerConfiguration
|
||||
.MinimumLevel.Information()
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console();
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton(TimeProvider.System);
|
||||
builder.Services.AddSingleton<ServiceStatus>();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddSingleton<ScanProgressStream>();
|
||||
builder.Services.AddSingleton<IScanProgressPublisher>(sp => sp.GetRequiredService<ScanProgressStream>());
|
||||
builder.Services.AddSingleton<IScanProgressReader>(sp => sp.GetRequiredService<ScanProgressStream>());
|
||||
builder.Services.AddSingleton<IScanCoordinator, InMemoryScanCoordinator>();
|
||||
|
||||
var pluginHostOptions = ScannerPluginHostFactory.Build(bootstrapOptions, contentRoot);
|
||||
builder.Services.RegisterPluginRoutines(builder.Configuration, pluginHostOptions);
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
if (bootstrapOptions.Authority.Enabled)
|
||||
{
|
||||
builder.Services.AddStellaOpsAuthClient(clientOptions =>
|
||||
{
|
||||
clientOptions.Authority = bootstrapOptions.Authority.Issuer;
|
||||
clientOptions.ClientId = bootstrapOptions.Authority.ClientId ?? string.Empty;
|
||||
clientOptions.ClientSecret = bootstrapOptions.Authority.ClientSecret;
|
||||
clientOptions.HttpTimeout = TimeSpan.FromSeconds(bootstrapOptions.Authority.BackchannelTimeoutSeconds);
|
||||
|
||||
clientOptions.DefaultScopes.Clear();
|
||||
foreach (var scope in bootstrapOptions.Authority.ClientScopes)
|
||||
{
|
||||
clientOptions.DefaultScopes.Add(scope);
|
||||
}
|
||||
|
||||
var resilience = bootstrapOptions.Authority.Resilience ?? new ScannerWebServiceOptions.AuthorityOptions.ResilienceOptions();
|
||||
if (resilience.EnableRetries.HasValue)
|
||||
{
|
||||
clientOptions.EnableRetries = resilience.EnableRetries.Value;
|
||||
}
|
||||
|
||||
if (resilience.RetryDelays is { Count: > 0 })
|
||||
{
|
||||
clientOptions.RetryDelays.Clear();
|
||||
foreach (var delay in resilience.RetryDelays)
|
||||
{
|
||||
clientOptions.RetryDelays.Add(delay);
|
||||
}
|
||||
}
|
||||
|
||||
if (resilience.AllowOfflineCacheFallback.HasValue)
|
||||
{
|
||||
clientOptions.AllowOfflineCacheFallback = resilience.AllowOfflineCacheFallback.Value;
|
||||
}
|
||||
|
||||
if (resilience.OfflineCacheTolerance.HasValue)
|
||||
{
|
||||
clientOptions.OfflineCacheTolerance = resilience.OfflineCacheTolerance.Value;
|
||||
}
|
||||
});
|
||||
|
||||
builder.Services.AddStellaOpsResourceServerAuthentication(
|
||||
builder.Configuration,
|
||||
configurationSection: null,
|
||||
configure: resourceOptions =>
|
||||
{
|
||||
resourceOptions.Authority = bootstrapOptions.Authority.Issuer;
|
||||
resourceOptions.RequireHttpsMetadata = bootstrapOptions.Authority.RequireHttpsMetadata;
|
||||
resourceOptions.MetadataAddress = bootstrapOptions.Authority.MetadataAddress;
|
||||
resourceOptions.BackchannelTimeout = TimeSpan.FromSeconds(bootstrapOptions.Authority.BackchannelTimeoutSeconds);
|
||||
resourceOptions.TokenClockSkew = TimeSpan.FromSeconds(bootstrapOptions.Authority.TokenClockSkewSeconds);
|
||||
|
||||
resourceOptions.Audiences.Clear();
|
||||
foreach (var audience in bootstrapOptions.Authority.Audiences)
|
||||
{
|
||||
resourceOptions.Audiences.Add(audience);
|
||||
}
|
||||
|
||||
resourceOptions.RequiredScopes.Clear();
|
||||
foreach (var scope in bootstrapOptions.Authority.RequiredScopes)
|
||||
{
|
||||
resourceOptions.RequiredScopes.Add(scope);
|
||||
}
|
||||
|
||||
resourceOptions.BypassNetworks.Clear();
|
||||
foreach (var network in bootstrapOptions.Authority.BypassNetworks)
|
||||
{
|
||||
resourceOptions.BypassNetworks.Add(network);
|
||||
}
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddStellaOpsScopePolicy(ScannerPolicies.ScansEnqueue, bootstrapOptions.Authority.RequiredScopes.ToArray());
|
||||
options.AddStellaOpsScopePolicy(ScannerPolicies.ScansRead, ScannerAuthorityScopes.ScansRead);
|
||||
options.AddStellaOpsScopePolicy(ScannerPolicies.Reports, ScannerAuthorityScopes.ReportsRead);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = "Anonymous";
|
||||
options.DefaultChallengeScheme = "Anonymous";
|
||||
})
|
||||
.AddScheme<AuthenticationSchemeOptions, AnonymousAuthenticationHandler>("Anonymous", _ => { });
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy(ScannerPolicies.ScansEnqueue, policy => policy.RequireAssertion(_ => true));
|
||||
options.AddPolicy(ScannerPolicies.ScansRead, policy => policy.RequireAssertion(_ => true));
|
||||
options.AddPolicy(ScannerPolicies.Reports, policy => policy.RequireAssertion(_ => true));
|
||||
});
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
var resolvedOptions = app.Services.GetRequiredService<IOptions<ScannerWebServiceOptions>>().Value;
|
||||
var authorityConfigured = resolvedOptions.Authority.Enabled;
|
||||
if (authorityConfigured && resolvedOptions.Authority.AllowAnonymousFallback)
|
||||
{
|
||||
app.Logger.LogWarning(
|
||||
"Scanner authority authentication is enabled but anonymous fallback remains allowed. Disable fallback before production rollout.");
|
||||
}
|
||||
|
||||
if (resolvedOptions.Telemetry.EnableLogging && resolvedOptions.Telemetry.EnableRequestLogging)
|
||||
{
|
||||
app.UseSerilogRequestLogging(options =>
|
||||
{
|
||||
options.GetLevel = (httpContext, elapsed, exception) =>
|
||||
exception is null ? LogEventLevel.Information : LogEventLevel.Error;
|
||||
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
|
||||
{
|
||||
diagnosticContext.Set("RequestId", httpContext.TraceIdentifier);
|
||||
diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString());
|
||||
if (Activity.Current is { TraceId: var traceId } && traceId != default)
|
||||
{
|
||||
diagnosticContext.Set("TraceId", traceId.ToString());
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
app.UseExceptionHandler(errorApp =>
|
||||
{
|
||||
errorApp.Run(async context =>
|
||||
{
|
||||
context.Response.ContentType = "application/problem+json";
|
||||
var feature = context.Features.Get<IExceptionHandlerFeature>();
|
||||
var error = feature?.Error;
|
||||
|
||||
var extensions = new Dictionary<string, object?>(StringComparer.Ordinal)
|
||||
{
|
||||
["traceId"] = Activity.Current?.TraceId.ToString() ?? context.TraceIdentifier,
|
||||
};
|
||||
|
||||
var problem = Results.Problem(
|
||||
detail: error?.Message,
|
||||
instance: context.Request.Path,
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Unexpected server error",
|
||||
type: "https://stellaops.org/problems/internal-error",
|
||||
extensions: extensions);
|
||||
|
||||
await problem.ExecuteAsync(context).ConfigureAwait(false);
|
||||
});
|
||||
});
|
||||
|
||||
if (authorityConfigured)
|
||||
{
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
}
|
||||
|
||||
app.MapHealthEndpoints();
|
||||
|
||||
var apiGroup = app.MapGroup(resolvedOptions.Api.BasePath);
|
||||
|
||||
if (app.Environment.IsEnvironment("Testing"))
|
||||
{
|
||||
apiGroup.MapGet("/__auth-probe", () => Results.Ok("ok"))
|
||||
.RequireAuthorization(ScannerPolicies.ScansEnqueue)
|
||||
.WithName("scanner.auth-probe");
|
||||
}
|
||||
|
||||
apiGroup.MapScanEndpoints();
|
||||
await app.RunAsync().ConfigureAwait(false);
|
||||
Reference in New Issue
Block a user