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.Cryptography.DependencyInjection; using StellaOps.Cryptography.Plugin.BouncyCastle; using StellaOps.Policy; using StellaOps.Scanner.Cache; 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.SectionName, (opts, _) => { ScannerWebServiceOptionsPostConfigure.Apply(opts, contentRoot); ScannerWebServiceOptionsValidator.Validate(opts); }); builder.Services.AddOptions() .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.AddScannerCache(builder.Configuration); builder.Services.AddSingleton(); builder.Services.AddHttpContextAccessor(); builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); builder.Services.AddSingleton(sp => sp.GetRequiredService()); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddStellaOpsCrypto(); builder.Services.AddBouncyCastleEd25519Provider(); builder.Services.AddSingleton(); if (bootstrapOptions.Events is { Enabled: true } eventsOptions && string.Equals(eventsOptions.Driver, "redis", StringComparison.OrdinalIgnoreCase)) { builder.Services.AddSingleton(); } else { builder.Services.AddSingleton(); } builder.Services.AddSingleton(); var pluginHostOptions = ScannerPluginHostFactory.Build(bootstrapOptions, contentRoot); builder.Services.RegisterPluginRoutines(builder.Configuration, pluginHostOptions); builder.Services.AddOpenApiIfAvailable(); 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("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>().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(); var error = feature?.Error; var extensions = new Dictionary(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(resolvedOptions.Api.ScansSegment); if (resolvedOptions.Features.EnablePolicyPreview) { apiGroup.MapPolicyEndpoints(resolvedOptions.Api.PolicySegment); } apiGroup.MapReportEndpoints(resolvedOptions.Api.ReportsSegment); app.MapOpenApiIfAvailable(); await app.RunAsync().ConfigureAwait(false);