Backend (Scanner .NET): - New ScanPolicyEndpoints.cs with GET/POST/PUT/DELETE /api/v1/scan-policies - In-memory ConcurrentDictionary storage (no migration needed) - Auth: scanner:read for list, orch:operate for mutations - Registered in Scanner Program.cs Frontend (Angular): - New scan-policy.component.ts with table view, inline create/edit form, enable/disable toggle, dynamic rules (type/severity/action) - Route added at /security/scan-policies in security-risk.routes.ts Gateway route already exists in router-gateway-local.json. Sprint 002: all 7 tasks now DONE. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
848 lines
40 KiB
C#
848 lines
40 KiB
C#
|
|
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>(
|
|
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.AssemblyInformationalVersionAttribute>(System.Reflection.Assembly.GetExecutingAssembly())?.InformationalVersion ?? "1.0.0",
|
|
routerOptionsSection: "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.AddDeterminismDefaults();
|
|
builder.Services.AddScannerCache(builder.Configuration);
|
|
builder.Services.AddOptions<SliceCacheOptions>()
|
|
.Bind(builder.Configuration.GetSection("scanner:slices:cache"));
|
|
builder.Services.AddOptions<SliceQueryServiceOptions>()
|
|
.Bind(builder.Configuration.GetSection("scanner:slices:query"));
|
|
builder.Services.AddOptions<ReplayCommandServiceOptions>()
|
|
.Bind(builder.Configuration.GetSection("scanner:replayCommands"));
|
|
builder.Services.AddOptions<ReachabilityStackRepositoryOptions>()
|
|
.Bind(builder.Configuration.GetSection("scanner:reachabilityStack"));
|
|
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>();
|
|
|
|
// SARIF export services (Sprint: SPRINT_20260109_010_001)
|
|
builder.Services.AddSingleton<StellaOps.Scanner.Sarif.Rules.ISarifRuleRegistry, StellaOps.Scanner.Sarif.Rules.SarifRuleRegistry>();
|
|
builder.Services.AddSingleton<StellaOps.Scanner.Sarif.Fingerprints.IFingerprintGenerator, StellaOps.Scanner.Sarif.Fingerprints.FingerprintGenerator>();
|
|
builder.Services.AddSingleton<StellaOps.Scanner.Sarif.ISarifExportService, StellaOps.Scanner.Sarif.SarifExportService>();
|
|
builder.Services.AddSingleton<ISarifExportService, ScanFindingsSarifExportService>();
|
|
|
|
builder.Services.AddSingleton<ICycloneDxExportService, NullCycloneDxExportService>();
|
|
builder.Services.AddSingleton<IOpenVexExportService, NullOpenVexExportService>();
|
|
builder.Services.AddSingleton<ISpdxComposer, SpdxComposer>();
|
|
builder.Services.AddSingleton<ISbomExportService, SbomExportService>();
|
|
|
|
// GitHub Code Scanning integration (Sprint: SPRINT_20260109_010_002)
|
|
builder.Services.AddSingleton<IGitHubCodeScanningService, NullGitHubCodeScanningService>();
|
|
|
|
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.AddScoped<ISbomHotLookupService, SbomHotLookupService>();
|
|
builder.Services.AddScoped<ILayerSbomService, LayerSbomService>();
|
|
builder.Services.AddSingleton<ISbomUploadStore, InMemorySbomUploadStore>();
|
|
builder.Services.AddScoped<ISbomByosUploadService, SbomByosUploadService>();
|
|
builder.Services.AddSingleton<ILayerSbomService, LayerSbomService>(); // Sprint: SPRINT_20260106_003_001
|
|
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.TryAddSingleton<IVexGateResultsStore, InMemoryVexGateResultsStore>();
|
|
builder.Services.TryAddSingleton<IVexGateQueryService, VexGateQueryService>();
|
|
builder.Services.TryAddSingleton<IVexReachabilityDecisionFilter, VexReachabilityDecisionFilter>();
|
|
builder.Services.TryAddSingleton<IMaterialRiskChangeRepository, PostgresMaterialRiskChangeRepository>();
|
|
builder.Services.TryAddSingleton<IVexCandidateStore, PostgresVexCandidateStore>();
|
|
builder.Services.TryAddSingleton<IScanMetadataRepository, InMemoryScanMetadataRepository>();
|
|
builder.Services.TryAddSingleton<ISliceCache, SliceCache>();
|
|
builder.Services.TryAddSingleton<VerdictComputer>();
|
|
builder.Services.TryAddSingleton<SliceExtractor>();
|
|
builder.Services.TryAddSingleton<SliceHasher>();
|
|
builder.Services.TryAddSingleton<StellaOps.Scanner.Reachability.Slices.Replay.SliceDiffComputer>();
|
|
builder.Services.TryAddSingleton<SliceDsseSigner>();
|
|
builder.Services.TryAddSingleton<SliceCasStorage>();
|
|
builder.Services.TryAddScoped<ISliceQueryService, SliceQueryService>();
|
|
builder.Services.TryAddScoped<IReplayCommandService, ReplayCommandService>();
|
|
|
|
var reachabilityStackRepositoryOptions = builder.Configuration
|
|
.GetSection("scanner:reachabilityStack")
|
|
.Get<ReachabilityStackRepositoryOptions>() ?? new ReachabilityStackRepositoryOptions();
|
|
|
|
if (reachabilityStackRepositoryOptions.Enabled)
|
|
{
|
|
builder.Services.TryAddSingleton<IReachabilityStackRepository, FileBackedReachabilityStackRepository>();
|
|
}
|
|
|
|
// Secret Detection Settings (Sprint: SPRINT_20260104_006_BE)
|
|
builder.Services.AddScoped<ISecretDetectionSettingsService, SecretDetectionSettingsService>();
|
|
builder.Services.AddScoped<ISecretExceptionPatternService, SecretExceptionPatternService>();
|
|
|
|
builder.Services.AddDbContext<TriageDbContext>(options =>
|
|
options.UseNpgsql(bootstrapOptions.Storage.Dsn, npgsqlOptions =>
|
|
{
|
|
npgsqlOptions.MapEnum<TriageLane>();
|
|
npgsqlOptions.MapEnum<TriageVerdict>();
|
|
npgsqlOptions.MapEnum<TriageReachability>();
|
|
npgsqlOptions.MapEnum<TriageVexStatus>();
|
|
npgsqlOptions.MapEnum<TriageDecisionKind>();
|
|
npgsqlOptions.MapEnum<TriageSnapshotTrigger>();
|
|
npgsqlOptions.MapEnum<TriageEvidenceType>();
|
|
}));
|
|
builder.Services.AddScoped<ITriageQueryService, TriageQueryService>();
|
|
builder.Services.AddScoped<ITriageStatusService, TriageStatusService>();
|
|
builder.Services.AddScoped<IGatingReasonService, GatingReasonService>();
|
|
builder.Services.AddScoped<IUnifiedEvidenceService, UnifiedEvidenceService>();
|
|
builder.Services.AddScoped<IEvidenceBundleExporter, EvidenceBundleExporter>();
|
|
builder.Services.TryAddScoped<IFindingQueryService, FindingQueryService>();
|
|
builder.Services.TryAddSingleton<IExploitPathGroupingService, ExploitPathGroupingService>();
|
|
builder.Services.AddScoped<IUnknownsQueryService, UnknownsQueryService>();
|
|
|
|
// Verdict rationale rendering (Sprint: SPRINT_20260106_001_001_LB_verdict_rationale_renderer)
|
|
builder.Services.AddVerdictExplainability();
|
|
builder.Services.AddScoped<IFindingRationaleService, FindingRationaleService>();
|
|
|
|
// 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.AddHttpClient("ScannerOciAttestationPublisher")
|
|
.ConfigurePrimaryHttpMessageHandler(() =>
|
|
{
|
|
if (!bootstrapOptions.ArtifactStore.AllowInsecureTls)
|
|
{
|
|
return new HttpClientHandler();
|
|
}
|
|
|
|
return new HttpClientHandler
|
|
{
|
|
ServerCertificateCustomValidationCallback =
|
|
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
|
};
|
|
});
|
|
builder.Services.TryAddSingleton(sp =>
|
|
{
|
|
var options = sp.GetRequiredService<IOptions<ScannerWebServiceOptions>>().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<IHttpClientFactory>().CreateClient("ScannerOciAttestationPublisher");
|
|
httpClient.Timeout = TimeSpan.FromSeconds(Math.Max(1, options.AttestationAttachment.RegistryTimeoutSeconds));
|
|
|
|
return new OciArtifactPusher(
|
|
httpClient,
|
|
sp.GetRequiredService<StellaOps.Cryptography.ICryptoHash>(),
|
|
registryOptions,
|
|
sp.GetRequiredService<ILogger<OciArtifactPusher>>(),
|
|
sp.GetService<TimeProvider>());
|
|
});
|
|
builder.Services.TryAddSingleton<IOciAttestationPublisher>(sp =>
|
|
{
|
|
var options = sp.GetRequiredService<IOptions<ScannerWebServiceOptions>>().Value;
|
|
if (!options.AttestationAttachment.AutoAttach)
|
|
{
|
|
return NullOciAttestationPublisher.Instance;
|
|
}
|
|
|
|
return ActivatorUtilities.CreateInstance<OciAttestationPublisher>(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<PostgresOptions>()
|
|
.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<ScannerSourcesDataSource>();
|
|
builder.Services.AddSbomSources();
|
|
builder.Services.AddSbomSourceCredentialResolver<NullCredentialResolver>();
|
|
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);
|
|
|
|
// Read collections directly from IConfiguration to work around
|
|
// .NET Configuration.Bind() not populating IList<string> in nested init objects.
|
|
var authoritySection = builder.Configuration.GetSection("scanner:Authority");
|
|
|
|
var audiences = authoritySection.GetSection("Audiences").Get<string[]>() ?? [];
|
|
resourceOptions.Audiences.Clear();
|
|
foreach (var audience in audiences)
|
|
{
|
|
resourceOptions.Audiences.Add(audience);
|
|
}
|
|
|
|
var requiredScopes = authoritySection.GetSection("RequiredScopes").Get<string[]>() ?? [];
|
|
resourceOptions.RequiredScopes.Clear();
|
|
foreach (var scope in requiredScopes)
|
|
{
|
|
resourceOptions.RequiredScopes.Add(scope);
|
|
}
|
|
|
|
var bypassNetworks = authoritySection.GetSection("BypassNetworks").Get<string[]>() ?? [];
|
|
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<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.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<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();
|
|
});
|
|
|
|
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<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;
|
|
if (error is not null)
|
|
{
|
|
app.Logger.LogError(error, "Unhandled exception.");
|
|
}
|
|
|
|
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.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<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;
|
|
}
|
|
}
|
|
|
|
|