Files
git.stella-ops.org/src/Scanner/StellaOps.Scanner.WebService/Program.cs
StellaOps Bot f46bde5575 save progress
2026-01-02 15:52:55 +02:00

597 lines
27 KiB
C#

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.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.Authority.Persistence.Postgres.Repositories;
using StellaOps.Configuration;
using StellaOps.Plugin.DependencyInjection;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Policy;
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.ReachabilityDrift.DependencyInjection;
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.WebService.Diagnostics;
using StellaOps.Scanner.WebService.Determinism;
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;
using StellaOps.Scanner.WebService.Replay;
using StellaOps.Scanner.WebService.Middleware;
using StellaOps.Scanner.Storage;
using StellaOps.Scanner.Storage.Extensions;
using StellaOps.Router.AspNet;
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.AddStellaOpsCrypto(bootstrapOptions.Crypto);
builder.Services.AddControllers();
// Stella Router integration - enables ASP.NET endpoints to be registered with the Router
builder.Services.TryAddStellaRouter(
serviceName: "scanner",
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
routerOptions: bootstrapOptions.Router);
builder.Services.AddOptions<ScannerWebServiceOptions>()
.Bind(builder.Configuration.GetSection(ScannerWebServiceOptions.SectionName))
.PostConfigure(options =>
{
ScannerWebServiceOptionsPostConfigure.Apply(options, contentRoot);
ScannerWebServiceOptionsValidator.Validate(options);
})
.ValidateOnStart();
builder.Services.AddSingleton<IValidateOptions<OfflineKitOptions>, OfflineKitOptionsValidator>();
builder.Services.AddOptions<OfflineKitOptions>()
.Bind(builder.Configuration.GetSection(OfflineKitOptions.SectionName))
.ValidateOnStart();
builder.Services.AddSingleton<IPublicKeyLoader, FileSystemPublicKeyLoader>();
builder.Services.AddSingleton<ITrustAnchorRegistry, TrustAnchorRegistry>();
builder.Services.TryAddScoped<IOfflineKitAuditEmitter, NullOfflineKitAuditEmitter>();
builder.Services.AddSingleton<OfflineKitMetricsStore>();
builder.Services.AddSingleton<OfflineKitStateStore>();
builder.Services.AddScoped<OfflineKitImportService>();
builder.Services.AddScoped<OfflineKitManifestService>();
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<TimeProvider>(_ => new DeterministicTimeProvider(bootstrapOptions.Determinism.FixedInstantUtc));
}
else
{
builder.Services.AddSingleton(TimeProvider.System);
}
builder.Services.AddScannerCache(builder.Configuration);
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>();
builder.Services.AddSingleton<IReachabilityComputeService, NullReachabilityComputeService>();
builder.Services.AddSingleton<IReachabilityQueryService, NullReachabilityQueryService>();
builder.Services.AddSingleton<IReachabilityExplainService, NullReachabilityExplainService>();
builder.Services.AddSingleton<ISarifExportService, NullSarifExportService>();
builder.Services.AddSingleton<ICycloneDxExportService, NullCycloneDxExportService>();
builder.Services.AddSingleton<IOpenVexExportService, NullOpenVexExportService>();
builder.Services.AddSingleton<IEvidenceCompositionService, EvidenceCompositionService>();
builder.Services.AddSingleton<IPolicyDecisionAttestationService, PolicyDecisionAttestationService>();
builder.Services.AddSingleton<IRichGraphAttestationService, RichGraphAttestationService>();
builder.Services.AddSingleton<IAttestationChainVerifier, AttestationChainVerifier>();
builder.Services.AddSingleton<IHumanApprovalAttestationService, HumanApprovalAttestationService>();
builder.Services.AddScoped<ICallGraphIngestionService, CallGraphIngestionService>();
builder.Services.AddScoped<ISbomIngestionService, SbomIngestionService>();
builder.Services.AddSingleton<ISbomUploadStore, InMemorySbomUploadStore>();
builder.Services.AddScoped<ISbomByosUploadService, SbomByosUploadService>();
builder.Services.AddSingleton<IPolicySnapshotRepository, InMemoryPolicySnapshotRepository>();
builder.Services.AddSingleton<IPolicyAuditRepository, InMemoryPolicyAuditRepository>();
builder.Services.AddSingleton<PolicySnapshotStore>();
builder.Services.AddSingleton<PolicyPreviewService>();
builder.Services.AddSingleton<IRecordModeService, RecordModeService>();
builder.Services.AddSingleton<IScoreReplayService, ScoreReplayService>();
builder.Services.AddSingleton<IScanManifestRepository, InMemoryScanManifestRepository>();
builder.Services.AddSingleton<IProofBundleRepository, InMemoryProofBundleRepository>();
builder.Services.AddSingleton<IScoringService, DeterministicScoringService>();
builder.Services.AddSingleton<IScanManifestSigner, ScanManifestSigner>();
builder.Services.AddSingleton<IDeltaCompareService, DeltaCompareService>();
builder.Services.AddSingleton<IBaselineService, BaselineService>();
builder.Services.AddSingleton<IActionablesService, ActionablesService>();
builder.Services.AddSingleton<ICounterfactualApiService, CounterfactualApiService>();
builder.Services.AddDbContext<TriageDbContext>(options =>
options.UseNpgsql(bootstrapOptions.Storage.Dsn));
builder.Services.AddScoped<ITriageQueryService, TriageQueryService>();
// Register Storage.Repositories implementations for ManifestEndpoints
builder.Services.AddSingleton<StellaOps.Scanner.Storage.Repositories.IScanManifestRepository, TestManifestRepository>();
builder.Services.AddSingleton<StellaOps.Scanner.Storage.Repositories.IProofBundleRepository, TestProofBundleRepository>();
builder.Services.AddSingleton<IProofBundleWriter>(sp =>
{
var options = sp.GetRequiredService<IOptions<ScannerWebServiceOptions>>().Value;
var hostEnvironment = sp.GetRequiredService<IHostEnvironment>();
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<IReportSigner, ReportSigner>();
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<IConfigureOptions<ScannerWebServiceOptions>, ScannerSurfaceSecretConfigurator>();
builder.Services.AddSingleton<IConfigureOptions<ScannerWebServiceOptions>, SurfaceFeatureFlagsConfigurator>();
builder.Services.AddSingleton<IConfigureOptions<SurfaceCacheOptions>>(sp =>
new SurfaceCacheOptionsConfigurator(sp.GetRequiredService<ISurfaceEnvironment>()));
builder.Services.AddSingleton<IConfigureOptions<SurfaceManifestStoreOptions>>(sp =>
new SurfaceManifestStoreOptionsConfigurator(
sp.GetRequiredService<ISurfaceEnvironment>(),
sp.GetRequiredService<IOptions<SurfaceCacheOptions>>()));
builder.Services.AddSingleton<ISurfacePointerService, SurfacePointerService>();
builder.Services.AddSingleton<IRedisConnectionFactory, RedisConnectionFactory>();
if (bootstrapOptions.Events is { Enabled: true } eventsOptions
&& string.Equals(eventsOptions.Driver, "redis", StringComparison.OrdinalIgnoreCase))
{
builder.Services.AddSingleton<IPlatformEventPublisher, RedisPlatformEventPublisher>();
}
else
{
builder.Services.AddSingleton<IPlatformEventPublisher, NullPlatformEventPublisher>();
}
builder.Services.AddSingleton<IReportEventDispatcher, ReportEventDispatcher>();
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.AddSingleton<IPostConfigureOptions<ScannerStorageOptions>, ScannerStorageOptionsPostConfigurator>();
builder.Services.AddOptions<StellaOps.Scanner.ProofSpine.Options.ProofSpineDsseSigningOptions>()
.Bind(builder.Configuration.GetSection(StellaOps.Scanner.ProofSpine.Options.ProofSpineDsseSigningOptions.SectionName));
builder.Services.AddSingleton<StellaOps.Scanner.ProofSpine.ICryptoProfile, StellaOps.Scanner.ProofSpine.DefaultCryptoProfile>();
builder.Services.AddSingleton<StellaOps.Scanner.ProofSpine.IDsseSigningService, StellaOps.Scanner.ProofSpine.HmacDsseSigningService>();
builder.Services.AddTransient<StellaOps.Scanner.ProofSpine.ProofSpineBuilder>();
builder.Services.AddSingleton<StellaOps.Scanner.ProofSpine.ProofSpineVerifier>();
builder.Services.AddSingleton<RuntimeEventRateLimiter>();
builder.Services.AddSingleton<IDeltaScanRequestHandler, DeltaScanRequestHandler>();
builder.Services.AddSingleton<IRuntimeEventIngestionService, RuntimeEventIngestionService>();
builder.Services.AddSingleton<IRuntimeInventoryReconciler, RuntimeInventoryReconciler>();
builder.Services.AddSingleton<IRuntimeAttestationVerifier, RuntimeAttestationVerifier>();
builder.Services.AddSingleton<ILinksetResolver, LinksetResolver>();
builder.Services.AddSingleton<IRuntimePolicyService, RuntimePolicyService>();
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);
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.ScansWrite, ScannerAuthorityScopes.ScansWrite);
options.AddStellaOpsScopePolicy(ScannerPolicies.ScansApprove, ScannerAuthorityScopes.ScansWrite);
options.AddStellaOpsScopePolicy(ScannerPolicies.Reports, ScannerAuthorityScopes.ReportsRead);
options.AddStellaOpsScopePolicy(ScannerPolicies.RuntimeIngest, ScannerAuthorityScopes.RuntimeIngest);
options.AddStellaOpsScopePolicy(ScannerPolicies.CallGraphIngest, ScannerAuthorityScopes.CallGraphIngest);
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<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.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.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<EvidenceCompositionOptions>(builder.Configuration.GetSection("EvidenceComposition"));
// Concelier Linkset integration for advisory enrichment
builder.Services.Configure<ConcelierLinksetOptions>(builder.Configuration.GetSection(ConcelierLinksetOptions.SectionName));
builder.Services.AddHttpClient<ConcelierHttpLinksetQueryService>((sp, client) =>
{
var options = sp.GetRequiredService<IOptions<ConcelierLinksetOptions>>().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<IAdvisoryLinksetQueryService>(sp =>
{
var options = sp.GetRequiredService<IOptions<ConcelierLinksetOptions>>().Value;
if (options.Enabled && !string.IsNullOrWhiteSpace(options.BaseUrl))
{
return sp.GetRequiredService<ConcelierHttpLinksetQueryService>();
}
return new NullAdvisoryLinksetQueryService();
});
var app = builder.Build();
// Fail fast if surface configuration is invalid at startup.
using (var validationScope = app.Services.CreateScope())
{
var services = validationScope.ServiceProvider;
var env = services.GetRequiredService<ISurfaceEnvironment>();
var runner = services.GetRequiredService<ISurfaceValidatorRunner>();
await runner.EnsureAsync(
SurfaceValidationContext.Create(services, "Scanner.WebService.Startup", env.Settings),
app.Lifetime.ApplicationStopping)
.ConfigureAwait(false);
}
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);
});
});
// Always add authentication and authorization middleware
// Even in anonymous mode, endpoints use RequireAuthorization() which needs the middleware
app.UseAuthentication();
app.UseAuthorization();
// Stella Router integration - enables request dispatch from Router to ASP.NET endpoints
app.TryUseStellaRouter(resolvedOptions.Router);
// Idempotency middleware (Sprint: SPRINT_3500_0002_0003)
app.UseIdempotency();
// Rate limiting for replay/manifest endpoints (Sprint: SPRINT_3500_0002_0003)
app.UseRateLimiter();
app.MapHealthEndpoints();
app.MapObservabilityEndpoints();
app.MapOfflineKitEndpoints();
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);
apiGroup.MapSbomUploadEndpoints();
apiGroup.MapReachabilityDriftRootEndpoints();
apiGroup.MapDeltaCompareEndpoints();
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.MapSliceEndpoints(); // Sprint: SPRINT_3820_0001_0001
if (resolvedOptions.Features.EnablePolicyPreview)
{
apiGroup.MapPolicyEndpoints(resolvedOptions.Api.PolicySegment);
}
apiGroup.MapReportEndpoints(resolvedOptions.Api.ReportsSegment);
apiGroup.MapRuntimeEndpoints(resolvedOptions.Api.RuntimeSegment);
app.MapControllers();
app.MapOpenApiIfAvailable();
// Refresh Router endpoint cache after all endpoints are registered
app.TryRefreshStellaRouterEndpoints(resolvedOptions.Router);
await app.RunAsync().ConfigureAwait(false);
// Make Program class file-scoped to prevent it from being exposed to referencing assemblies
file sealed partial class Program;
internal sealed class SurfaceCacheOptionsConfigurator : IConfigureOptions<SurfaceCacheOptions>
{
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;
}
}