using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Serilog; using Serilog.Events; using StellaOps.Auth.Abstractions; using StellaOps.Auth.Client; using StellaOps.Auth.ServerIntegration; using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Authority.Persistence.Postgres.Repositories; using StellaOps.Concelier.Core.Linksets; using StellaOps.Configuration; using StellaOps.Cryptography.DependencyInjection; using StellaOps.Cryptography.Plugin.BouncyCastle; using StellaOps.Determinism; using StellaOps.Infrastructure.Postgres.Options; using StellaOps.Localization; using StellaOps.Plugin.DependencyInjection; using StellaOps.Policy; using StellaOps.Policy.Explainability; using StellaOps.Router.AspNet; using StellaOps.Scanner.Cache; using StellaOps.Scanner.Core; using StellaOps.Scanner.Core.Configuration; using StellaOps.Scanner.Core.Contracts; using StellaOps.Scanner.Core.TrustAnchors; using StellaOps.Scanner.Emit.Composition; using StellaOps.Scanner.Gate; using StellaOps.Scanner.ReachabilityDrift.DependencyInjection; using StellaOps.Scanner.Reachability.Slices; using StellaOps.Scanner.SmartDiff.Detection; using StellaOps.Scanner.Sources.DependencyInjection; using StellaOps.Scanner.Sources.Persistence; using StellaOps.Scanner.Storage; using StellaOps.Scanner.Storage.Extensions; using StellaOps.Scanner.Storage.Oci; using StellaOps.Scanner.Storage.Postgres; using StellaOps.Scanner.Surface.Env; using StellaOps.Scanner.Surface.FS; using StellaOps.Scanner.Surface.Secrets; using StellaOps.Scanner.Surface.Validation; using StellaOps.Scanner.Triage; using StellaOps.Scanner.Triage.Entities; using StellaOps.Scanner.Triage.Services; using StellaOps.Scanner.WebService.Determinism; using StellaOps.Scanner.WebService.Diagnostics; using StellaOps.Scanner.WebService.Endpoints; using StellaOps.Scanner.WebService.Endpoints.Triage; using StellaOps.Scanner.WebService.Extensions; using StellaOps.Scanner.WebService.Hosting; using StellaOps.Scanner.WebService.Middleware; using StellaOps.Scanner.WebService.Options; using StellaOps.Scanner.WebService.Replay; using StellaOps.Scanner.WebService.Security; using StellaOps.Scanner.WebService.Services; using System.Collections.Generic; using System.Diagnostics; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; 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.AddStellaOpsCrypto(bootstrapOptions.Crypto); builder.Services.AddControllers(); // Stella Router integration - enables ASP.NET endpoints to be registered with the Router var routerEnabled = builder.Services.AddRouterMicroservice( builder.Configuration, serviceName: "scanner", version: System.Reflection.CustomAttributeExtensions.GetCustomAttribute(System.Reflection.Assembly.GetExecutingAssembly())?.InformationalVersion ?? "1.0.0", routerOptionsSection: "Router"); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(ScannerWebServiceOptions.SectionName)) .PostConfigure(options => { ScannerWebServiceOptionsPostConfigure.Apply(options, contentRoot); ScannerWebServiceOptionsValidator.Validate(options); }) .ValidateOnStart(); builder.Services.AddSingleton, OfflineKitOptionsValidator>(); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(OfflineKitOptions.SectionName)) .ValidateOnStart(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.TryAddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Host.UseSerilog((context, services, loggerConfiguration) => { loggerConfiguration .MinimumLevel.Information() .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) .Enrich.FromLogContext() .WriteTo.Console(); }); if (bootstrapOptions.Determinism.FixedClock) { builder.Services.AddSingleton(_ => new DeterministicTimeProvider(bootstrapOptions.Determinism.FixedInstantUtc)); } else { builder.Services.AddSingleton(TimeProvider.System); } builder.Services.AddDeterminismDefaults(); builder.Services.AddScannerCache(builder.Configuration); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection("scanner:slices:cache")); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection("scanner:slices:query")); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection("scanner:replayCommands")); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection("scanner:reachabilityStack")); 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(); // SARIF export services (Sprint: SPRINT_20260109_010_001) builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // GitHub Code Scanning integration (Sprint: SPRINT_20260109_010_002) builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddSingleton(); // Sprint: SPRINT_20260106_003_001 builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddScoped(); builder.Services.TryAddScoped(); var reachabilityStackRepositoryOptions = builder.Configuration .GetSection("scanner:reachabilityStack") .Get() ?? new ReachabilityStackRepositoryOptions(); if (reachabilityStackRepositoryOptions.Enabled) { builder.Services.TryAddSingleton(); } // Secret Detection Settings (Sprint: SPRINT_20260104_006_BE) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddDbContext(options => options.UseNpgsql(bootstrapOptions.Storage.Dsn, npgsqlOptions => { npgsqlOptions.MapEnum(); npgsqlOptions.MapEnum(); npgsqlOptions.MapEnum(); npgsqlOptions.MapEnum(); npgsqlOptions.MapEnum(); npgsqlOptions.MapEnum(); npgsqlOptions.MapEnum(); })); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.TryAddScoped(); builder.Services.TryAddSingleton(); builder.Services.AddScoped(); // Verdict rationale rendering (Sprint: SPRINT_20260106_001_001_LB_verdict_rationale_renderer) builder.Services.AddVerdictExplainability(); builder.Services.AddScoped(); // Register Storage.Repositories implementations for ManifestEndpoints builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => { var options = sp.GetRequiredService>().Value; var hostEnvironment = sp.GetRequiredService(); var configuredPath = options.ScoreReplay.BundleStoragePath?.Trim() ?? string.Empty; var defaultPath = hostEnvironment.IsEnvironment("Testing") ? Path.Combine(Path.GetTempPath(), "stellaops-proofs-testing") : Path.Combine(Path.GetTempPath(), "stellaops-proofs"); return new ProofBundleWriter(new ProofBundleWriterOptions { StorageBasePath = string.IsNullOrWhiteSpace(configuredPath) ? defaultPath : configuredPath, ContentAddressed = true }); }); builder.Services.AddReachabilityDrift(); builder.Services.AddStellaOpsCrypto(); builder.Services.AddBouncyCastleEd25519Provider(); builder.Services.AddSingleton(); builder.Services.AddSurfaceEnvironment(options => { options.ComponentName = "Scanner.WebService"; options.AddPrefix("SCANNER"); }); builder.Services.AddSurfaceValidation(); builder.Services.AddSurfaceFileCache(); builder.Services.AddSurfaceManifestStore(); builder.Services.AddSurfaceSecrets(); builder.Services.AddSingleton, ScannerSurfaceSecretConfigurator>(); builder.Services.AddSingleton, SurfaceFeatureFlagsConfigurator>(); builder.Services.AddSingleton>(sp => new SurfaceCacheOptionsConfigurator(sp.GetRequiredService())); builder.Services.AddSingleton>(sp => new SurfaceManifestStoreOptionsConfigurator( sp.GetRequiredService(), sp.GetRequiredService>())); builder.Services.AddSingleton(); 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(); builder.Services.AddHttpClient("ScannerOciAttestationPublisher") .ConfigurePrimaryHttpMessageHandler(() => { if (!bootstrapOptions.ArtifactStore.AllowInsecureTls) { return new HttpClientHandler(); } return new HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }; }); builder.Services.TryAddSingleton(sp => { var options = sp.GetRequiredService>().Value; var defaultRegistry = string.IsNullOrWhiteSpace(options.Registry.DefaultRegistry) ? "docker.io" : options.Registry.DefaultRegistry!.Trim(); var authOptions = new OciRegistryAuthOptions(); var credential = options.Registry.Credentials .FirstOrDefault(c => string.Equals(c.Registry?.Trim(), defaultRegistry, StringComparison.OrdinalIgnoreCase)) ?? options.Registry.Credentials.FirstOrDefault(); if (credential is not null) { authOptions.Username = credential.Username; authOptions.Password = credential.Password; authOptions.Token = credential.RegistryToken ?? credential.IdentityToken; authOptions.AllowAnonymousFallback = string.IsNullOrWhiteSpace(authOptions.Username) && string.IsNullOrWhiteSpace(authOptions.Token); } var registryOptions = new OciRegistryOptions { DefaultRegistry = defaultRegistry, AllowInsecure = bootstrapOptions.ArtifactStore.AllowInsecureTls, Auth = authOptions }; var httpClient = sp.GetRequiredService().CreateClient("ScannerOciAttestationPublisher"); httpClient.Timeout = TimeSpan.FromSeconds(Math.Max(1, options.AttestationAttachment.RegistryTimeoutSeconds)); return new OciArtifactPusher( httpClient, sp.GetRequiredService(), registryOptions, sp.GetRequiredService>(), sp.GetService()); }); builder.Services.TryAddSingleton(sp => { var options = sp.GetRequiredService>().Value; if (!options.AttestationAttachment.AutoAttach) { return NullOciAttestationPublisher.Instance; } return ActivatorUtilities.CreateInstance(sp); }); builder.Services.AddScannerStorage(storageOptions => { storageOptions.Postgres.ConnectionString = bootstrapOptions.Storage.Dsn; storageOptions.Postgres.SchemaName = string.IsNullOrWhiteSpace(bootstrapOptions.Storage.Database) ? ScannerStorageDefaults.DefaultSchemaName : bootstrapOptions.Storage.Database!.Trim(); storageOptions.Postgres.CommandTimeoutSeconds = bootstrapOptions.Storage.CommandTimeoutSeconds; storageOptions.Postgres.AutoMigrate = true; storageOptions.ObjectStore.Headers.Clear(); foreach (var header in bootstrapOptions.ArtifactStore.Headers) { storageOptions.ObjectStore.Headers[header.Key] = header.Value; } if (!string.IsNullOrWhiteSpace(bootstrapOptions.ArtifactStore.Bucket)) { storageOptions.ObjectStore.BucketName = bootstrapOptions.ArtifactStore.Bucket; } if (!string.IsNullOrWhiteSpace(bootstrapOptions.ArtifactStore.RootPrefix)) { storageOptions.ObjectStore.RootPrefix = bootstrapOptions.ArtifactStore.RootPrefix; } var artifactDriver = bootstrapOptions.ArtifactStore.Driver?.Trim() ?? string.Empty; if (string.Equals(artifactDriver, ScannerStorageDefaults.ObjectStoreProviders.RustFs, StringComparison.OrdinalIgnoreCase)) { storageOptions.ObjectStore.Driver = ScannerStorageDefaults.ObjectStoreProviders.RustFs; storageOptions.ObjectStore.RustFs.BaseUrl = bootstrapOptions.ArtifactStore.Endpoint; storageOptions.ObjectStore.RustFs.AllowInsecureTls = bootstrapOptions.ArtifactStore.AllowInsecureTls; storageOptions.ObjectStore.RustFs.Timeout = TimeSpan.FromSeconds(Math.Max(1, bootstrapOptions.ArtifactStore.TimeoutSeconds)); storageOptions.ObjectStore.RustFs.ApiKey = bootstrapOptions.ArtifactStore.ApiKey; storageOptions.ObjectStore.RustFs.ApiKeyHeader = bootstrapOptions.ArtifactStore.ApiKeyHeader ?? string.Empty; storageOptions.ObjectStore.EnableObjectLock = false; storageOptions.ObjectStore.ComplianceRetention = null; } else { var resolvedDriver = string.Equals(artifactDriver, ScannerStorageDefaults.ObjectStoreProviders.Minio, StringComparison.OrdinalIgnoreCase) ? ScannerStorageDefaults.ObjectStoreProviders.Minio : ScannerStorageDefaults.ObjectStoreProviders.S3; storageOptions.ObjectStore.Driver = resolvedDriver; if (!string.IsNullOrWhiteSpace(bootstrapOptions.ArtifactStore.Endpoint)) { storageOptions.ObjectStore.ServiceUrl = bootstrapOptions.ArtifactStore.Endpoint; } if (!string.IsNullOrWhiteSpace(bootstrapOptions.ArtifactStore.Region)) { storageOptions.ObjectStore.Region = bootstrapOptions.ArtifactStore.Region; } storageOptions.ObjectStore.EnableObjectLock = bootstrapOptions.ArtifactStore.EnableObjectLock; storageOptions.ObjectStore.ForcePathStyle = true; storageOptions.ObjectStore.ComplianceRetention = bootstrapOptions.ArtifactStore.EnableObjectLock ? TimeSpan.FromDays(Math.Max(1, bootstrapOptions.ArtifactStore.ObjectLockRetentionDays)) : null; storageOptions.ObjectStore.RustFs.ApiKey = null; storageOptions.ObjectStore.RustFs.ApiKeyHeader = string.Empty; storageOptions.ObjectStore.RustFs.BaseUrl = string.Empty; } }); builder.Services.AddOptions() .Configure(options => { options.ConnectionString = bootstrapOptions.Storage.Dsn; options.CommandTimeoutSeconds = bootstrapOptions.Storage.CommandTimeoutSeconds; options.SchemaName = string.IsNullOrWhiteSpace(bootstrapOptions.Storage.Database) ? ScannerStorageDefaults.DefaultSchemaName : bootstrapOptions.Storage.Database!.Trim(); options.AutoMigrate = false; options.MigrationsPath = null; }); builder.Services.TryAddSingleton(); builder.Services.AddSbomSources(); builder.Services.AddSbomSourceCredentialResolver(); builder.Services.AddSingleton, ScannerStorageOptionsPostConfigurator>(); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(StellaOps.Scanner.ProofSpine.Options.ProofSpineDsseSigningOptions.SectionName)); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); var pluginHostOptions = ScannerPluginHostFactory.Build(bootstrapOptions, contentRoot); builder.Services.RegisterPluginRoutines(builder.Configuration, pluginHostOptions); // Idempotency middleware (Sprint: SPRINT_3500_0002_0003) builder.Services.AddIdempotency(builder.Configuration); // Rate limiting for replay/manifest endpoints (Sprint: SPRINT_3500_0002_0003) builder.Services.AddScannerRateLimiting(); 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); // Read collections directly from IConfiguration to work around // .NET Configuration.Bind() not populating IList in nested init objects. var authoritySection = builder.Configuration.GetSection("scanner:Authority"); var audiences = authoritySection.GetSection("Audiences").Get() ?? []; resourceOptions.Audiences.Clear(); foreach (var audience in audiences) { resourceOptions.Audiences.Add(audience); } var requiredScopes = authoritySection.GetSection("RequiredScopes").Get() ?? []; resourceOptions.RequiredScopes.Clear(); foreach (var scope in requiredScopes) { resourceOptions.RequiredScopes.Add(scope); } var bypassNetworks = authoritySection.GetSection("BypassNetworks").Get() ?? []; resourceOptions.BypassNetworks.Clear(); foreach (var network in 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.ScansWrite, ScannerAuthorityScopes.ScansWrite); options.AddStellaOpsScopePolicy(ScannerPolicies.ScansApprove, ScannerAuthorityScopes.ScansApprove); options.AddStellaOpsScopePolicy(ScannerPolicies.Reports, ScannerAuthorityScopes.ReportsRead); options.AddStellaOpsScopePolicy(ScannerPolicies.RuntimeIngest, ScannerAuthorityScopes.RuntimeIngest); options.AddStellaOpsScopePolicy(ScannerPolicies.CallGraphIngest, ScannerAuthorityScopes.CallGraphIngest); options.AddStellaOpsScopePolicy(ScannerPolicies.TriageRead, ScannerAuthorityScopes.ScansRead); options.AddStellaOpsScopePolicy(ScannerPolicies.TriageWrite, ScannerAuthorityScopes.ScansWrite); options.AddStellaOpsScopePolicy(ScannerPolicies.Admin, ScannerAuthorityScopes.Admin); options.AddStellaOpsScopePolicy(ScannerPolicies.SourcesRead, ScannerAuthorityScopes.SourcesRead); options.AddStellaOpsScopePolicy(ScannerPolicies.SourcesWrite, ScannerAuthorityScopes.SourcesWrite); options.AddStellaOpsScopePolicy(ScannerPolicies.SourcesAdmin, ScannerAuthorityScopes.SourcesAdmin); options.AddStellaOpsScopePolicy(ScannerPolicies.SecretSettingsRead, ScannerAuthorityScopes.SecretSettingsRead); options.AddStellaOpsScopePolicy(ScannerPolicies.SecretSettingsWrite, ScannerAuthorityScopes.SecretSettingsWrite); options.AddStellaOpsScopePolicy(ScannerPolicies.SecretExceptionsRead, ScannerAuthorityScopes.SecretExceptionsRead); options.AddStellaOpsScopePolicy(ScannerPolicies.SecretExceptionsWrite, ScannerAuthorityScopes.SecretExceptionsWrite); options.AddStellaOpsScopePolicy(ScannerPolicies.OfflineKitImport, StellaOpsScopes.AirgapImport); options.AddStellaOpsScopePolicy(ScannerPolicies.OfflineKitStatusRead, StellaOpsScopes.AirgapStatusRead); options.AddStellaOpsScopePolicy(ScannerPolicies.OfflineKitManifestRead, StellaOpsScopes.AirgapStatusRead); options.AddStellaOpsScopePolicy(ScannerPolicies.OfflineKitValidate, StellaOpsScopes.AirgapImport); }); } 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.ScansWrite, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.ScansApprove, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.Reports, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.RuntimeIngest, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.CallGraphIngest, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.TriageRead, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.TriageWrite, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.Admin, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.SourcesRead, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.SourcesWrite, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.SourcesAdmin, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.SecretSettingsRead, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.SecretSettingsWrite, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.SecretExceptionsRead, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.SecretExceptionsWrite, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.OfflineKitImport, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.OfflineKitStatusRead, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.OfflineKitManifestRead, policy => policy.RequireAssertion(_ => true)); options.AddPolicy(ScannerPolicies.OfflineKitValidate, policy => policy.RequireAssertion(_ => true)); }); } // Evidence composition configuration builder.Services.Configure(builder.Configuration.GetSection("EvidenceComposition")); // Concelier Linkset integration for advisory enrichment builder.Services.Configure(builder.Configuration.GetSection(ConcelierLinksetOptions.SectionName)); builder.Services.AddHttpClient((sp, client) => { var options = sp.GetRequiredService>().Value; if (!string.IsNullOrWhiteSpace(options.BaseUrl)) { client.BaseAddress = new Uri(options.BaseUrl); } client.Timeout = TimeSpan.FromSeconds(Math.Max(1, options.TimeoutSeconds)); if (!string.IsNullOrWhiteSpace(options.ApiKey)) { var header = string.IsNullOrWhiteSpace(options.ApiKeyHeader) ? "Authorization" : options.ApiKeyHeader; client.DefaultRequestHeaders.TryAddWithoutValidation(header, options.ApiKey); } }) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AutomaticDecompression = System.Net.DecompressionMethods.All }); builder.Services.AddSingleton(sp => { var options = sp.GetRequiredService>().Value; if (options.Enabled && !string.IsNullOrWhiteSpace(options.BaseUrl)) { return sp.GetRequiredService(); } return new NullAdvisoryLinksetQueryService(); }); builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); builder.Services.AddStellaOpsLocalization(builder.Configuration, options => { options.DefaultLocale = string.IsNullOrWhiteSpace(options.DefaultLocale) ? "en-US" : options.DefaultLocale; if (options.SupportedLocales.Count == 0) { options.SupportedLocales.Add("en-US"); } if (!options.SupportedLocales.Contains("de-DE", StringComparer.OrdinalIgnoreCase)) { options.SupportedLocales.Add("de-DE"); } if (string.IsNullOrWhiteSpace(options.RemoteBundleUrl)) { var platformUrl = builder.Configuration["STELLAOPS_PLATFORM_URL"] ?? builder.Configuration["Platform:BaseUrl"]; if (!string.IsNullOrWhiteSpace(platformUrl)) { options.RemoteBundleUrl = platformUrl; } } options.EnableRemoteBundles = options.EnableRemoteBundles || !string.IsNullOrWhiteSpace(options.RemoteBundleUrl); }); builder.Services.AddTranslationBundle(System.Reflection.Assembly.GetExecutingAssembly()); builder.Services.AddRemoteTranslationBundles(); builder.TryAddStellaOpsLocalBinding("scanner"); var app = builder.Build(); app.LogStellaOpsLocalHostname("scanner"); // Fail fast if surface configuration is invalid at startup. using (var validationScope = app.Services.CreateScope()) { var services = validationScope.ServiceProvider; var env = services.GetRequiredService(); var runner = services.GetRequiredService(); await runner.EnsureAsync( SurfaceValidationContext.Create(services, "Scanner.WebService.Startup", env.Settings), app.Lifetime.ApplicationStopping) .ConfigureAwait(false); } 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; if (error is not null) { app.Logger.LogError(error, "Unhandled exception."); } 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); }); }); // Always add authentication and authorization middleware // Even in anonymous mode, endpoints use RequireAuthorization() which needs the middleware app.UseStellaOpsCors(); app.UseStellaOpsLocalization(); app.UseIdentityEnvelopeAuthentication(); app.UseAuthentication(); app.UseAuthorization(); app.UseStellaOpsTenantMiddleware(); // Stella Router integration - enables request dispatch from Router to ASP.NET endpoints app.TryUseStellaRouter(routerEnabled); // Idempotency middleware (Sprint: SPRINT_3500_0002_0003) app.UseIdempotency(); // Rate limiting for replay/manifest endpoints (Sprint: SPRINT_3500_0002_0003) app.UseRateLimiter(); await app.LoadTranslationsAsync(); app.MapHealthEndpoints(); app.MapObservabilityEndpoints(); app.MapOfflineKitEndpoints(); var apiGroup = app.MapGroup(resolvedOptions.Api.BasePath).RequireTenant(); if (app.Environment.IsEnvironment("Testing")) { apiGroup.MapGet("/__auth-probe", () => Results.Ok("ok")) .RequireAuthorization(ScannerPolicies.ScansEnqueue) .WithName("scanner.auth-probe"); } apiGroup.MapScanEndpoints(resolvedOptions.Api.ScansSegment); apiGroup.MapSourcesEndpoints(); apiGroup.MapWebhookEndpoints(); apiGroup.MapSbomUploadEndpoints(); apiGroup.MapReachabilityDriftRootEndpoints(); apiGroup.MapDeltaCompareEndpoints(); apiGroup.MapSmartDiffEndpoints(); apiGroup.MapBaselineEndpoints(); apiGroup.MapActionablesEndpoints(); apiGroup.MapCounterfactualEndpoints(); apiGroup.MapProofSpineEndpoints(resolvedOptions.Api.SpinesSegment, resolvedOptions.Api.ScansSegment); apiGroup.MapReplayEndpoints(); if (resolvedOptions.ScoreReplay.Enabled) { apiGroup.MapScoreReplayEndpoints(); } apiGroup.MapWitnessEndpoints(); // Sprint: SPRINT_3700_0001_0001 apiGroup.MapEpssEndpoints(); // Sprint: SPRINT_3410_0002_0001 apiGroup.MapTriageStatusEndpoints(); apiGroup.MapTriageInboxEndpoints(); apiGroup.MapBatchTriageEndpoints(); apiGroup.MapProofBundleEndpoints(); apiGroup.MapUnknownsEndpoints(); apiGroup.MapSecretDetectionSettingsEndpoints(); // Sprint: SPRINT_20260104_006_BE apiGroup.MapSecurityAdapterEndpoints(); // Pack v2 security adapter routes apiGroup.MapScanPolicyEndpoints(); // Sprint: S1-T03 Scan Policy CRUD if (resolvedOptions.Features.EnablePolicyPreview) { apiGroup.MapPolicyEndpoints(resolvedOptions.Api.PolicySegment); } apiGroup.MapReportEndpoints(resolvedOptions.Api.ReportsSegment); apiGroup.MapRuntimeEndpoints(resolvedOptions.Api.RuntimeSegment); apiGroup.MapReachabilityStackEndpoints(); app.MapControllers(); app.MapOpenApiIfAvailable(); app.MapSliceEndpoints(); // Sprint: SPRINT_3820_0001_0001 // Refresh Router endpoint cache after all endpoints are registered app.TryRefreshStellaRouterEndpoints(routerEnabled); await app.RunAsync().ConfigureAwait(false); // Expose Program class for WebApplicationFactory-based integration tests public partial class Program { } internal sealed class SurfaceCacheOptionsConfigurator : IConfigureOptions { private readonly ISurfaceEnvironment _surfaceEnvironment; public SurfaceCacheOptionsConfigurator(ISurfaceEnvironment surfaceEnvironment) { _surfaceEnvironment = surfaceEnvironment ?? throw new ArgumentNullException(nameof(surfaceEnvironment)); } public void Configure(SurfaceCacheOptions options) { ArgumentNullException.ThrowIfNull(options); var settings = _surfaceEnvironment.Settings; options.RootDirectory = settings.CacheRoot.FullName; } }