up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
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
devportal-offline / build-offline (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-25 22:09:44 +02:00
parent 6bee1fdcf5
commit 9f6e6f7fb3
116 changed files with 4495 additions and 730 deletions

View File

@@ -19,8 +19,6 @@ 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;
@@ -32,11 +30,11 @@ using StellaOps.Concelier.Core.Observations;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.WebService.Diagnostics;
using ServiceStatus = StellaOps.Concelier.WebService.Diagnostics.ServiceStatus;
using Serilog;
using StellaOps.Concelier.Merge;
using StellaOps.Concelier.Merge.Services;
using StellaOps.Concelier.WebService.Extensions;
using StellaOps.Concelier.WebService.Services;
using StellaOps.Concelier.WebService.Jobs;
using StellaOps.Concelier.WebService.Options;
using StellaOps.Concelier.WebService.Filters;
@@ -61,8 +59,8 @@ using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Aliases;
using StellaOps.Provenance.Mongo;
using StellaOps.Concelier.Core.Attestation;
using AttestationClaims = StellaOps.Concelier.Core.Attestation.AttestationClaims;
using StellaOps.Concelier.Storage.Mongo.Orchestrator;
using System.Security.Cryptography;
using System.Diagnostics.Metrics;
using StellaOps.Concelier.Models.Observations;
@@ -117,28 +115,33 @@ var contentRootPath = builder.Environment.ContentRootPath;
ConcelierOptions concelierOptions;
if (builder.Environment.IsEnvironment("Testing"))
{
// Allow a fully pre-bound options instance to be supplied by the test host.
#pragma warning disable ASP0000 // test-only: create provider to fetch pre-bound options
using var tempProvider = builder.Services.BuildServiceProvider();
#pragma warning restore ASP0000
concelierOptions = tempProvider.GetService<IOptions<ConcelierOptions>>()?.Value ?? new ConcelierOptions
{
Storage = new ConcelierOptions.StorageOptions
// Allow a fully pre-bound options instance to be supplied by the test host.
#pragma warning disable ASP0000 // test-only: create provider to fetch pre-bound options
using var tempProvider = builder.Services.BuildServiceProvider();
#pragma warning restore ASP0000
concelierOptions = tempProvider.GetService<IOptions<ConcelierOptions>>()?.Value ?? new ConcelierOptions
{
Dsn = Environment.GetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN") ?? "mongodb://localhost:27017/test-health",
Driver = "mongo",
CommandTimeoutSeconds = 30
},
Telemetry = new ConcelierOptions.TelemetryOptions
{
Enabled = false
}
};
Storage = new ConcelierOptions.StorageOptions
{
Dsn = Environment.GetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN") ?? "mongodb://localhost:27017/test-health",
Driver = "mongo",
CommandTimeoutSeconds = 30
},
Telemetry = new ConcelierOptions.TelemetryOptions
{
Enabled = false
}
};
ConcelierOptionsPostConfigure.Apply(concelierOptions, contentRootPath);
// Skip validation in Testing to allow factory-provided wiring.
}
concelierOptions.Storage ??= new ConcelierOptions.StorageOptions();
concelierOptions.Storage.Dsn = Environment.GetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN") ?? "mongodb://localhost:27017/orch-tests";
concelierOptions.Storage.Driver = "mongo";
concelierOptions.Storage.CommandTimeoutSeconds = concelierOptions.Storage.CommandTimeoutSeconds <= 0 ? 30 : concelierOptions.Storage.CommandTimeoutSeconds;
ConcelierOptionsPostConfigure.Apply(concelierOptions, contentRootPath);
// Skip validation in Testing to allow factory-provided wiring.
}
else
{
concelierOptions = builder.Configuration.BindOptions<ConcelierOptions>(postConfigure: (opts, _) =>
@@ -171,12 +174,27 @@ builder.Services.AddMemoryCache();
builder.Services.AddSingleton<MirrorRateLimiter>();
builder.Services.AddSingleton<MirrorFileLocator>();
builder.Services.AddMongoStorage(storageOptions =>
var isTesting = builder.Environment.IsEnvironment("Testing");
var mongoBypass = isTesting || string.Equals(
Environment.GetEnvironmentVariable("CONCELIER_BYPASS_MONGO"),
"1",
StringComparison.OrdinalIgnoreCase);
if (!isTesting)
{
storageOptions.ConnectionString = concelierOptions.Storage.Dsn;
storageOptions.DatabaseName = concelierOptions.Storage.Database;
storageOptions.CommandTimeout = TimeSpan.FromSeconds(concelierOptions.Storage.CommandTimeoutSeconds);
});
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>();
}
builder.Services.AddOptions<AdvisoryObservationEventPublisherOptions>()
.Bind(builder.Configuration.GetSection("advisoryObservationEvents"))
.PostConfigure(options =>
@@ -229,7 +247,7 @@ builder.Services.PostConfigure<JobSchedulerOptions>(options =>
});
builder.Services.AddSingleton<OpenApiDiscoveryDocumentProvider>();
builder.Services.AddSingleton<ServiceStatus>(sp => new ServiceStatus(sp.GetRequiredService<TimeProvider>()));
builder.Services.AddSingleton<StellaOps.Concelier.WebService.Diagnostics.ServiceStatus>(sp => new StellaOps.Concelier.WebService.Diagnostics.ServiceStatus(sp.GetRequiredService<TimeProvider>()));
builder.Services.AddAocGuard();
var authorityConfigured = concelierOptions.Authority is { Enabled: true };
@@ -743,7 +761,7 @@ app.MapGet("/v1/lnm/linksets", async (
resolvedPageSize,
cancellationToken).ConfigureAwait(false);
var items = new List<LnmLinksetResponse>(result.Items.Length);
var items = new List<LnmLinksetResponse>(result.Items.Count);
foreach (var linkset in result.Items)
{
var summary = await BuildObservationSummaryAsync(observationQueryService, tenant!, linkset, cancellationToken).ConfigureAwait(false);
@@ -788,7 +806,7 @@ app.MapPost("/v1/lnm/linksets/search", async (
resolvedPageSize,
cancellationToken).ConfigureAwait(false);
var items = new List<LnmLinksetResponse>(result.Items.Length);
var items = new List<LnmLinksetResponse>(result.Items.Count);
foreach (var linkset in result.Items)
{
var summary = await BuildObservationSummaryAsync(observationQueryService, tenant!, linkset, cancellationToken).ConfigureAwait(false);
@@ -1346,7 +1364,7 @@ var evidenceSnapshotEndpoint = app.MapGet("/obs/evidence/advisories/{advisoryKey
var hash = await ComputeSha256Async(manifestStream, cancellationToken).ConfigureAwait(false);
var response = new EvidenceSnapshotResponse(
advisoryKey: advisoryKey.Trim(),
AdvisoryKey: advisoryKey.Trim(),
Tenant: tenant,
ManifestPath: manifestPath,
ManifestHash: hash,
@@ -2241,33 +2259,8 @@ async Task<LinksetObservationSummary> BuildObservationSummaryAsync(
return LinksetObservationSummary.Empty;
}
var published = result.Observations
.Where(o => o.Published.HasValue)
.Select(o => o.Published!.Value)
.OrderBy(p => p)
.FirstOrDefault();
var modified = result.Observations
.Where(o => o.Modified.HasValue)
.Select(o => o.Modified!.Value)
.OrderByDescending(p => p)
.FirstOrDefault();
var severity = result.Observations
.SelectMany(o => o.Severities)
.OrderByDescending(s => s.Score)
.FirstOrDefault();
var severityText = severity is null ? null : $"{severity.System}:{severity.Score:0.0}";
var evidenceHash = result.Observations
.Select(o => o.Provenance.SourceArtifactSha)
.FirstOrDefault();
return new LinksetObservationSummary(
PublishedAt: published == default ? null : published,
ModifiedAt: modified == default ? null : modified,
Severity: severityText,
EvidenceHash: evidenceHash);
// Observation timelines are not yet populated; return empty summary until ingestion enriches these fields.
return LinksetObservationSummary.Empty;
}
IReadOnlyList<LnmLinksetTimeline> BuildTimeline(AdvisoryLinkset linkset, LinksetObservationSummary summary)
@@ -2290,15 +2283,6 @@ IReadOnlyList<LnmLinksetTimeline> BuildTimeline(AdvisoryLinkset linkset, Linkset
return timeline;
}
readonly record struct LinksetObservationSummary(
DateTimeOffset? PublishedAt,
DateTimeOffset? ModifiedAt,
string? Severity,
string? EvidenceHash)
{
public static LinksetObservationSummary Empty { get; } = new(null, null, null, null);
}
IResult JsonResult<T>(T value, int? statusCode = null)
{
var payload = JsonSerializer.Serialize(value, JsonOptions);
@@ -2834,7 +2818,7 @@ void ApplyNoCache(HttpResponse response)
await InitializeMongoAsync(app);
app.MapGet("/health", ([FromServices] IOptions<ConcelierOptions> opts, [FromServices] ServiceStatus status, HttpContext context) =>
app.MapGet("/health", ([FromServices] IOptions<ConcelierOptions> opts, [FromServices] StellaOps.Concelier.WebService.Diagnostics.ServiceStatus status, HttpContext context) =>
{
ApplyNoCache(context.Response);
@@ -2863,7 +2847,7 @@ app.MapGet("/health", ([FromServices] IOptions<ConcelierOptions> opts, [FromServ
return JsonResult(response);
});
app.MapGet("/ready", async ([FromServices] IMongoDatabase database, [FromServices] ServiceStatus status, HttpContext context, CancellationToken cancellationToken) =>
app.MapGet("/ready", async ([FromServices] IMongoDatabase database, [FromServices] StellaOps.Concelier.WebService.Diagnostics.ServiceStatus status, HttpContext context, CancellationToken cancellationToken) =>
{
ApplyNoCache(context.Response);
@@ -3252,13 +3236,23 @@ var concelierTimelineEndpoint = app.MapGet("/obs/concelier/timeline", async (
await app.RunAsync();
}
static JsonSerializerOptions CreateJsonOptions()
static JsonSerializerOptions CreateJsonOptions()
{
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
options.Converters.Add(new JsonStringEnumConverter());
return options;
}
// Linkset summary used by advisory summary timeline
private readonly record struct LinksetObservationSummary(
DateTimeOffset? PublishedAt,
DateTimeOffset? ModifiedAt,
string? Severity,
string? EvidenceHash)
{
public static LinksetObservationSummary Empty { get; } = new(null, null, null, null);
}
static PluginHostOptions BuildPluginOptions(ConcelierOptions options, string contentRoot)
{
var pluginOptions = new PluginHostOptions
@@ -3293,7 +3287,7 @@ static async Task InitializeMongoAsync(WebApplication app)
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<ServiceStatus>();
var status = scope.ServiceProvider.GetRequiredService<StellaOps.Concelier.WebService.Diagnostics.ServiceStatus>();
var stopwatch = Stopwatch.StartNew();