using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Security.Claims; using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using MongoDB.Bson; using MongoDB.Driver; using StellaOps.Concelier.Core.Events; using StellaOps.Concelier.Core.Jobs; using StellaOps.Concelier.Storage.Mongo; using StellaOps.Concelier.Core.Observations; using StellaOps.Concelier.Core.Linksets; using StellaOps.Concelier.WebService.Diagnostics; using Serilog; using StellaOps.Concelier.Merge; using StellaOps.Concelier.Merge.Services; using StellaOps.Concelier.WebService.Extensions; using StellaOps.Concelier.WebService.Jobs; using StellaOps.Concelier.WebService.Options; using StellaOps.Concelier.WebService.Filters; using StellaOps.Concelier.WebService.Services; using Serilog.Events; using StellaOps.Plugin.DependencyInjection; using StellaOps.Plugin.Hosting; using StellaOps.Configuration; using StellaOps.Auth.Abstractions; using StellaOps.Auth.Client; using StellaOps.Auth.ServerIntegration; using StellaOps.Aoc; using StellaOps.Aoc.AspNetCore.Routing; using StellaOps.Aoc.AspNetCore.Results; using StellaOps.Concelier.WebService.Contracts; using StellaOps.Concelier.Core.Aoc; using StellaOps.Concelier.Core.Raw; using StellaOps.Concelier.RawModels; var builder = WebApplication.CreateBuilder(args); const string JobsPolicyName = "Concelier.Jobs.Trigger"; const string ObservationsPolicyName = "Concelier.Observations.Read"; const string AdvisoryIngestPolicyName = "Concelier.Advisories.Ingest"; const string AdvisoryReadPolicyName = "Concelier.Advisories.Read"; const string AocVerifyPolicyName = "Concelier.Aoc.Verify"; const string TenantHeaderName = "X-Stella-Tenant"; builder.Configuration.AddStellaOpsDefaults(options => { options.BasePath = builder.Environment.ContentRootPath; options.EnvironmentPrefix = "CONCELIER_"; options.ConfigureBuilder = configurationBuilder => { configurationBuilder.AddConcelierYaml(Path.Combine(builder.Environment.ContentRootPath, "../etc/concelier.yaml")); }; }); var contentRootPath = builder.Environment.ContentRootPath; var concelierOptions = builder.Configuration.BindOptions(postConfigure: (opts, _) => { ConcelierOptionsPostConfigure.Apply(opts, contentRootPath); ConcelierOptionsValidator.Validate(opts); }); builder.Services.AddOptions() .Bind(builder.Configuration) .PostConfigure(options => { ConcelierOptionsPostConfigure.Apply(options, contentRootPath); ConcelierOptionsValidator.Validate(options); }) .ValidateOnStart(); builder.Services.AddStellaOpsCrypto(concelierOptions.Crypto); builder.ConfigureConcelierTelemetry(concelierOptions); builder.Services.TryAddSingleton(_ => TimeProvider.System); builder.Services.AddMemoryCache(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddMongoStorage(storageOptions => { storageOptions.ConnectionString = concelierOptions.Storage.Dsn; storageOptions.DatabaseName = concelierOptions.Storage.Database; storageOptions.CommandTimeout = TimeSpan.FromSeconds(concelierOptions.Storage.CommandTimeoutSeconds); }); builder.Services.AddConcelierAocGuards(); builder.Services.AddConcelierLinksetMappers(); builder.Services.AddAdvisoryRawServices(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); var features = concelierOptions.Features ?? new ConcelierOptions.FeaturesOptions(); if (!features.NoMergeEnabled) { #pragma warning disable CS0618, CONCELIER0001, CONCELIER0002 // Legacy merge service is intentionally supported behind a feature toggle. builder.Services.AddMergeModule(builder.Configuration); #pragma warning restore CS0618, CONCELIER0001, CONCELIER0002 } builder.Services.AddJobScheduler(); builder.Services.AddBuiltInConcelierJobs(); builder.Services.PostConfigure(options => { if (features.NoMergeEnabled) { options.Definitions.Remove("merge:reconcile"); return; } if (features.MergeJobAllowlist is { Count: > 0 }) { var allowMergeJob = features.MergeJobAllowlist.Any(value => string.Equals(value, "merge:reconcile", StringComparison.OrdinalIgnoreCase)); if (!allowMergeJob) { options.Definitions.Remove("merge:reconcile"); } } }); builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => new ServiceStatus(sp.GetRequiredService())); builder.Services.AddAocGuard(); var authorityConfigured = concelierOptions.Authority is { Enabled: true }; if (authorityConfigured) { builder.Services.AddStellaOpsAuthClient(clientOptions => { clientOptions.Authority = concelierOptions.Authority.Issuer; clientOptions.ClientId = concelierOptions.Authority.ClientId ?? string.Empty; clientOptions.ClientSecret = concelierOptions.Authority.ClientSecret; clientOptions.HttpTimeout = TimeSpan.FromSeconds(concelierOptions.Authority.BackchannelTimeoutSeconds); clientOptions.DefaultScopes.Clear(); foreach (var scope in concelierOptions.Authority.ClientScopes) { clientOptions.DefaultScopes.Add(scope); } var resilience = concelierOptions.Authority.Resilience ?? new ConcelierOptions.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; } }); if (string.IsNullOrWhiteSpace(concelierOptions.Authority.TestSigningSecret)) { builder.Services.AddStellaOpsResourceServerAuthentication( builder.Configuration, configurationSection: null, configure: resourceOptions => { resourceOptions.Authority = concelierOptions.Authority.Issuer; resourceOptions.RequireHttpsMetadata = concelierOptions.Authority.RequireHttpsMetadata; resourceOptions.BackchannelTimeout = TimeSpan.FromSeconds(concelierOptions.Authority.BackchannelTimeoutSeconds); resourceOptions.TokenClockSkew = TimeSpan.FromSeconds(concelierOptions.Authority.TokenClockSkewSeconds); if (!string.IsNullOrWhiteSpace(concelierOptions.Authority.MetadataAddress)) { resourceOptions.MetadataAddress = concelierOptions.Authority.MetadataAddress; } foreach (var audience in concelierOptions.Authority.Audiences) { resourceOptions.Audiences.Add(audience); } foreach (var scope in concelierOptions.Authority.RequiredScopes) { resourceOptions.RequiredScopes.Add(scope); } foreach (var network in concelierOptions.Authority.BypassNetworks) { resourceOptions.BypassNetworks.Add(network); } }); } else { builder.Services .AddAuthentication(StellaOpsAuthenticationDefaults.AuthenticationScheme) .AddJwtBearer(StellaOpsAuthenticationDefaults.AuthenticationScheme, options => { options.RequireHttpsMetadata = concelierOptions.Authority.RequireHttpsMetadata; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(concelierOptions.Authority.TestSigningSecret!)), ValidateIssuer = true, ValidIssuer = concelierOptions.Authority.Issuer, ValidateAudience = concelierOptions.Authority.Audiences.Count > 0, ValidAudiences = concelierOptions.Authority.Audiences, ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(concelierOptions.Authority.TokenClockSkewSeconds), NameClaimType = StellaOpsClaimTypes.Subject, RoleClaimType = ClaimTypes.Role }; options.Events = new JwtBearerEvents { OnMessageReceived = context => { var logger = context.HttpContext.RequestServices.GetRequiredService>(); string? token = null; if (context.HttpContext.Request.Headers.TryGetValue("Authorization", out var authorizationValues)) { var authorization = authorizationValues.ToString(); if (!string.IsNullOrWhiteSpace(authorization) && authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) && authorization.Length > 7) { token = authorization.Substring("Bearer ".Length).Trim(); } } if (string.IsNullOrEmpty(token)) { token = context.Token; } if (!string.IsNullOrWhiteSpace(token)) { var parts = token.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length > 0) { token = parts[^1]; } token = token.Trim().Trim('"'); } if (string.IsNullOrWhiteSpace(token)) { logger.LogWarning("JWT token missing from request to {Path}", context.HttpContext.Request.Path); return Task.CompletedTask; } context.Token = token; return Task.CompletedTask; } }; }); } } builder.Services.AddAuthorization(options => { options.AddStellaOpsScopePolicy(JobsPolicyName, concelierOptions.Authority.RequiredScopes.ToArray()); options.AddStellaOpsScopePolicy(ObservationsPolicyName, StellaOpsScopes.VulnView); options.AddStellaOpsScopePolicy(AdvisoryIngestPolicyName, StellaOpsScopes.AdvisoryIngest); options.AddStellaOpsScopePolicy(AdvisoryReadPolicyName, StellaOpsScopes.AdvisoryRead); options.AddStellaOpsScopePolicy(AocVerifyPolicyName, StellaOpsScopes.AdvisoryRead, StellaOpsScopes.AocVerify); }); var pluginHostOptions = BuildPluginOptions(concelierOptions, builder.Environment.ContentRootPath); builder.Services.RegisterPluginRoutines(builder.Configuration, pluginHostOptions); builder.Services.AddEndpointsApiExplorer(); var app = builder.Build(); app.Logger.LogWarning("Authority enabled: {AuthorityEnabled}, test signing secret configured: {HasTestSecret}", authorityConfigured, !string.IsNullOrWhiteSpace(concelierOptions.Authority?.TestSigningSecret)); if (features.NoMergeEnabled) { app.Logger.LogWarning("Legacy merge module disabled via concelier:features:noMergeEnabled; Link-Not-Merge mode active."); } var resolvedConcelierOptions = app.Services.GetRequiredService>().Value; var resolvedAuthority = resolvedConcelierOptions.Authority ?? new ConcelierOptions.AuthorityOptions(); authorityConfigured = resolvedAuthority.Enabled; var enforceAuthority = resolvedAuthority.Enabled && !resolvedAuthority.AllowAnonymousFallback; var requiredTenants = (resolvedAuthority.RequiredTenants ?? Array.Empty()) .Select(static tenant => tenant?.Trim().ToLowerInvariant()) .Where(static tenant => !string.IsNullOrWhiteSpace(tenant)) .Distinct(StringComparer.Ordinal) .ToImmutableHashSet(StringComparer.Ordinal); var enforceTenantAllowlist = !requiredTenants.IsEmpty; if (resolvedAuthority.Enabled && resolvedAuthority.AllowAnonymousFallback) { app.Logger.LogWarning( "Authority authentication is configured but anonymous fallback remains enabled. Set authority.allowAnonymousFallback to false before 2025-12-31 to complete the rollout."); } if (authorityConfigured) { app.UseAuthentication(); app.UseAuthorization(); } app.MapConcelierMirrorEndpoints(authorityConfigured, enforceAuthority); app.MapGet("/.well-known/openapi", ([FromServices] OpenApiDiscoveryDocumentProvider provider, HttpContext context) => { var (payload, etag) = provider.GetDocument(); if (context.Request.Headers.IfNoneMatch.Count > 0) { foreach (var candidate in context.Request.Headers.IfNoneMatch) { if (Matches(candidate, etag)) { context.Response.Headers.ETag = etag; context.Response.Headers.CacheControl = "public, max-age=300, immutable"; return Results.StatusCode(StatusCodes.Status304NotModified); } } } context.Response.Headers.ETag = etag; context.Response.Headers.CacheControl = "public, max-age=300, immutable"; return Results.Text(payload, "application/vnd.oai.openapi+json;version=3.1"); static bool Matches(string? candidate, string expected) { if (string.IsNullOrWhiteSpace(candidate)) { return false; } var trimmed = candidate.Trim(); if (string.Equals(trimmed, expected, StringComparison.Ordinal)) { return true; } if (trimmed.StartsWith("W/", StringComparison.OrdinalIgnoreCase)) { var weakValue = trimmed[2..].TrimStart(); return string.Equals(weakValue, expected, StringComparison.Ordinal); } return false; } }).WithName("GetConcelierOpenApiDocument"); var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); jsonOptions.Converters.Add(new JsonStringEnumConverter()); var observationsEndpoint = app.MapGet("/concelier/observations", async ( HttpContext context, [FromQuery(Name = "observationId")] string[]? observationIds, [FromQuery(Name = "alias")] string[]? aliases, [FromQuery(Name = "purl")] string[]? purls, [FromQuery(Name = "cpe")] string[]? cpes, [FromQuery(Name = "limit")] int? limit, [FromQuery(Name = "cursor")] string? cursor, [FromServices] IAdvisoryObservationQueryService queryService, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); if (!TryResolveTenant(context, requireHeader: false, out var tenant, out var tenantError)) { return tenantError; } var authorizationError = EnsureTenantAuthorized(context, tenant); if (authorizationError is not null) { return authorizationError; } var normalizedTenant = tenant; var options = new AdvisoryObservationQueryOptions( normalizedTenant, observationIds, aliases, purls, cpes, limit, cursor); AdvisoryObservationQueryResult result; try { result = await queryService.QueryAsync(options, cancellationToken).ConfigureAwait(false); } catch (FormatException ex) { return Results.BadRequest(ex.Message); } var response = new AdvisoryObservationQueryResponse( result.Observations, new AdvisoryObservationLinksetAggregateResponse( result.Linkset.Aliases, result.Linkset.Purls, result.Linkset.Cpes, result.Linkset.References), result.NextCursor, result.HasMore); return Results.Ok(response); }).WithName("GetConcelierObservations"); if (authorityConfigured) { observationsEndpoint.RequireAuthorization(ObservationsPolicyName); } var advisoryIngestEndpoint = app.MapPost("/ingest/advisory", async ( HttpContext context, AdvisoryIngestRequest request, [FromServices] IAdvisoryRawService rawService, [FromServices] TimeProvider timeProvider, [FromServices] ILogger logger, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); var ingestRequest = request; if (ingestRequest is null || ingestRequest.Source is null || ingestRequest.Upstream is null || ingestRequest.Content is null || ingestRequest.Identifiers is null) { return Problem(context, "Invalid request", StatusCodes.Status400BadRequest, ProblemTypes.Validation, "source, upstream, content, and identifiers sections are required."); } if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError)) { return tenantError; } var authorizationError = EnsureTenantAuthorized(context, tenant); if (authorizationError is not null) { return authorizationError; } using var ingestScope = logger.BeginScope(new Dictionary(StringComparer.Ordinal) { ["tenant"] = tenant, ["source.vendor"] = ingestRequest.Source.Vendor, ["upstream.upstreamId"] = ingestRequest.Upstream.UpstreamId, ["contentHash"] = ingestRequest.Upstream.ContentHash ?? "(null)" }); AdvisoryRawDocument document; try { logger.LogWarning( "Binding advisory ingest request hash={Hash}", ingestRequest.Upstream.ContentHash ?? "(null)"); document = AdvisoryRawRequestMapper.Map(ingestRequest, tenant, timeProvider); logger.LogWarning( "Mapped advisory_raw document hash={Hash}", string.IsNullOrWhiteSpace(document.Upstream.ContentHash) ? "(empty)" : document.Upstream.ContentHash); } catch (Exception ex) when (ex is ArgumentException or InvalidOperationException) { return Problem(context, "Invalid advisory payload", StatusCodes.Status400BadRequest, ProblemTypes.Validation, ex.Message); } try { var result = await rawService.IngestAsync(document, cancellationToken).ConfigureAwait(false); var response = new AdvisoryIngestResponse( result.Record.Id, result.Inserted, result.Record.Document.Tenant, result.Record.Document.Upstream.ContentHash, result.Record.Document.Supersedes, result.Record.IngestedAt, result.Record.CreatedAt); var statusCode = result.Inserted ? StatusCodes.Status201Created : StatusCodes.Status200OK; if (result.Inserted) { context.Response.Headers.Location = $"/advisories/raw/{Uri.EscapeDataString(result.Record.Id)}"; } IngestionMetrics.IngestionWriteCounter.Add( 1, IngestionMetrics.BuildWriteTags( tenant, ingestRequest.Source.Vendor ?? "(unknown)", result.Inserted ? "inserted" : "duplicate")); return JsonResult(response, statusCode); } catch (ConcelierAocGuardException guardException) { logger.LogWarning( guardException, "AOC guard rejected advisory ingest tenant={Tenant} upstream={UpstreamId} requestHash={RequestHash} documentHash={DocumentHash} codes={Codes}", tenant, document.Upstream.UpstreamId, request!.Upstream?.ContentHash ?? "(null)", string.IsNullOrWhiteSpace(document.Upstream.ContentHash) ? "(empty)" : document.Upstream.ContentHash, string.Join(',', guardException.Violations.Select(static violation => violation.ErrorCode))); IngestionMetrics.IngestionWriteCounter.Add( 1, IngestionMetrics.BuildWriteTags( tenant, ingestRequest.Source.Vendor ?? "(unknown)", "rejected")); return MapAocGuardException(context, guardException); } }); var advisoryIngestGuardOptions = AocGuardOptions.Default with { RequireTenant = false, RequiredTopLevelFields = AocGuardOptions.Default.RequiredTopLevelFields.Remove("tenant") }; advisoryIngestEndpoint.RequireAocGuard(request => { if (request?.Source is null || request.Upstream is null || request.Content is null || request.Identifiers is null) { return Array.Empty(); } var guardDocument = AdvisoryRawRequestMapper.Map(request, "guard-tenant", TimeProvider.System); return new object?[] { guardDocument }; }, guardOptions: advisoryIngestGuardOptions); if (authorityConfigured) { advisoryIngestEndpoint.RequireAuthorization(AdvisoryIngestPolicyName); } var advisoryRawListEndpoint = app.MapGet("/advisories/raw", async ( HttpContext context, [FromServices] IAdvisoryRawService rawService, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); if (!TryResolveTenant(context, requireHeader: false, out var tenant, out var tenantError)) { return tenantError; } var authorizationError = EnsureTenantAuthorized(context, tenant); if (authorizationError is not null) { return authorizationError; } var query = context.Request.Query; var options = new AdvisoryRawQueryOptions(tenant); if (query.TryGetValue("vendor", out var vendorValues)) { options = options with { Vendors = AdvisoryRawRequestMapper.NormalizeStrings(vendorValues) }; } if (query.TryGetValue("upstreamId", out var upstreamValues)) { options = options with { UpstreamIds = AdvisoryRawRequestMapper.NormalizeStrings(upstreamValues) }; } if (query.TryGetValue("alias", out var aliasValues)) { options = options with { Aliases = AdvisoryRawRequestMapper.NormalizeStrings(aliasValues) }; } if (query.TryGetValue("purl", out var purlValues)) { options = options with { PackageUrls = AdvisoryRawRequestMapper.NormalizeStrings(purlValues) }; } if (query.TryGetValue("hash", out var hashValues)) { options = options with { ContentHashes = AdvisoryRawRequestMapper.NormalizeStrings(hashValues) }; } if (query.TryGetValue("since", out var sinceValues)) { var since = ParseDateTime(sinceValues.FirstOrDefault()); if (since.HasValue) { options = options with { Since = since }; } } if (query.TryGetValue("limit", out var limitValues) && int.TryParse(limitValues.FirstOrDefault(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLimit)) { options = options with { Limit = parsedLimit }; } if (query.TryGetValue("cursor", out var cursorValues)) { var cursor = cursorValues.FirstOrDefault(); if (!string.IsNullOrWhiteSpace(cursor)) { options = options with { Cursor = cursor }; } } var result = await rawService.QueryAsync(options, cancellationToken).ConfigureAwait(false); var records = result.Records .Select(record => new AdvisoryRawRecordResponse( record.Id, record.Document.Tenant, record.IngestedAt, record.CreatedAt, record.Document)) .ToArray(); var response = new AdvisoryRawListResponse(records, result.NextCursor, result.HasMore); return JsonResult(response); }); if (authorityConfigured) { advisoryRawListEndpoint.RequireAuthorization(AdvisoryReadPolicyName); } var advisoryRawGetEndpoint = app.MapGet("/advisories/raw/{id}", async ( string id, HttpContext context, [FromServices] IAdvisoryRawService rawService, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); if (!TryResolveTenant(context, requireHeader: false, out var tenant, out var tenantError)) { return tenantError; } var authorizationError = EnsureTenantAuthorized(context, tenant); if (authorizationError is not null) { return authorizationError; } if (string.IsNullOrWhiteSpace(id)) { return Problem(context, "id is required", StatusCodes.Status400BadRequest, ProblemTypes.Validation, "Provide an advisory identifier."); } var record = await rawService.FindByIdAsync(tenant, id.Trim(), cancellationToken).ConfigureAwait(false); if (record is null) { return Results.NotFound(); } var response = new AdvisoryRawRecordResponse( record.Id, record.Document.Tenant, record.IngestedAt, record.CreatedAt, record.Document); return JsonResult(response); }); if (authorityConfigured) { advisoryRawGetEndpoint.RequireAuthorization(AdvisoryReadPolicyName); } var advisoryRawProvenanceEndpoint = app.MapGet("/advisories/raw/{id}/provenance", async ( string id, HttpContext context, [FromServices] IAdvisoryRawService rawService, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); if (!TryResolveTenant(context, requireHeader: false, out var tenant, out var tenantError)) { return tenantError; } var authorizationError = EnsureTenantAuthorized(context, tenant); if (authorizationError is not null) { return authorizationError; } if (string.IsNullOrWhiteSpace(id)) { return Problem(context, "id is required", StatusCodes.Status400BadRequest, ProblemTypes.Validation, "Provide an advisory identifier."); } var record = await rawService.FindByIdAsync(tenant, id.Trim(), cancellationToken).ConfigureAwait(false); if (record is null) { return Results.NotFound(); } var response = new AdvisoryRawProvenanceResponse( record.Id, record.Document.Tenant, record.Document.Source, record.Document.Upstream, record.Document.Supersedes, record.IngestedAt, record.CreatedAt); return JsonResult(response); }); if (authorityConfigured) { advisoryRawProvenanceEndpoint.RequireAuthorization(AdvisoryReadPolicyName); } var advisoryEvidenceEndpoint = app.MapGet("/vuln/evidence/advisories/{advisoryKey}", async ( string advisoryKey, HttpContext context, [FromServices] IAdvisoryRawService rawService, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); if (!TryResolveTenant(context, requireHeader: false, out var tenant, out var tenantError)) { return tenantError; } var authorizationError = EnsureTenantAuthorized(context, tenant); if (authorizationError is not null) { return authorizationError; } if (string.IsNullOrWhiteSpace(advisoryKey)) { return Problem(context, "advisoryKey is required", StatusCodes.Status400BadRequest, ProblemTypes.Validation, "Provide an advisory identifier."); } var normalizedKey = advisoryKey.Trim(); var canonicalKey = normalizedKey.ToUpperInvariant(); var vendorFilter = AdvisoryRawRequestMapper.NormalizeStrings(context.Request.Query["vendor"]); var records = await rawService.FindByAdvisoryKeyAsync( tenant, canonicalKey, vendorFilter, cancellationToken).ConfigureAwait(false); if (records.Count == 0) { return Problem(context, "Advisory not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, $"No evidence available for {normalizedKey}."); } var recordResponses = records .Select(record => new AdvisoryRawRecordResponse( record.Id, record.Document.Tenant, record.IngestedAt, record.CreatedAt, record.Document)) .ToArray(); var responseKey = recordResponses[0].Document.AdvisoryKey ?? canonicalKey; var response = new AdvisoryEvidenceResponse(responseKey, recordResponses); return JsonResult(response); }); if (authorityConfigured) { advisoryEvidenceEndpoint.RequireAuthorization(AdvisoryReadPolicyName); } var advisoryChunksEndpoint = app.MapGet("/advisories/{advisoryKey}/chunks", async ( string advisoryKey, HttpContext context, [FromServices] IAdvisoryObservationQueryService observationService, [FromServices] AdvisoryChunkBuilder chunkBuilder, [FromServices] IAdvisoryChunkCache chunkCache, [FromServices] IAdvisoryAiTelemetry telemetry, [FromServices] TimeProvider timeProvider, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); var requestStart = timeProvider.GetTimestamp(); if (!TryResolveTenant(context, requireHeader: false, out var tenant, out var tenantError)) { telemetry.TrackChunkFailure(null, advisoryKey ?? string.Empty, "tenant_unresolved", "validation_error"); return tenantError; } var authorizationError = EnsureTenantAuthorized(context, tenant); if (authorizationError is not null) { var failureResult = authorizationError switch { UnauthorizedHttpResult => "unauthorized", _ => "forbidden" }; telemetry.TrackChunkFailure(tenant, advisoryKey ?? string.Empty, "tenant_not_authorized", failureResult); return authorizationError; } if (string.IsNullOrWhiteSpace(advisoryKey)) { telemetry.TrackChunkFailure(tenant, string.Empty, "missing_key", "validation_error"); return Problem(context, "advisoryKey is required", StatusCodes.Status400BadRequest, ProblemTypes.Validation, "Provide an advisory identifier."); } var normalizedKey = advisoryKey.Trim(); var chunkSettings = resolvedConcelierOptions.AdvisoryChunks ?? new ConcelierOptions.AdvisoryChunkOptions(); var chunkLimit = ResolveBoundedInt(context.Request.Query["limit"], chunkSettings.DefaultChunkLimit, 1, chunkSettings.MaxChunkLimit); var observationLimit = ResolveBoundedInt(context.Request.Query["observations"], chunkSettings.DefaultObservationLimit, 1, chunkSettings.MaxObservationLimit); var minimumLength = ResolveBoundedInt(context.Request.Query["minLength"], chunkSettings.DefaultMinimumLength, 16, chunkSettings.MaxMinimumLength); var sectionFilter = BuildFilterSet(context.Request.Query["section"]); var formatFilter = BuildFilterSet(context.Request.Query["format"]); var queryOptions = new AdvisoryObservationQueryOptions( tenant, aliases: new[] { normalizedKey }, limit: observationLimit); var observationResult = await observationService.QueryAsync(queryOptions, cancellationToken).ConfigureAwait(false); if (observationResult.Observations.IsDefaultOrEmpty || observationResult.Observations.Length == 0) { telemetry.TrackChunkFailure(tenant, normalizedKey, "advisory_not_found", "not_found"); return Problem(context, "Advisory not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, $"No observations available for {normalizedKey}."); } var observations = observationResult.Observations.ToArray(); var buildOptions = new AdvisoryChunkBuildOptions( normalizedKey, chunkLimit, observationLimit, sectionFilter, formatFilter, minimumLength); var cacheDuration = chunkSettings.CacheDurationSeconds > 0 ? TimeSpan.FromSeconds(chunkSettings.CacheDurationSeconds) : TimeSpan.Zero; AdvisoryChunkBuildResult buildResult; var cacheHit = false; if (cacheDuration > TimeSpan.Zero) { var cacheKey = AdvisoryChunkCacheKey.Create(tenant, normalizedKey, buildOptions, observations); if (chunkCache.TryGet(cacheKey, out var cachedResult)) { buildResult = cachedResult; cacheHit = true; } else { buildResult = chunkBuilder.Build(buildOptions, observations); chunkCache.Set(cacheKey, buildResult, cacheDuration); } } else { buildResult = chunkBuilder.Build(buildOptions, observations); } var duration = timeProvider.GetElapsedTime(requestStart); var guardrailCounts = cacheHit ? ImmutableDictionary.Empty : buildResult.Telemetry.GuardrailCounts; telemetry.TrackChunkResult(new AdvisoryAiChunkRequestTelemetry( tenant, normalizedKey, "ok", buildResult.Response.Truncated, cacheHit, observations.Length, buildResult.Response.Chunks.Count, duration, guardrailCounts)); return JsonResult(buildResult.Response); }); if (authorityConfigured) { advisoryChunksEndpoint.RequireAuthorization(AdvisoryReadPolicyName); } var aocVerifyEndpoint = app.MapPost("/aoc/verify", async ( HttpContext context, AocVerifyRequest request, [FromServices] IAdvisoryRawService rawService, [FromServices] TimeProvider timeProvider, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); if (!TryResolveTenant(context, requireHeader: false, out var tenant, out var tenantError)) { return tenantError; } var authorizationError = EnsureTenantAuthorized(context, tenant); if (authorizationError is not null) { return authorizationError; } var now = timeProvider.GetUtcNow(); var windowStart = (request?.Since ?? now.AddHours(-24)).ToUniversalTime(); var windowEnd = (request?.Until ?? now).ToUniversalTime(); if (windowEnd < windowStart) { return Problem(context, "Invalid verification window", StatusCodes.Status400BadRequest, ProblemTypes.Validation, "'until' must be greater than 'since'."); } var limit = request?.Limit ?? 20; if (limit < 0) { limit = 0; } var sources = AdvisoryRawRequestMapper.NormalizeStrings(request?.Sources); var codes = AdvisoryRawRequestMapper.NormalizeStrings(request?.Codes); var verificationRequest = new AdvisoryRawVerificationRequest( tenant, windowStart, windowEnd, limit, sources, codes); var result = await rawService.VerifyAsync(verificationRequest, cancellationToken).ConfigureAwait(false); var violationResponses = result.Violations .Select(violation => new AocVerifyViolation( violation.Code, violation.Count, violation.Examples.Select(example => new AocVerifyViolationExample( example.SourceVendor, example.DocumentId, example.ContentHash, example.Path)).ToArray())) .ToArray(); var metrics = new AocVerifyMetrics(result.CheckedCount, result.Violations.Sum(v => v.Count)); var response = new AocVerifyResponse( result.Tenant, new AocVerifyWindow(result.WindowStart, result.WindowEnd), new AocVerifyChecked(result.CheckedCount, 0), violationResponses, metrics, result.Truncated); var verificationOutcome = response.Truncated ? "truncated" : (violationResponses.Length == 0 ? "ok" : "violations"); IngestionMetrics.VerificationCounter.Add( 1, IngestionMetrics.BuildVerifyTags(tenant, verificationOutcome)); return JsonResult(response); }); if (authorityConfigured) { aocVerifyEndpoint.RequireAuthorization(AocVerifyPolicyName); } app.MapGet("/concelier/advisories/{vulnerabilityKey}/replay", async ( string vulnerabilityKey, DateTimeOffset? asOf, [FromServices] IAdvisoryEventLog eventLog, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(vulnerabilityKey)) { return Results.BadRequest("vulnerabilityKey must be provided."); } var replay = await eventLog.ReplayAsync(vulnerabilityKey.Trim(), asOf, cancellationToken).ConfigureAwait(false); if (replay.Statements.Length == 0 && replay.Conflicts.Length == 0) { return Results.NotFound(); } var response = new { replay.VulnerabilityKey, replay.AsOf, Statements = replay.Statements.Select(statement => new { statement.StatementId, statement.VulnerabilityKey, statement.AdvisoryKey, statement.Advisory, StatementHash = Convert.ToHexString(statement.StatementHash.ToArray()), statement.AsOf, statement.RecordedAt, InputDocumentIds = statement.InputDocumentIds }).ToArray(), Conflicts = replay.Conflicts.Select(conflict => new { conflict.ConflictId, conflict.VulnerabilityKey, conflict.StatementIds, ConflictHash = Convert.ToHexString(conflict.ConflictHash.ToArray()), conflict.AsOf, conflict.RecordedAt, Details = conflict.CanonicalJson, Explainer = MergeConflictExplainerPayload.FromCanonicalJson(conflict.CanonicalJson) }).ToArray() }; return JsonResult(response); }); var loggingEnabled = concelierOptions.Telemetry?.EnableLogging ?? true; if (loggingEnabled) { app.UseSerilogRequestLogging(options => { options.IncludeQueryInRequestPath = true; options.GetLevel = (httpContext, elapsedMs, 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; 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: ProblemTypes.JobFailure, extensions: extensions); await problem.ExecuteAsync(context); }); }); if (authorityConfigured) { app.Use(async (context, next) => { await next().ConfigureAwait(false); if (!context.Request.Path.StartsWithSegments("/jobs", StringComparison.OrdinalIgnoreCase)) { return; } if (context.Response.StatusCode != StatusCodes.Status401Unauthorized) { return; } var optionsMonitor = context.RequestServices.GetRequiredService>().Value.Authority; if (optionsMonitor is null || !optionsMonitor.Enabled) { return; } var logger = context.RequestServices .GetRequiredService() .CreateLogger(JobAuthorizationAuditFilter.LoggerName); var matcher = new NetworkMaskMatcher(optionsMonitor.BypassNetworks); var remote = context.Connection.RemoteIpAddress; var bypassAllowed = matcher.IsAllowed(remote); logger.LogWarning( "Concelier authorization denied route={Route} remote={RemoteAddress} bypassAllowed={BypassAllowed} hasPrincipal={HasPrincipal}", context.Request.Path.Value ?? string.Empty, remote?.ToString() ?? "unknown", bypassAllowed, context.User?.Identity?.IsAuthenticated ?? false); }); } IResult JsonResult(T value, int? statusCode = null) { var payload = JsonSerializer.Serialize(value, jsonOptions); return Results.Content(payload, "application/json", Encoding.UTF8, statusCode); } IResult Problem(HttpContext context, string title, int statusCode, string type, string? detail = null, IDictionary? extensions = null) { var traceId = Activity.Current?.TraceId.ToString() ?? context.TraceIdentifier; extensions ??= new Dictionary(StringComparer.Ordinal) { ["traceId"] = traceId, }; if (!extensions.ContainsKey("traceId")) { extensions["traceId"] = traceId; } var problemDetails = new ProblemDetails { Type = type, Title = title, Detail = detail, Status = statusCode, Instance = context.Request.Path }; foreach (var entry in extensions) { problemDetails.Extensions[entry.Key] = entry.Value; } var payload = JsonSerializer.Serialize(problemDetails, jsonOptions); return Results.Content(payload, "application/problem+json", Encoding.UTF8, statusCode); } bool TryResolveTenant(HttpContext context, bool requireHeader, out string tenant, out IResult? error) { tenant = string.Empty; error = null; var headerTenant = context.Request.Headers[TenantHeaderName].FirstOrDefault(); var queryTenant = context.Request.Query.TryGetValue("tenant", out var tenantValues) ? tenantValues.FirstOrDefault() : null; if (requireHeader && string.IsNullOrWhiteSpace(headerTenant)) { error = Problem(context, "Tenant header required", StatusCodes.Status400BadRequest, ProblemTypes.Validation, $"Header '{TenantHeaderName}' must be provided."); return false; } if (!string.IsNullOrWhiteSpace(headerTenant) && !string.IsNullOrWhiteSpace(queryTenant) && !string.Equals(headerTenant.Trim(), queryTenant.Trim(), StringComparison.OrdinalIgnoreCase)) { error = Problem(context, "Tenant mismatch", StatusCodes.Status400BadRequest, ProblemTypes.Validation, $"Values for '{TenantHeaderName}' and 'tenant' query parameter must match."); return false; } var resolved = !string.IsNullOrWhiteSpace(headerTenant) ? headerTenant : queryTenant; if (string.IsNullOrWhiteSpace(resolved)) { error = Problem(context, "Tenant required", StatusCodes.Status400BadRequest, ProblemTypes.Validation, $"Specify the tenant via '{TenantHeaderName}' header or 'tenant' query parameter."); return false; } tenant = resolved.Trim().ToLowerInvariant(); return true; } IResult? EnsureTenantAuthorized(HttpContext context, string tenant) { if (!authorityConfigured) { return null; } if (enforceTenantAllowlist && !requiredTenants.Contains(tenant)) { return Results.Forbid(); } var principal = context.User; if (enforceAuthority && (principal?.Identity?.IsAuthenticated != true)) { return Results.Unauthorized(); } if (principal?.Identity?.IsAuthenticated == true) { var tenantClaim = principal.FindFirstValue(StellaOpsClaimTypes.Tenant); if (string.IsNullOrWhiteSpace(tenantClaim)) { return Results.Forbid(); } var normalizedClaim = tenantClaim.Trim().ToLowerInvariant(); if (!string.Equals(normalizedClaim, tenant, StringComparison.Ordinal)) { return Results.Forbid(); } if (enforceTenantAllowlist && !requiredTenants.Contains(normalizedClaim)) { return Results.Forbid(); } } return null; } ImmutableHashSet BuildFilterSet(StringValues values) { if (values.Count == 0) { return ImmutableHashSet.Empty; } var builder = ImmutableHashSet.CreateBuilder(StringComparer.OrdinalIgnoreCase); foreach (var value in values) { if (string.IsNullOrWhiteSpace(value)) { continue; } var segments = value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (segments.Length == 0) { builder.Add(value.Trim()); continue; } foreach (var segment in segments) { if (!string.IsNullOrWhiteSpace(segment)) { builder.Add(segment.Trim()); } } } return builder.ToImmutable(); } int ResolveBoundedInt(StringValues values, int fallback, int minValue, int maxValue) { foreach (var value in values) { if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)) { return Math.Clamp(parsed, minValue, maxValue); } } return Math.Clamp(fallback, minValue, maxValue); } static DateTimeOffset? ParseDateTime(string? value) { if (string.IsNullOrWhiteSpace(value)) { return null; } return DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed) ? parsed.ToUniversalTime() : null; } IResult MapAocGuardException(HttpContext context, ConcelierAocGuardException exception) { var guardException = new AocGuardException(exception.Result); return AocHttpResults.Problem(context, guardException); } static KeyValuePair[] BuildJobMetricTags(string jobKind, string trigger, string outcome) => new[] { new KeyValuePair("job.kind", jobKind), new KeyValuePair("job.trigger", trigger), new KeyValuePair("job.outcome", outcome), }; void ApplyNoCache(HttpResponse response) { if (response is null) { return; } response.Headers.CacheControl = "no-store, no-cache, max-age=0, must-revalidate"; response.Headers.Pragma = "no-cache"; response.Headers["Expires"] = "0"; } await InitializeMongoAsync(app); app.MapGet("/health", ([FromServices] IOptions opts, [FromServices] ServiceStatus status, HttpContext context) => { ApplyNoCache(context.Response); var snapshot = status.CreateSnapshot(); var uptimeSeconds = Math.Max((snapshot.CapturedAt - snapshot.StartedAt).TotalSeconds, 0d); var storage = new StorageBootstrapHealth( Driver: opts.Value.Storage.Driver, Completed: snapshot.BootstrapCompletedAt is not null, CompletedAt: snapshot.BootstrapCompletedAt, DurationMs: snapshot.BootstrapDuration?.TotalMilliseconds); var telemetry = new TelemetryHealth( Enabled: opts.Value.Telemetry.Enabled, Tracing: opts.Value.Telemetry.EnableTracing, Metrics: opts.Value.Telemetry.EnableMetrics, Logging: opts.Value.Telemetry.EnableLogging); var response = new HealthDocument( Status: "healthy", StartedAt: snapshot.StartedAt, UptimeSeconds: uptimeSeconds, Storage: storage, Telemetry: telemetry); return JsonResult(response); }); app.MapGet("/ready", async ([FromServices] IMongoDatabase database, [FromServices] ServiceStatus status, HttpContext context, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); var stopwatch = Stopwatch.StartNew(); try { await database.RunCommandAsync((Command)"{ ping: 1 }", cancellationToken: cancellationToken).ConfigureAwait(false); stopwatch.Stop(); status.RecordMongoCheck(success: true, latency: stopwatch.Elapsed, error: null); var snapshot = status.CreateSnapshot(); var uptimeSeconds = Math.Max((snapshot.CapturedAt - snapshot.StartedAt).TotalSeconds, 0d); var mongo = new MongoReadyHealth( Status: "ready", LatencyMs: snapshot.LastMongoLatency?.TotalMilliseconds, CheckedAt: snapshot.LastReadyCheckAt, Error: null); var response = new ReadyDocument( Status: "ready", StartedAt: snapshot.StartedAt, UptimeSeconds: uptimeSeconds, Mongo: mongo); return JsonResult(response); } catch (Exception ex) { stopwatch.Stop(); status.RecordMongoCheck(success: false, latency: stopwatch.Elapsed, error: ex.Message); var snapshot = status.CreateSnapshot(); var uptimeSeconds = Math.Max((snapshot.CapturedAt - snapshot.StartedAt).TotalSeconds, 0d); var mongo = new MongoReadyHealth( Status: "unready", LatencyMs: snapshot.LastMongoLatency?.TotalMilliseconds, CheckedAt: snapshot.LastReadyCheckAt, Error: snapshot.LastMongoError ?? ex.Message); var response = new ReadyDocument( Status: "unready", StartedAt: snapshot.StartedAt, UptimeSeconds: uptimeSeconds, Mongo: mongo); var extensions = new Dictionary(StringComparer.Ordinal) { ["mongoLatencyMs"] = snapshot.LastMongoLatency?.TotalMilliseconds, ["mongoError"] = snapshot.LastMongoError ?? ex.Message, }; return Problem(context, "Mongo unavailable", StatusCodes.Status503ServiceUnavailable, ProblemTypes.ServiceUnavailable, snapshot.LastMongoError ?? ex.Message, extensions); } }); app.MapGet("/diagnostics/aliases/{seed}", async (string seed, [FromServices] AliasGraphResolver resolver, HttpContext context, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); if (string.IsNullOrWhiteSpace(seed)) { return Problem(context, "Seed advisory key is required.", StatusCodes.Status400BadRequest, ProblemTypes.Validation); } var component = await resolver.BuildComponentAsync(seed, cancellationToken).ConfigureAwait(false); var aliases = component.AliasMap.ToDictionary( static kvp => kvp.Key, static kvp => kvp.Value .Select(record => new { record.Scheme, record.Value, UpdatedAt = record.UpdatedAt }) .ToArray()); var response = new { Seed = component.SeedAdvisoryKey, Advisories = component.AdvisoryKeys, Collisions = component.Collisions .Select(collision => new { collision.Scheme, collision.Value, AdvisoryKeys = collision.AdvisoryKeys }) .ToArray(), Aliases = aliases }; return JsonResult(response); }); var jobsListEndpoint = app.MapGet("/jobs", async (string? kind, int? limit, [FromServices] IJobCoordinator coordinator, HttpContext context, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); var take = Math.Clamp(limit.GetValueOrDefault(50), 1, 200); var runs = await coordinator.GetRecentRunsAsync(kind, take, cancellationToken).ConfigureAwait(false); var payload = runs.Select(JobRunResponse.FromSnapshot).ToArray(); return JsonResult(payload); }).AddEndpointFilter(); if (enforceAuthority) { jobsListEndpoint.RequireAuthorization(JobsPolicyName); } var jobByIdEndpoint = app.MapGet("/jobs/{runId:guid}", async (Guid runId, [FromServices] IJobCoordinator coordinator, HttpContext context, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); var run = await coordinator.GetRunAsync(runId, cancellationToken).ConfigureAwait(false); if (run is null) { return Problem(context, "Job run not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, $"Job run '{runId}' was not found."); } return JsonResult(JobRunResponse.FromSnapshot(run)); }).AddEndpointFilter(); if (enforceAuthority) { jobByIdEndpoint.RequireAuthorization(JobsPolicyName); } var jobDefinitionsEndpoint = app.MapGet("/jobs/definitions", async ([FromServices] IJobCoordinator coordinator, HttpContext context, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); var definitions = await coordinator.GetDefinitionsAsync(cancellationToken).ConfigureAwait(false); if (definitions.Count == 0) { return JsonResult(Array.Empty()); } var definitionKinds = definitions.Select(static definition => definition.Kind).ToArray(); var lastRuns = await coordinator.GetLastRunsAsync(definitionKinds, cancellationToken).ConfigureAwait(false); var responses = new List(definitions.Count); foreach (var definition in definitions) { lastRuns.TryGetValue(definition.Kind, out var lastRun); responses.Add(JobDefinitionResponse.FromDefinition(definition, lastRun)); } return JsonResult(responses); }).AddEndpointFilter(); if (enforceAuthority) { jobDefinitionsEndpoint.RequireAuthorization(JobsPolicyName); } var jobDefinitionEndpoint = app.MapGet("/jobs/definitions/{kind}", async (string kind, [FromServices] IJobCoordinator coordinator, HttpContext context, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); var definition = (await coordinator.GetDefinitionsAsync(cancellationToken).ConfigureAwait(false)) .FirstOrDefault(d => string.Equals(d.Kind, kind, StringComparison.Ordinal)); if (definition is null) { return Problem(context, "Job definition not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, $"Job kind '{kind}' is not registered."); } var lastRuns = await coordinator.GetLastRunsAsync(new[] { definition.Kind }, cancellationToken).ConfigureAwait(false); lastRuns.TryGetValue(definition.Kind, out var lastRun); var response = JobDefinitionResponse.FromDefinition(definition, lastRun); return JsonResult(response); }).AddEndpointFilter(); if (enforceAuthority) { jobDefinitionEndpoint.RequireAuthorization(JobsPolicyName); } var jobDefinitionRunsEndpoint = app.MapGet("/jobs/definitions/{kind}/runs", async (string kind, int? limit, [FromServices] IJobCoordinator coordinator, HttpContext context, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); var definition = (await coordinator.GetDefinitionsAsync(cancellationToken).ConfigureAwait(false)) .FirstOrDefault(d => string.Equals(d.Kind, kind, StringComparison.Ordinal)); if (definition is null) { return Problem(context, "Job definition not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, $"Job kind '{kind}' is not registered."); } var take = Math.Clamp(limit.GetValueOrDefault(20), 1, 200); var runs = await coordinator.GetRecentRunsAsync(kind, take, cancellationToken).ConfigureAwait(false); var payload = runs.Select(JobRunResponse.FromSnapshot).ToArray(); return JsonResult(payload); }).AddEndpointFilter(); if (enforceAuthority) { jobDefinitionRunsEndpoint.RequireAuthorization(JobsPolicyName); } var activeJobsEndpoint = app.MapGet("/jobs/active", async ([FromServices] IJobCoordinator coordinator, HttpContext context, CancellationToken cancellationToken) => { ApplyNoCache(context.Response); var runs = await coordinator.GetActiveRunsAsync(cancellationToken).ConfigureAwait(false); var payload = runs.Select(JobRunResponse.FromSnapshot).ToArray(); return JsonResult(payload); }).AddEndpointFilter(); if (enforceAuthority) { activeJobsEndpoint.RequireAuthorization(JobsPolicyName); } var triggerJobEndpoint = app.MapPost("/jobs/{*jobKind}", async (string jobKind, JobTriggerRequest request, [FromServices] IJobCoordinator coordinator, HttpContext context) => { ApplyNoCache(context.Response); request ??= new JobTriggerRequest(); request.Parameters ??= new Dictionary(StringComparer.Ordinal); var trigger = string.IsNullOrWhiteSpace(request.Trigger) ? "api" : request.Trigger; var lifetime = context.RequestServices.GetRequiredService(); var result = await coordinator.TriggerAsync(jobKind, request.Parameters, trigger, lifetime.ApplicationStopping).ConfigureAwait(false); var outcome = result.Outcome; var tags = BuildJobMetricTags(jobKind, trigger, outcome.ToString().ToLowerInvariant()); switch (outcome) { case JobTriggerOutcome.Accepted: JobMetrics.TriggerCounter.Add(1, tags); if (result.Run is null) { return Results.StatusCode(StatusCodes.Status202Accepted); } var acceptedRun = JobRunResponse.FromSnapshot(result.Run); context.Response.Headers.Location = $"/jobs/{acceptedRun.RunId}"; return JsonResult(acceptedRun, StatusCodes.Status202Accepted); case JobTriggerOutcome.NotFound: JobMetrics.TriggerConflictCounter.Add(1, tags); return Problem(context, "Job not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, result.ErrorMessage ?? $"Job '{jobKind}' is not registered."); case JobTriggerOutcome.Disabled: JobMetrics.TriggerConflictCounter.Add(1, tags); return Problem(context, "Job disabled", StatusCodes.Status423Locked, ProblemTypes.Locked, result.ErrorMessage ?? $"Job '{jobKind}' is disabled."); case JobTriggerOutcome.AlreadyRunning: JobMetrics.TriggerConflictCounter.Add(1, tags); return Problem(context, "Job already running", StatusCodes.Status409Conflict, ProblemTypes.Conflict, result.ErrorMessage ?? $"Job '{jobKind}' already has an active run."); case JobTriggerOutcome.LeaseRejected: JobMetrics.TriggerConflictCounter.Add(1, tags); return Problem(context, "Job lease rejected", StatusCodes.Status409Conflict, ProblemTypes.LeaseRejected, result.ErrorMessage ?? $"Job '{jobKind}' could not acquire a lease."); case JobTriggerOutcome.InvalidParameters: { JobMetrics.TriggerConflictCounter.Add(1, tags); var extensions = new Dictionary(StringComparer.Ordinal) { ["parameters"] = request.Parameters, }; return Problem(context, "Invalid job parameters", StatusCodes.Status400BadRequest, ProblemTypes.Validation, result.ErrorMessage, extensions); } case JobTriggerOutcome.Cancelled: { JobMetrics.TriggerConflictCounter.Add(1, tags); var extensions = new Dictionary(StringComparer.Ordinal) { ["run"] = result.Run is null ? null : JobRunResponse.FromSnapshot(result.Run), }; return Problem(context, "Job cancelled", StatusCodes.Status409Conflict, ProblemTypes.Conflict, result.ErrorMessage ?? $"Job '{jobKind}' was cancelled before completion.", extensions); } case JobTriggerOutcome.Failed: { JobMetrics.TriggerFailureCounter.Add(1, tags); var extensions = new Dictionary(StringComparer.Ordinal) { ["run"] = result.Run is null ? null : JobRunResponse.FromSnapshot(result.Run), }; return Problem(context, "Job execution failed", StatusCodes.Status500InternalServerError, ProblemTypes.JobFailure, result.ErrorMessage, extensions); } default: JobMetrics.TriggerFailureCounter.Add(1, tags); return Problem(context, "Unexpected job outcome", StatusCodes.Status500InternalServerError, ProblemTypes.JobFailure, $"Job '{jobKind}' returned outcome '{outcome}'."); } }).AddEndpointFilter(); if (enforceAuthority) { triggerJobEndpoint.RequireAuthorization(JobsPolicyName); } await app.RunAsync(); static PluginHostOptions BuildPluginOptions(ConcelierOptions options, string contentRoot) { var pluginOptions = new PluginHostOptions { BaseDirectory = options.Plugins.BaseDirectory ?? contentRoot, PluginsDirectory = options.Plugins.Directory ?? Path.Combine(contentRoot, "StellaOps.Concelier.PluginBinaries"), PrimaryPrefix = "StellaOps.Concelier", EnsureDirectoryExists = true, RecursiveSearch = false, }; if (options.Plugins.SearchPatterns.Count == 0) { pluginOptions.SearchPatterns.Add("StellaOps.Concelier.Plugin.*.dll"); } else { foreach (var pattern in options.Plugins.SearchPatterns) { if (!string.IsNullOrWhiteSpace(pattern)) { pluginOptions.SearchPatterns.Add(pattern); } } } return pluginOptions; } static async Task InitializeMongoAsync(WebApplication app) { await using var scope = app.Services.CreateAsyncScope(); var bootstrapper = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("MongoBootstrapper"); var status = scope.ServiceProvider.GetRequiredService(); var stopwatch = Stopwatch.StartNew(); try { await bootstrapper.InitializeAsync(app.Lifetime.ApplicationStopping).ConfigureAwait(false); stopwatch.Stop(); status.MarkBootstrapCompleted(stopwatch.Elapsed); logger.LogInformation("Mongo bootstrap completed in {ElapsedMs} ms", stopwatch.Elapsed.TotalMilliseconds); } catch (Exception ex) { stopwatch.Stop(); status.RecordMongoCheck(success: false, latency: stopwatch.Elapsed, error: ex.Message); logger.LogCritical(ex, "Mongo bootstrap failed after {ElapsedMs} ms", stopwatch.Elapsed.TotalMilliseconds); throw; } } public partial class Program;