feat: Implement BerkeleyDB reader for RPM databases
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
console-runner-image / build-runner-image (push) Has been cancelled
wine-csp-build / Build Wine CSP Image (push) Has been cancelled
wine-csp-build / Integration Tests (push) Has been cancelled
wine-csp-build / Security Scan (push) Has been cancelled
wine-csp-build / Generate SBOM (push) Has been cancelled
wine-csp-build / Publish Image (push) Has been cancelled
wine-csp-build / Air-Gap Bundle (push) Has been cancelled
wine-csp-build / Test Summary (push) Has been cancelled

- Added BerkeleyDbReader class to read and extract RPM header blobs from BerkeleyDB hash databases.
- Implemented methods to detect BerkeleyDB format and extract values, including handling of page sizes and magic numbers.
- Added tests for BerkeleyDbReader to ensure correct functionality and header extraction.

feat: Add Yarn PnP data tests

- Created YarnPnpDataTests to validate package resolution and data loading from Yarn PnP cache.
- Implemented tests for resolved keys, package presence, and loading from cache structure.

test: Add egg-info package fixtures for Python tests

- Created egg-info package fixtures for testing Python analyzers.
- Included PKG-INFO, entry_points.txt, and installed-files.txt for comprehensive coverage.

test: Enhance RPM database reader tests

- Added tests for RpmDatabaseReader to validate fallback to legacy packages when SQLite is missing.
- Implemented helper methods to create legacy package files and RPM headers for testing.

test: Implement dual signing tests

- Added DualSignTests to validate secondary signature addition when configured.
- Created stub implementations for crypto providers and key resolvers to facilitate testing.

chore: Update CI script for Playwright Chromium installation

- Modified ci-console-exports.sh to ensure deterministic Chromium binary installation for console exports tests.
- Added checks for Windows compatibility and environment variable setups for Playwright browsers.
This commit is contained in:
StellaOps Bot
2025-12-07 16:24:45 +02:00
parent e3f28a21ab
commit 11597679ed
199 changed files with 9809 additions and 4404 deletions

View File

