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
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:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user