using Microsoft.Extensions.DependencyInjection; using StellaOps.Auth.ServerIntegration; using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Infrastructure.Postgres.Options; using StellaOps.Localization; using StellaOps.Remediation.Core.Abstractions; using StellaOps.Remediation.Core.Services; using StellaOps.Remediation.Persistence.Postgres; using StellaOps.Remediation.Persistence.Repositories; using StellaOps.Remediation.WebService.Endpoints; using StellaOps.Router.AspNet; var builder = WebApplication.CreateBuilder(args); var storageDriver = ResolveStorageDriver(builder.Configuration, "Remediation"); builder.Services.AddProblemDetails(); builder.Services.AddHealthChecks(); builder.Services.AddRouting(options => options.LowercaseUrls = true); builder.Services.AddAuthorization(options => { options.AddPolicy("remediation.read", policy => policy.RequireAssertion(_ => true)); options.AddPolicy("remediation.submit", policy => policy.RequireAssertion(_ => true)); options.AddPolicy("remediation.manage", policy => policy.RequireAssertion(_ => true)); }); builder.Services.AddAuthentication(); builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); builder.Services.AddStellaOpsLocalization(builder.Configuration); builder.Services.AddTranslationBundle(System.Reflection.Assembly.GetExecutingAssembly()); // Core services builder.Services.AddSingleton(); builder.Services.AddSingleton(); RegisterPersistence(builder.Services, builder.Configuration, builder.Environment, storageDriver); // Registry/matcher: compose from repositories. builder.Services.AddSingleton(); builder.Services.AddSingleton(); var routerEnabled = builder.Services.AddRouterMicroservice( builder.Configuration, serviceName: "remediation", version: System.Reflection.CustomAttributeExtensions.GetCustomAttribute(System.Reflection.Assembly.GetExecutingAssembly())?.InformationalVersion ?? "1.0.0", routerOptionsSection: "Router"); builder.TryAddStellaOpsLocalBinding("remediation"); var app = builder.Build(); app.LogStellaOpsLocalHostname("remediation"); app.UseAuthentication(); app.UseStellaOpsCors(); app.UseStellaOpsLocalization(); app.UseAuthorization(); app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); app.MapHealthChecks("/healthz").AllowAnonymous(); app.MapRemediationRegistryEndpoints(); app.MapRemediationMatchEndpoints(); app.MapRemediationSourceEndpoints(); await app.LoadTranslationsAsync(); app.TryRefreshStellaRouterEndpoints(routerEnabled); app.Run(); static void RegisterPersistence( IServiceCollection services, IConfiguration configuration, IHostEnvironment environment, string storageDriver) { if (string.Equals(storageDriver, "postgres", StringComparison.OrdinalIgnoreCase)) { var connectionString = ResolvePostgresConnectionString(configuration, "Remediation"); if (string.IsNullOrWhiteSpace(connectionString)) { throw new InvalidOperationException( "Remediation requires PostgreSQL connection settings when Storage:Driver=postgres. " + "Set ConnectionStrings:Default or Remediation:Storage:Postgres:ConnectionString."); } var schemaName = ResolveSchemaName(configuration, "Remediation") ?? RemediationDataSource.DefaultSchemaName; services.Configure(options => { options.ConnectionString = connectionString; options.SchemaName = schemaName; }); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); return; } if (string.Equals(storageDriver, "inmemory", StringComparison.OrdinalIgnoreCase)) { if (!IsTestEnvironment(environment)) { throw new InvalidOperationException( "Remediation in-memory storage driver is restricted to Test/Testing environments."); } services.AddSingleton(_ => new PostgresFixTemplateRepository()); services.AddSingleton(_ => new PostgresPrSubmissionRepository()); services.AddSingleton(_ => new PostgresMarketplaceSourceRepository()); return; } throw new InvalidOperationException( $"Unsupported Remediation storage driver '{storageDriver}'. Allowed values: postgres, inmemory."); } static bool IsTestEnvironment(IHostEnvironment environment) { return environment.IsEnvironment("Test") || environment.IsEnvironment("Testing"); } static string ResolveStorageDriver(IConfiguration configuration, string serviceName) { return FirstNonEmpty( configuration[$"{serviceName}:Storage:Driver"], configuration["Storage:Driver"]) ?? "postgres"; } static string? ResolveSchemaName(IConfiguration configuration, string serviceName) { return FirstNonEmpty( configuration[$"{serviceName}:Storage:Postgres:SchemaName"], configuration["Storage:Postgres:SchemaName"], configuration[$"Postgres:{serviceName}:SchemaName"]); } static string? ResolvePostgresConnectionString(IConfiguration configuration, string serviceName) { return FirstNonEmpty( configuration[$"{serviceName}:Storage:Postgres:ConnectionString"], configuration["Storage:Postgres:ConnectionString"], configuration[$"Postgres:{serviceName}:ConnectionString"], configuration[$"ConnectionStrings:{serviceName}"], configuration["ConnectionStrings:Default"]); } static string? FirstNonEmpty(params string?[] values) { foreach (var value in values) { if (!string.IsNullOrWhiteSpace(value)) { return value; } } return null; } public partial class Program { } /// /// Repository-backed registry implementation composed from repositories. /// internal sealed class RepositoryBackedRemediationRegistry : IRemediationRegistry { private readonly IFixTemplateRepository _templates; private readonly IPrSubmissionRepository _submissions; public RepositoryBackedRemediationRegistry(IFixTemplateRepository templates, IPrSubmissionRepository submissions) { _templates = templates; _submissions = submissions; } public Task> ListTemplatesAsync(string? cveId, string? purl, int limit, int offset, CancellationToken ct) => _templates.ListAsync(cveId, purl, limit, offset, ct); public Task GetTemplateAsync(Guid id, CancellationToken ct) => _templates.GetByIdAsync(id, ct); public Task CreateTemplateAsync(StellaOps.Remediation.Core.Models.FixTemplate template, CancellationToken ct) => _templates.InsertAsync(template, ct); public Task> ListSubmissionsAsync(string? cveId, string? status, int limit, int offset, CancellationToken ct) => _submissions.ListAsync(cveId, status, limit, offset, ct); public Task GetSubmissionAsync(Guid id, CancellationToken ct) => _submissions.GetByIdAsync(id, ct); public Task CreateSubmissionAsync(StellaOps.Remediation.Core.Models.PrSubmission submission, CancellationToken ct) => _submissions.InsertAsync(submission, ct); public Task UpdateSubmissionStatusAsync(Guid id, string status, string? verdict, CancellationToken ct) => _submissions.UpdateStatusAsync(id, status, verdict, ct); } /// /// Repository-backed matcher implementation that delegates to template repository. /// internal sealed class RepositoryBackedRemediationMatcher : IRemediationMatcher { private readonly IFixTemplateRepository _templates; public RepositoryBackedRemediationMatcher(IFixTemplateRepository templates) { _templates = templates; } public Task> FindMatchesAsync(string cveId, string? purl, string? version, CancellationToken ct) => _templates.FindMatchesAsync(cveId, purl, version, ct); }