@@ -22,8 +22,6 @@ using System.Diagnostics;
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.Core.Observations;
@@ -40,6 +38,7 @@ using StellaOps.Concelier.WebService.Options;
using StellaOps.Concelier.WebService.Filters;
using StellaOps.Concelier.WebService.Services;
using StellaOps.Concelier.WebService.Telemetry;
using StellaOps.Concelier.WebService.Results;
using Serilog.Events;
using StellaOps.Plugin.DependencyInjection;
using StellaOps.Plugin.Hosting;
@@ -50,23 +49,23 @@ using StellaOps.Auth.ServerIntegration;
using StellaOps.Aoc;
using StellaOps.Concelier.WebService.Deprecation;
using StellaOps.Aoc.AspNetCore.Routing;
using StellaOps.Aoc.AspNetCore.Results;
using StellaOps.Concelier.WebService.Contracts;
using StellaOps.Concelier.WebService.Results;
using StellaOps.Concelier.Core.Aoc;
using StellaOps.Concelier.Core.Raw;
using StellaOps.Concelier.RawModels;
using StellaOps.Concelier.Storage.Postgres;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Aliases;
using StellaOps.Concelier.Storage.Postgres;
using StellaOps.Provenance.Mongo;
using StellaOps.Concelier.Core.Attestation;
using StellaOps.Concelier.Core.Signals;
using AttestationClaims = StellaOps.Concelier.Core.Attestation.AttestationClaims;
using StellaOps.Concelier.Core.Orchestration;
using System.Diagnostics.Metrics;
using StellaOps.Concelier.Models.Observations;
using StellaOps.Aoc.AspNetCore.Results;
using StellaOps.Provenance.Mongo;
using HttpResults = Microsoft.AspNetCore.Http.Results;
namespace StellaOps.Concelier.WebService
{
@@ -179,26 +178,6 @@ builder.Services.AddSingleton<MirrorRateLimiter>();
builder.Services.AddSingleton<MirrorFileLocator>();
var isTesting = builder.Environment.IsEnvironment("Testing");
var mongoBypass = isTesting || string.Equals(
Environment.GetEnvironmentVariable("CONCELIER_BYPASS_MONGO"),
"1",
StringComparison.OrdinalIgnoreCase);
if (!isTesting)
{
builder.Services.AddMongoStorage(storageOptions =>
{
storageOptions.ConnectionString = concelierOptions.Storage.Dsn;
storageOptions.DatabaseName = concelierOptions.Storage.Database;
storageOptions.CommandTimeout = TimeSpan.FromSeconds(concelierOptions.Storage.CommandTimeoutSeconds);
});
}
else
{
// In test host we entirely bypass Mongo validation/bootstrapping; tests inject fakes.
builder.Services.RemoveAll<IMongoClient>();
builder.Services.RemoveAll<IMongoDatabase>();
}
// Add PostgreSQL storage for LNM linkset cache if configured.
// This provides a PostgreSQL-backed implementation of IAdvisoryLinksetStore for the read-through cache.
@@ -511,14 +490,14 @@ app.MapGet("/.well-known/openapi", ([FromServices] OpenApiDiscoveryDocumentProvi
{
context.Response.Headers.ETag = etag;
context.Response.Headers.CacheControl = "public, max-age=300, immutable";
return Results.StatusCode(StatusCodes.Status304NotModified);
return HttpResults.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");
return HttpResults.Text(payload, "application/vnd.oai.openapi+json;version=3.1");
static bool Matches(string? candidate, string expected)
{
@@ -587,7 +566,7 @@ orchestratorGroup.MapPost("/registry", async (
await store.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
return Results.Accepted();
return HttpResults.Accepted();
}).WithName("UpsertOrchestratorRegistry");
orchestratorGroup.MapPost("/heartbeat", async (
@@ -628,7 +607,7 @@ orchestratorGroup.MapPost("/heartbeat", async (
timestamp);
await store.AppendHeartbeatAsync(heartbeat, cancellationToken).ConfigureAwait(false);
return Results.Accepted();
return HttpResults.Accepted();
}).WithName("RecordOrchestratorHeartbeat");
orchestratorGroup.MapPost("/commands", async (
@@ -672,7 +651,7 @@ orchestratorGroup.MapPost("/commands", async (
request.ExpiresAt);
await store.EnqueueCommandAsync(command, cancellationToken).ConfigureAwait(false);
return Results.Accepted();
return HttpResults.Accepted();
}).WithName("EnqueueOrchestratorCommand");
orchestratorGroup.MapGet("/commands", async (
@@ -696,7 +675,7 @@ orchestratorGroup.MapGet("/commands", async (
}
var commands = await store.GetPendingCommandsAsync(tenant, connectorId.Trim(), runId, afterSequence, cancellationToken).ConfigureAwait(false);
return Results.Ok(commands);
return HttpResults.Ok(commands);
}).WithName("GetOrchestratorCommands");
var observationsEndpoint = app.MapGet("/concelier/observations", async (
HttpContext context,
@@ -772,7 +751,7 @@ var observationsEndpoint = app.MapGet("/concelier/observations", async (
result.NextCursor,
result.HasMore);
return Results.Ok(response);
return HttpResults.Ok(response);
}).WithName("GetConcelierObservations");
const int DefaultLnmPageSize = 50;
@@ -824,7 +803,7 @@ app.MapGet("/v1/lnm/linksets", async (
items.Add(ToLnmResponse(linkset, includeConflicts.GetValueOrDefault(true), includeTimeline: false, includeObservations: false, summary));
}
return Results.Ok(new LnmLinksetPage(items, resolvedPage, resolvedPageSize, result.Total));
return HttpResults.Ok(new LnmLinksetPage(items, resolvedPage, resolvedPageSize, result.Total));
}).WithName("ListLnmLinksets");
app.MapPost("/v1/lnm/linksets/search", async (
@@ -874,7 +853,7 @@ app.MapPost("/v1/lnm/linksets/search", async (
summary));
}
return Results.Ok(new LnmLinksetPage(items, resolvedPage, resolvedPageSize, result.Total));
return HttpResults.Ok(new LnmLinksetPage(items, resolvedPage, resolvedPageSize, result.Total));
}).WithName("SearchLnmLinksets");
app.MapGet("/v1/lnm/linksets/{advisoryId}", async (
@@ -960,7 +939,7 @@ app.MapGet("/v1/lnm/linksets/{advisoryId}", async (
var summary = await BuildObservationSummaryAsync(observationQueryService, tenant!, linkset, cancellationToken).ConfigureAwait(false);
var response = ToLnmResponse(linkset, includeConflicts, includeTimeline: false, includeObservations: includeObservations, summary, cached: fromCache);
return Results.Ok(response);
return HttpResults.Ok(response);
}).WithName("GetLnmLinkset");
app.MapGet("/linksets", async (
@@ -999,7 +978,7 @@ app.MapGet("/linksets", async (
nextCursor = result.NextCursor
};
return Results.Ok(payload);
return HttpResults.Ok(payload);
}).WithName("ListLinksetsLegacy");
if (authorityConfigured)
@@ -1334,20 +1313,20 @@ var advisoryObservationsEndpoint = app.MapGet("/advisories/observations", async
var query = context.Request.Query;
// Parse query parameters
var aliases = query.TryGetValue("alias", out var aliasValues)
? AdvisoryRawRequestMapper.NormalizeStrings(aliasValues)
string[]? aliases = query.TryGetValue("alias", out var aliasValues)
? AdvisoryRawRequestMapper.NormalizeStrings(aliasValues).ToArray()
: null;
var purls = query.TryGetValue("purl", out var purlValues)
? AdvisoryRawRequestMapper.NormalizeStrings(purlValues)
string[]? purls = query.TryGetValue("purl", out var purlValues)
? AdvisoryRawRequestMapper.NormalizeStrings(purlValues).ToArray()
: null;
var cpes = query.TryGetValue("cpe", out var cpeValues)
? AdvisoryRawRequestMapper.NormalizeStrings(cpeValues)
string[]? cpes = query.TryGetValue("cpe", out var cpeValues)
? AdvisoryRawRequestMapper.NormalizeStrings(cpeValues).ToArray()
: null;
var observationIds = query.TryGetValue("id", out var idValues)
? AdvisoryRawRequestMapper.NormalizeStrings(idValues)
string[]? observationIds = query.TryGetValue("id", out var idValues)
? AdvisoryRawRequestMapper.NormalizeStrings(idValues).ToArray()
: null;
int? limit = null;
@@ -1428,14 +1407,14 @@ var advisoryLinksetsEndpoint = app.MapGet("/advisories/linksets", async (
var query = context.Request.Query;
// Parse advisory IDs (alias values like CVE-*, GHSA-*)
var advisoryIds = query.TryGetValue("advisoryId", out var advisoryIdValues)
? AdvisoryRawRequestMapper.NormalizeStrings(advisoryIdValues)
string[]? advisoryIds = query.TryGetValue("advisoryId", out var advisoryIdValues)
? AdvisoryRawRequestMapper.NormalizeStrings(advisoryIdValues).ToArray()
: (query.TryGetValue("alias", out var aliasValues)
? AdvisoryRawRequestMapper.NormalizeStrings(aliasValues)
? AdvisoryRawRequestMapper.NormalizeStrings(aliasValues).ToArray()
: null);
var sources = query.TryGetValue("source", out var sourceValues)
? AdvisoryRawRequestMapper.NormalizeStrings(sourceValues)
string[]? sources = query.TryGetValue("source", out var sourceValues)
? AdvisoryRawRequestMapper.NormalizeStrings(sourceValues).ToArray()
: null;
int? limit = null;
@@ -1496,7 +1475,8 @@ var advisoryLinksetsEndpoint = app.MapGet("/advisories/linksets", async (
linkset.Normalized.Purls,
linkset.Normalized.Cpes,
linkset.Normalized.Versions,
null) // Ranges serialized differently
null, // Ranges serialized differently
null) // Severities not yet populated
: null,
false, // Not from cache
Array.Empty<string>(),
@@ -1533,12 +1513,12 @@ var advisoryLinksetsExportEndpoint = app.MapGet("/advisories/linksets/export", a
var query = context.Request.Query;
var advisoryIds = query.TryGetValue("advisoryId", out var advisoryIdValues)
? AdvisoryRawRequestMapper.NormalizeStrings(advisoryIdValues)
string[]? advisoryIds = query.TryGetValue("advisoryId", out var advisoryIdValues)
? AdvisoryRawRequestMapper.NormalizeStrings(advisoryIdValues).ToArray()
: null;
var sources = query.TryGetValue("source", out var sourceValues)
? AdvisoryRawRequestMapper.NormalizeStrings(sourceValues)
string[]? sources = query.TryGetValue("source", out var sourceValues)
? AdvisoryRawRequestMapper.NormalizeStrings(sourceValues).ToArray()
: null;
var options = new AdvisoryLinksetQueryOptions(tenant, advisoryIds, sources, 1000, null);
@@ -1634,7 +1614,7 @@ app.MapPost("/internal/events/observations/publish", async (
published++;
}
return Results.Ok(new { tenant, published, requestedCount = request.ObservationIds.Count, timestamp = timeProvider.GetUtcNow() });
return HttpResults.Ok(new { tenant, published, requestedCount = request.ObservationIds.Count, timestamp = timeProvider.GetUtcNow() });
}).WithName("PublishObservationEvents");
// Internal endpoint for publishing linkset events to NATS/Redis.
@@ -1681,7 +1661,7 @@ app.MapPost("/internal/events/linksets/publish", async (
published++;
}
return Results.Ok(new { tenant, published, requestedCount = request.AdvisoryIds.Count, hasMore = result.HasMore, timestamp = timeProvider.GetUtcNow() });
return HttpResults.Ok(new { tenant, published, requestedCount = request.AdvisoryIds.Count, hasMore = result.HasMore, timestamp = timeProvider.GetUtcNow() });
}).WithName("PublishLinksetEvents");
var advisoryEvidenceEndpoint = app.MapGet("/vuln/evidence/advisories/{advisoryKey}", async (
@@ -1782,7 +1762,7 @@ var attestationVerifyEndpoint = app.MapPost("/internal/attestations/verify", asy
request.PipelineVersion ?? evidenceOptions.PipelineVersion ?? "git:unknown"),
cancellationToken).ConfigureAwait(false);
return Results.Json(claims);
return HttpResults.Json(claims);
}
catch (Exception ex)
{
@@ -1834,7 +1814,7 @@ var evidenceSnapshotEndpoint = app.MapGet("/obs/evidence/advisories/{advisoryKey
TransparencyPath: File.Exists(transparencyPath) ? transparencyPath : null,
PipelineVersion: options.PipelineVersion);
return Results.Json(response);
return HttpResults.Json(response);
});
if (authorityConfigured)
{
@@ -1898,7 +1878,7 @@ var evidenceAttestationEndpoint = app.MapGet("/obs/attestations/advisories/{advi
TransparencyPath: File.Exists(transparencyPath) ? transparencyPath : null,
PipelineVersion: options.PipelineVersion);
return Results.Json(response);
return HttpResults.Json(response);
});
if (authorityConfigured)
{
@@ -1927,7 +1907,7 @@ var incidentGetEndpoint = app.MapGet("/obs/incidents/advisories/{advisoryKey}",
return Problem(context, "Incident not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, "No incident marker present.");
}
return Results.Json(status);
return HttpResults.Json(status);
});
if (authorityConfigured)
{
@@ -1967,7 +1947,7 @@ var incidentUpsertEndpoint = app.MapPost("/obs/incidents/advisories/{advisoryKey
cancellationToken).ConfigureAwait(false);
var status = await IncidentFileStore.ReadAsync(evidenceOptions, tenant!, advisoryKey, timeProvider.GetUtcNow(), cancellationToken).ConfigureAwait(false);
return Results.Json(status);
return HttpResults.Json(status);
});
if (authorityConfigured)
{
@@ -1989,7 +1969,7 @@ var incidentDeleteEndpoint = app.MapDelete("/obs/incidents/advisories/{advisoryK
var evidenceOptions = concelierOptions.Value.Evidence ?? new ConcelierOptions.EvidenceBundleOptions();
await IncidentFileStore.DeleteAsync(evidenceOptions, tenant!, advisoryKey, cancellationToken).ConfigureAwait(false);
return Results.NoContent();
return HttpResults.NoContent();
});
if (authorityConfigured)
{
@@ -2224,7 +2204,7 @@ var advisorySummaryEndpoint = app.MapGet("/advisories/summary", async (
context.Response.Headers["X-Stella-Cache-Ttl"] = "0";
var response = AdvisorySummaryMapper.ToResponse(normalizedTenant, orderedItems, nextCursor, sortKey);
return Results.Ok(response);
return HttpResults.Ok(response);
}).WithName("GetAdvisoriesSummary");
// Evidence batch (component-centric) endpoint for graph overlays / evidence exports.
@@ -2292,7 +2272,7 @@ app.MapPost("/v1/evidence/batch", async (
responses.Add(responseItem);
}
return Results.Ok(new EvidenceBatchResponse(responses));
return HttpResults.Ok(new EvidenceBatchResponse(responses));
}).WithName("GetEvidenceBatch");
if (authorityConfigured)
@@ -2384,6 +2364,7 @@ if (authorityConfigured)
app.MapGet("/concelier/advisories/{vulnerabilityKey}/replay", async (
string vulnerabilityKey,
HttpContext context,
DateTimeOffset? asOf,
[FromServices] IAdvisoryEventLog eventLog,
CancellationToken cancellationToken) =>
@@ -2468,7 +2449,7 @@ var statementProvenanceEndpoint = app.MapPost("/events/statements/{statementId:g
return Problem(context, "Statement not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, ex.Message);
}
return Results.Accepted($"/events/statements/{statementId}");
return HttpResults.Accepted($"/events/statements/{statementId}");
});
if (authorityConfigured)
@@ -2509,7 +2490,7 @@ app.UseExceptionHandler(errorApp =>
["traceId"] = Activity.Current?.TraceId.ToString() ?? context.TraceIdentifier,
};
var problem = Results.Problem(
var problem = HttpResults.Problem(
detail: error?.Message,
instance: context.Request.Path,
statusCode: StatusCodes.Status500InternalServerError,
@@ -2752,7 +2733,7 @@ IReadOnlyList<LnmLinksetTimeline> BuildTimeline(AdvisoryLinkset linkset, Linkset
IResult JsonResult<T>(T value, int? statusCode = null)
{
var payload = JsonSerializer.Serialize(value, JsonOptions);
return Results.Content(payload, "application/json", Encoding.UTF8, statusCode);
return HttpResults.Content(payload, "application/json", Encoding.UTF8, statusCode);
}
IResult Problem(HttpContext context, string title, int statusCode, string type, string? detail = null, IDictionary<string, object?>? extensions = null, string? errorCode = null)
@@ -2789,7 +2770,7 @@ IResult Problem(HttpContext context, string title, int statusCode, string type,
}
var payload = JsonSerializer.Serialize(problemDetails, JsonOptions);
return Results.Content(payload, "application/problem+json", Encoding.UTF8, statusCode);
return HttpResults.Content(payload, "application/problem+json", Encoding.UTF8, statusCode);
}
bool TryResolveTenant(HttpContext context, bool requireHeader, out string tenant, out IResult? error)
@@ -2833,14 +2814,14 @@ IResult? EnsureTenantAuthorized(HttpContext context, string tenant)
if (enforceTenantAllowlist && !requiredTenants.Contains(tenant))
{
return Results.Forbid();
return HttpResults.Forbid();
}
var principal = context.User;
if (enforceAuthority && (principal?.Identity?.IsAuthenticated != true))
{
return Results.Unauthorized();
return HttpResults.Unauthorized();
}
if (principal?.Identity?.IsAuthenticated == true)
@@ -2848,18 +2829,18 @@ IResult? EnsureTenantAuthorized(HttpContext context, string tenant)
var tenantClaim = principal.FindFirstValue(StellaOpsClaimTypes.Tenant);
if (string.IsNullOrWhiteSpace(tenantClaim))
{
return Results.Forbid();
return HttpResults.Forbid();
}
var normalizedClaim = tenantClaim.Trim().ToLowerInvariant();
if (!string.Equals(normalizedClaim, tenant, StringComparison.Ordinal))
{
return Results.Forbid();
return HttpResults.Forbid();
}
if (enforceTenantAllowlist && !requiredTenants.Contains(normalizedClaim))
{
return Results.Forbid();
return HttpResults.Forbid();
}
}
@@ -3319,62 +3300,26 @@ app.MapGet("/health", ([FromServices] IOptions<ConcelierOptions> opts, [FromServ
return JsonResult(response);
});
app.MapGet("/ready", async ([FromServices] IMongoDatabase database, [FromServices] StellaOps.Concelier.WebService.Diagnostics.ServiceStatus status, HttpContext context, CancellationToken cancellationToken) =>
app.MapGet("/ready", ([FromServices] StellaOps.Concelier.WebService.Diagnostics.ServiceStatus status, HttpContext context) =>
{
ApplyNoCache(context.Response);
var stopwatch = Stopwatch.StartNew();
try
{
await database.RunCommandAsync((Command<BsonDocument>)"{ 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 snapshot = status.CreateSnapshot();
var uptimeSeconds = Math.Max((snapshot.CapturedAt - snapshot.StartedAt).TotalSeconds, 0d);
var mongo = new MongoReadyHealth(
Status: "bypassed",
LatencyMs: null,
CheckedAt: snapshot.LastReadyCheckAt,
Error: "mongo disabled");
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);
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<string, object?>(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);
}
return JsonResult(response);
});
app.MapGet("/diagnostics/aliases/{seed}", async (string seed, [FromServices] AliasGraphResolver resolver, HttpContext context, CancellationToken cancellationToken) =>
@@ -3553,7 +3498,7 @@ var triggerJobEndpoint = app.MapPost("/jobs/{*jobKind}", async (string jobKind,
JobMetrics.TriggerCounter.Add(1, tags);
if (result.Run is null)
{
return Results.StatusCode(StatusCodes.Status202Accepted);
return HttpResults.StatusCode(StatusCodes.Status202Accepted);
}
var acceptedRun = JobRunResponse.FromSnapshot(result.Run);
@@ -3638,7 +3583,7 @@ var concelierHealthEndpoint = app.MapGet("/obs/concelier/health", (
Window: "5m",
UpdatedAt: now.ToString("O", CultureInfo.InvariantCulture));
return Results.Ok(payload);
return HttpResults.Ok(payload);
});
var concelierTimelineEndpoint = app.MapGet("/obs/concelier/timeline", async (
@@ -3702,7 +3647,7 @@ var concelierTimelineEndpoint = app.MapGet("/obs/concelier/timeline", async (
context.Response.Headers["X-Next-Cursor"] = nextCursor.ToString(CultureInfo.InvariantCulture);
logger.LogInformation("obs timeline emitted {Count} events for tenant {Tenant} starting at {StartId} next {Next}", events.Count, tenant, startId, nextCursor);
return Results.Empty;
return HttpResults.Empty;
});
// ==========================================
@@ -3774,7 +3719,7 @@ app.MapGet("/v1/signals/symbols", async (
var result = await symbolProvider.QueryAsync(options, cancellationToken);
return Results.Ok(new SignalsSymbolQueryResponse(
return HttpResults.Ok(new SignalsSymbolQueryResponse(
Symbols: result.Symbols.Select(s => ToSymbolResponse(s)).ToList(),
TotalCount: result.TotalCount,
HasMore: result.HasMore,
@@ -3807,7 +3752,7 @@ app.MapGet("/v1/signals/symbols/advisory/{advisoryId}", async (
var symbolSet = await symbolProvider.GetByAdvisoryAsync(tenant!, advisoryId.Trim(), cancellationToken);
return Results.Ok(ToSymbolSetResponse(symbolSet));
return HttpResults.Ok(ToSymbolSetResponse(symbolSet));
}).WithName("GetAffectedSymbolsByAdvisory");
app.MapGet("/v1/signals/symbols/package/{*purl}", async (
@@ -3831,7 +3776,7 @@ app.MapGet("/v1/signals/symbols/package/{*purl}", async (
if (string.IsNullOrWhiteSpace(purl))
{
return Problem(
return HttpResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "Package URL required",
detail: "The purl parameter is required.",
@@ -3840,7 +3785,7 @@ app.MapGet("/v1/signals/symbols/package/{*purl}", async (
var symbolSet = await symbolProvider.GetByPackageAsync(tenant!, purl.Trim(), cancellationToken);
return Results.Ok(ToSymbolSetResponse(symbolSet));
return HttpResults.Ok(ToSymbolSetResponse(symbolSet));
}).WithName("GetAffectedSymbolsByPackage");
app.MapPost("/v1/signals/symbols/batch", async (
@@ -3864,7 +3809,7 @@ app.MapPost("/v1/signals/symbols/batch", async (
if (request.AdvisoryIds is not { Count: > 0 })
{
return Problem(
return HttpResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "Advisory IDs required",
detail: "At least one advisoryId is required in the batch request.",
@@ -3873,7 +3818,7 @@ app.MapPost("/v1/signals/symbols/batch", async (
if (request.AdvisoryIds.Count > 100)
{
return Problem(
return HttpResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "Batch size exceeded",
detail: "Maximum batch size is 100 advisory IDs.",
@@ -3887,7 +3832,7 @@ app.MapPost("/v1/signals/symbols/batch", async (
kvp => kvp.Key,
kvp => ToSymbolSetResponse(kvp.Value)));
return Results.Ok(response);
return HttpResults.Ok(response);
}).WithName("GetAffectedSymbolsBatch");
app.MapGet("/v1/signals/symbols/exists/{advisoryId}", async (
@@ -3916,7 +3861,7 @@ app.MapGet("/v1/signals/symbols/exists/{advisoryId}", async (
var exists = await symbolProvider.HasSymbolsAsync(tenant!, advisoryId.Trim(), cancellationToken);
return Results.Ok(new SignalsSymbolExistsResponse(Exists: exists, AdvisoryId: advisoryId.Trim()));
return HttpResults.Ok(new SignalsSymbolExistsResponse(Exists: exists, AdvisoryId: advisoryId.Trim()));
}).WithName("CheckAffectedSymbolsExist");
await app.RunAsync();
@@ -4076,41 +4021,7 @@ static SignalsSymbolSetResponse ToSymbolSetResponse(AffectedSymbolSet symbolSet)
static async Task InitializeMongoAsync(WebApplication app)
{
// Skip Mongo initialization in testing/bypass mode.
var isTesting = string.Equals(
Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"),
"Testing",
StringComparison.OrdinalIgnoreCase);
var bypass = string.Equals(
Environment.GetEnvironmentVariable("CONCELIER_BYPASS_MONGO"),
"1",
StringComparison.OrdinalIgnoreCase);
if (isTesting || bypass)
{
return;
}
await using var scope = app.Services.CreateAsyncScope();
var bootstrapper = scope.ServiceProvider.GetRequiredService<MongoBootstrapper>();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("MongoBootstrapper");
var status = scope.ServiceProvider.GetRequiredService<StellaOps.Concelier.WebService.Diagnostics.ServiceStatus>();
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;
}
await Task.CompletedTask;
}
}