Files
git.stella-ops.org/src/Remediation/StellaOps.Remediation.WebService/Program.cs

213 lines
8.7 KiB
C#

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<IContributorTrustScorer, ContributorTrustScorer>();
builder.Services.AddSingleton<IRemediationVerifier, RemediationVerifier>();
RegisterPersistence(builder.Services, builder.Configuration, builder.Environment, storageDriver);
// Registry/matcher: compose from repositories.
builder.Services.AddSingleton<IRemediationRegistry, RepositoryBackedRemediationRegistry>();
builder.Services.AddSingleton<IRemediationMatcher, RepositoryBackedRemediationMatcher>();
var routerEnabled = builder.Services.AddRouterMicroservice(
builder.Configuration,
serviceName: "remediation",
version: System.Reflection.CustomAttributeExtensions.GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>(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<PostgresOptions>(options =>
{
options.ConnectionString = connectionString;
options.SchemaName = schemaName;
});
services.AddSingleton<RemediationDataSource>();
services.AddSingleton<IFixTemplateRepository, PostgresFixTemplateRepository>();
services.AddSingleton<IPrSubmissionRepository, PostgresPrSubmissionRepository>();
services.AddSingleton<IMarketplaceSourceRepository, PostgresMarketplaceSourceRepository>();
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<IFixTemplateRepository>(_ => new PostgresFixTemplateRepository());
services.AddSingleton<IPrSubmissionRepository>(_ => new PostgresPrSubmissionRepository());
services.AddSingleton<IMarketplaceSourceRepository>(_ => 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 { }
/// <summary>
/// Repository-backed registry implementation composed from repositories.
/// </summary>
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<IReadOnlyList<StellaOps.Remediation.Core.Models.FixTemplate>> ListTemplatesAsync(string? cveId, string? purl, int limit, int offset, CancellationToken ct)
=> _templates.ListAsync(cveId, purl, limit, offset, ct);
public Task<StellaOps.Remediation.Core.Models.FixTemplate?> GetTemplateAsync(Guid id, CancellationToken ct)
=> _templates.GetByIdAsync(id, ct);
public Task<StellaOps.Remediation.Core.Models.FixTemplate> CreateTemplateAsync(StellaOps.Remediation.Core.Models.FixTemplate template, CancellationToken ct)
=> _templates.InsertAsync(template, ct);
public Task<IReadOnlyList<StellaOps.Remediation.Core.Models.PrSubmission>> ListSubmissionsAsync(string? cveId, string? status, int limit, int offset, CancellationToken ct)
=> _submissions.ListAsync(cveId, status, limit, offset, ct);
public Task<StellaOps.Remediation.Core.Models.PrSubmission?> GetSubmissionAsync(Guid id, CancellationToken ct)
=> _submissions.GetByIdAsync(id, ct);
public Task<StellaOps.Remediation.Core.Models.PrSubmission> 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);
}
/// <summary>
/// Repository-backed matcher implementation that delegates to template repository.
/// </summary>
internal sealed class RepositoryBackedRemediationMatcher : IRemediationMatcher
{
private readonly IFixTemplateRepository _templates;
public RepositoryBackedRemediationMatcher(IFixTemplateRepository templates)
{
_templates = templates;
}
public Task<IReadOnlyList<StellaOps.Remediation.Core.Models.FixTemplate>> FindMatchesAsync(string cveId, string? purl, string? version, CancellationToken ct)
=> _templates.FindMatchesAsync(cveId, purl, version, ct);
}