more audit work

This commit is contained in:
master
2026-01-08 10:21:51 +02:00
parent 43c02081ef
commit 51cf4bc16c
546 changed files with 36721 additions and 4003 deletions

View File

@@ -25,8 +25,8 @@ internal static class AirGapEndpointExtensions
// GET /api/v1/concelier/airgap/catalog - Aggregated bundle catalog
group.MapGet("/catalog", async (
HttpContext context,
IBundleCatalogService catalogService,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IBundleCatalogService catalogService,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromQuery] string? cursor,
[FromQuery] int? limit,
CancellationToken cancellationToken) =>
@@ -46,8 +46,8 @@ internal static class AirGapEndpointExtensions
// GET /api/v1/concelier/airgap/sources - List registered sources
group.MapGet("/sources", (
HttpContext context,
IBundleSourceRegistry sourceRegistry,
IOptionsMonitor<ConcelierOptions> optionsMonitor) =>
[FromServices] IBundleSourceRegistry sourceRegistry,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor) =>
{
var airGapOptions = optionsMonitor.CurrentValue.AirGap;
if (!airGapOptions.Enabled)
@@ -62,8 +62,8 @@ internal static class AirGapEndpointExtensions
// POST /api/v1/concelier/airgap/sources - Register new source
group.MapPost("/sources", async (
HttpContext context,
IBundleSourceRegistry sourceRegistry,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IBundleSourceRegistry sourceRegistry,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromBody] BundleSourceRegistration registration,
CancellationToken cancellationToken) =>
{
@@ -87,8 +87,8 @@ internal static class AirGapEndpointExtensions
// GET /api/v1/concelier/airgap/sources/{sourceId} - Get specific source
group.MapGet("/sources/{sourceId}", (
HttpContext context,
IBundleSourceRegistry sourceRegistry,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IBundleSourceRegistry sourceRegistry,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
string sourceId) =>
{
var airGapOptions = optionsMonitor.CurrentValue.AirGap;
@@ -109,8 +109,8 @@ internal static class AirGapEndpointExtensions
// DELETE /api/v1/concelier/airgap/sources/{sourceId} - Unregister source
group.MapDelete("/sources/{sourceId}", async (
HttpContext context,
IBundleSourceRegistry sourceRegistry,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IBundleSourceRegistry sourceRegistry,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
string sourceId,
CancellationToken cancellationToken) =>
{
@@ -131,8 +131,8 @@ internal static class AirGapEndpointExtensions
// POST /api/v1/concelier/airgap/sources/{sourceId}/validate - Validate source
group.MapPost("/sources/{sourceId}/validate", async (
HttpContext context,
IBundleSourceRegistry sourceRegistry,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IBundleSourceRegistry sourceRegistry,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
string sourceId,
CancellationToken cancellationToken) =>
{
@@ -151,8 +151,8 @@ internal static class AirGapEndpointExtensions
// GET /api/v1/concelier/airgap/status - Sealed-mode status
group.MapGet("/status", (
HttpContext context,
ISealedModeEnforcer sealedModeEnforcer,
IOptionsMonitor<ConcelierOptions> optionsMonitor) =>
[FromServices] ISealedModeEnforcer sealedModeEnforcer,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor) =>
{
var airGapOptions = optionsMonitor.CurrentValue.AirGap;
if (!airGapOptions.Enabled)
@@ -168,9 +168,9 @@ internal static class AirGapEndpointExtensions
// Per CONCELIER-WEB-AIRGAP-58-001
group.MapPost("/bundles/{bundleId}/import", async (
HttpContext context,
IBundleCatalogService catalogService,
IBundleTimelineEmitter timelineEmitter,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IBundleCatalogService catalogService,
[FromServices] IBundleTimelineEmitter timelineEmitter,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
string bundleId,
[FromBody] BundleImportRequestDto requestDto,
CancellationToken cancellationToken) =>

View File

@@ -30,8 +30,8 @@ internal static class CanonicalAdvisoryEndpointExtensions
// GET /api/v1/canonical/{id} - Get canonical advisory by ID
group.MapGet("/{id:guid}", async (
Guid id,
ICanonicalAdvisoryService service,
IInterestScoringService? scoringService,
[FromServices] ICanonicalAdvisoryService service,
[FromServices] IInterestScoringService? scoringService,
HttpContext context,
CancellationToken ct) =>
{
@@ -63,7 +63,7 @@ internal static class CanonicalAdvisoryEndpointExtensions
[FromQuery] string? mergeHash,
[FromQuery] int? offset,
[FromQuery] int? limit,
ICanonicalAdvisoryService service,
[FromServices] ICanonicalAdvisoryService service,
HttpContext context,
CancellationToken ct) =>
{
@@ -126,7 +126,7 @@ internal static class CanonicalAdvisoryEndpointExtensions
group.MapPost("/ingest/{source}", async (
string source,
[FromBody] RawAdvisoryRequest request,
ICanonicalAdvisoryService service,
[FromServices] ICanonicalAdvisoryService service,
HttpContext context,
CancellationToken ct) =>
{
@@ -187,7 +187,7 @@ internal static class CanonicalAdvisoryEndpointExtensions
group.MapPost("/ingest/{source}/batch", async (
string source,
[FromBody] IEnumerable<RawAdvisoryRequest> requests,
ICanonicalAdvisoryService service,
[FromServices] ICanonicalAdvisoryService service,
HttpContext context,
CancellationToken ct) =>
{
@@ -246,7 +246,7 @@ internal static class CanonicalAdvisoryEndpointExtensions
group.MapPatch("/{id:guid}/status", async (
Guid id,
[FromBody] UpdateStatusRequest request,
ICanonicalAdvisoryService service,
[FromServices] ICanonicalAdvisoryService service,
HttpContext context,
CancellationToken ct) =>
{
@@ -267,8 +267,8 @@ internal static class CanonicalAdvisoryEndpointExtensions
// GET /api/v1/canonical/{id}/provenance - Get provenance scopes for canonical
group.MapGet("/{id:guid}/provenance", async (
Guid id,
IProvenanceScopeService? provenanceService,
ICanonicalAdvisoryService canonicalService,
[FromServices] IProvenanceScopeService? provenanceService,
[FromServices] ICanonicalAdvisoryService canonicalService,
HttpContext context,
CancellationToken ct) =>
{

View File

@@ -23,8 +23,8 @@ internal static class FederationEndpointExtensions
// GET /api/v1/federation/export - Export delta bundle
group.MapGet("/export", async (
HttpContext context,
IBundleExportService exportService,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IBundleExportService exportService,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
CancellationToken cancellationToken,
[FromQuery(Name = "since_cursor")] string? sinceCursor = null,
[FromQuery] bool sign = true,
@@ -83,8 +83,8 @@ internal static class FederationEndpointExtensions
// GET /api/v1/federation/export/preview - Preview export statistics
group.MapGet("/export/preview", async (
HttpContext context,
IBundleExportService exportService,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IBundleExportService exportService,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
CancellationToken cancellationToken,
[FromQuery(Name = "since_cursor")] string? sinceCursor = null) =>
{
@@ -114,7 +114,7 @@ internal static class FederationEndpointExtensions
// GET /api/v1/federation/status - Federation status
group.MapGet("/status", (
HttpContext context,
IOptionsMonitor<ConcelierOptions> optionsMonitor) =>
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor) =>
{
var options = optionsMonitor.CurrentValue;
@@ -134,8 +134,8 @@ internal static class FederationEndpointExtensions
// Per SPRINT_8200_0014_0003_CONCEL_bundle_import_merge Task 25-26.
group.MapPost("/import", async (
HttpContext context,
IBundleImportService importService,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IBundleImportService importService,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
CancellationToken cancellationToken,
[FromQuery(Name = "dry_run")] bool dryRun = false,
[FromQuery(Name = "skip_signature")] bool skipSignature = false,
@@ -230,8 +230,8 @@ internal static class FederationEndpointExtensions
// POST /api/v1/federation/import/validate - Validate bundle without importing
group.MapPost("/import/validate", async (
HttpContext context,
IBundleImportService importService,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IBundleImportService importService,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
CancellationToken cancellationToken) =>
{
var options = optionsMonitor.CurrentValue;
@@ -264,8 +264,8 @@ internal static class FederationEndpointExtensions
// POST /api/v1/federation/import/preview - Preview import
group.MapPost("/import/preview", async (
HttpContext context,
IBundleImportService importService,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IBundleImportService importService,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
CancellationToken cancellationToken) =>
{
var options = optionsMonitor.CurrentValue;
@@ -313,8 +313,8 @@ internal static class FederationEndpointExtensions
// Per SPRINT_8200_0014_0003_CONCEL_bundle_import_merge Task 30.
group.MapGet("/sites", async (
HttpContext context,
ISyncLedgerRepository ledgerRepository,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] ISyncLedgerRepository ledgerRepository,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
CancellationToken cancellationToken,
[FromQuery(Name = "enabled_only")] bool enabledOnly = false) =>
{
@@ -350,8 +350,8 @@ internal static class FederationEndpointExtensions
// GET /api/v1/federation/sites/{siteId} - Get site details
group.MapGet("/sites/{siteId}", async (
HttpContext context,
ISyncLedgerRepository ledgerRepository,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] ISyncLedgerRepository ledgerRepository,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
string siteId,
CancellationToken cancellationToken) =>
{
@@ -404,8 +404,8 @@ internal static class FederationEndpointExtensions
// Per SPRINT_8200_0014_0003_CONCEL_bundle_import_merge Task 31.
group.MapPut("/sites/{siteId}/policy", async (
HttpContext context,
ISyncLedgerRepository ledgerRepository,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] ISyncLedgerRepository ledgerRepository,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
string siteId,
[FromBody] SitePolicyUpdateRequest request,
CancellationToken cancellationToken) =>

View File

@@ -79,8 +79,8 @@ internal static class FeedSnapshotEndpointExtensions
private static async Task<IResult> CreateSnapshotAsync(
HttpContext context,
IFeedSnapshotCoordinator coordinator,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IFeedSnapshotCoordinator coordinator,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromBody] CreateSnapshotRequest? request,
CancellationToken cancellationToken)
{
@@ -129,8 +129,8 @@ internal static class FeedSnapshotEndpointExtensions
private static async Task<IResult> ListSnapshotsAsync(
HttpContext context,
IFeedSnapshotCoordinator coordinator,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IFeedSnapshotCoordinator coordinator,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromQuery] int? limit,
[FromQuery] string? cursor,
CancellationToken cancellationToken)
@@ -165,8 +165,8 @@ internal static class FeedSnapshotEndpointExtensions
private static async Task<IResult> GetSnapshotAsync(
HttpContext context,
IFeedSnapshotCoordinator coordinator,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IFeedSnapshotCoordinator coordinator,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
string snapshotId,
CancellationToken cancellationToken)
{
@@ -201,8 +201,8 @@ internal static class FeedSnapshotEndpointExtensions
private static async Task<IResult> ExportSnapshotAsync(
HttpContext context,
IFeedSnapshotCoordinator coordinator,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IFeedSnapshotCoordinator coordinator,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
string snapshotId,
[FromQuery] string? format,
CancellationToken cancellationToken)
@@ -242,8 +242,8 @@ internal static class FeedSnapshotEndpointExtensions
private static async Task<IResult> ImportSnapshotAsync(
HttpContext context,
IFeedSnapshotCoordinator coordinator,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IFeedSnapshotCoordinator coordinator,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
IFormFile file,
[FromQuery] bool? validate,
CancellationToken cancellationToken)
@@ -293,8 +293,8 @@ internal static class FeedSnapshotEndpointExtensions
private static async Task<IResult> ValidateSnapshotAsync(
HttpContext context,
IFeedSnapshotCoordinator coordinator,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] IFeedSnapshotCoordinator coordinator,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
string snapshotId,
CancellationToken cancellationToken)
{
@@ -330,8 +330,8 @@ internal static class FeedSnapshotEndpointExtensions
private static IResult ListSourcesAsync(
HttpContext context,
IFeedSnapshotCoordinator coordinator,
IOptionsMonitor<ConcelierOptions> optionsMonitor)
[FromServices] IFeedSnapshotCoordinator coordinator,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor)
{
var options = optionsMonitor.CurrentValue;

View File

@@ -28,7 +28,7 @@ internal static class InterestScoreEndpointExtensions
// GET /api/v1/canonical/{id}/score - Get interest score for a canonical advisory
group.MapGet("/canonical/{id:guid}/score", async (
Guid id,
IInterestScoringService scoringService,
[FromServices] IInterestScoringService scoringService,
CancellationToken ct) =>
{
var score = await scoringService.GetScoreAsync(id, ct).ConfigureAwait(false);
@@ -48,7 +48,7 @@ internal static class InterestScoreEndpointExtensions
[FromQuery] double? maxScore,
[FromQuery] int? offset,
[FromQuery] int? limit,
IInterestScoreRepository repository,
[FromServices] IInterestScoreRepository repository,
CancellationToken ct) =>
{
var scores = await repository.GetAllAsync(offset ?? 0, limit ?? 50, ct).ConfigureAwait(false);
@@ -80,7 +80,7 @@ internal static class InterestScoreEndpointExtensions
// GET /api/v1/scores/distribution - Get score distribution statistics
group.MapGet("/scores/distribution", async (
IInterestScoreRepository repository,
[FromServices] IInterestScoreRepository repository,
CancellationToken ct) =>
{
var distribution = await repository.GetScoreDistributionAsync(ct).ConfigureAwait(false);
@@ -103,7 +103,7 @@ internal static class InterestScoreEndpointExtensions
// POST /api/v1/canonical/{id}/score/compute - Compute score for a canonical
group.MapPost("/canonical/{id:guid}/score/compute", async (
Guid id,
IInterestScoringService scoringService,
[FromServices] IInterestScoringService scoringService,
CancellationToken ct) =>
{
var score = await scoringService.ComputeScoreAsync(id, ct).ConfigureAwait(false);
@@ -118,7 +118,7 @@ internal static class InterestScoreEndpointExtensions
// POST /api/v1/scores/recalculate - Admin endpoint to trigger full recalculation
group.MapPost("/scores/recalculate", async (
[FromBody] RecalculateRequest? request,
IInterestScoringService scoringService,
[FromServices] IInterestScoringService scoringService,
CancellationToken ct) =>
{
int updated;
@@ -147,8 +147,8 @@ internal static class InterestScoreEndpointExtensions
// POST /api/v1/scores/degrade - Admin endpoint to run stub degradation
group.MapPost("/scores/degrade", async (
[FromBody] DegradeRequest? request,
IInterestScoringService scoringService,
Microsoft.Extensions.Options.IOptions<InterestScoreOptions> options,
[FromServices] IInterestScoringService scoringService,
[FromServices] Microsoft.Extensions.Options.IOptions<InterestScoreOptions> options,
CancellationToken ct) =>
{
var threshold = request?.Threshold ?? options.Value.DegradationPolicy.DegradationThreshold;
@@ -169,8 +169,8 @@ internal static class InterestScoreEndpointExtensions
// POST /api/v1/scores/restore - Admin endpoint to restore stubs
group.MapPost("/scores/restore", async (
[FromBody] RestoreRequest? request,
IInterestScoringService scoringService,
Microsoft.Extensions.Options.IOptions<InterestScoreOptions> options,
[FromServices] IInterestScoringService scoringService,
[FromServices] Microsoft.Extensions.Options.IOptions<InterestScoreOptions> options,
CancellationToken ct) =>
{
var threshold = request?.Threshold ?? options.Value.DegradationPolicy.RestorationThreshold;

View File

@@ -1,6 +1,7 @@
using System.Globalization;
using System.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using StellaOps.Concelier.WebService.Diagnostics;
using StellaOps.Concelier.WebService.Options;
@@ -18,9 +19,9 @@ internal static class MirrorEndpointExtensions
public static void MapConcelierMirrorEndpoints(this WebApplication app, bool authorityConfigured, bool enforceAuthority)
{
app.MapGet("/concelier/exports/index.json", async (
MirrorFileLocator locator,
MirrorRateLimiter limiter,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] MirrorFileLocator locator,
[FromServices] MirrorRateLimiter limiter,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
HttpContext context,
CancellationToken cancellationToken) =>
{
@@ -51,9 +52,9 @@ internal static class MirrorEndpointExtensions
app.MapGet("/concelier/exports/{**relativePath}", async (
string? relativePath,
MirrorFileLocator locator,
MirrorRateLimiter limiter,
IOptionsMonitor<ConcelierOptions> optionsMonitor,
[FromServices] MirrorFileLocator locator,
[FromServices] MirrorRateLimiter limiter,
[FromServices] IOptionsMonitor<ConcelierOptions> optionsMonitor,
HttpContext context,
CancellationToken cancellationToken) =>
{

View File

@@ -25,7 +25,7 @@ internal static class SbomEndpointExtensions
// POST /api/v1/learn/sbom - Register and learn from an SBOM
group.MapPost("/learn/sbom", async (
[FromBody] LearnSbomRequest request,
ISbomRegistryService registryService,
[FromServices] ISbomRegistryService registryService,
CancellationToken ct) =>
{
var input = new SbomRegistrationInput
@@ -62,7 +62,7 @@ internal static class SbomEndpointExtensions
// GET /api/v1/sboms/{digest}/affected - Get advisories affecting an SBOM
group.MapGet("/sboms/{digest}/affected", async (
string digest,
ISbomRegistryService registryService,
[FromServices] ISbomRegistryService registryService,
CancellationToken ct) =>
{
var registration = await registryService.GetByDigestAsync(digest, ct).ConfigureAwait(false);
@@ -103,7 +103,7 @@ internal static class SbomEndpointExtensions
[FromQuery] int? offset,
[FromQuery] int? limit,
[FromQuery] string? tenantId,
ISbomRegistryService registryService,
[FromServices] ISbomRegistryService registryService,
CancellationToken ct) =>
{
var registrations = await registryService.ListAsync(
@@ -140,7 +140,7 @@ internal static class SbomEndpointExtensions
// GET /api/v1/sboms/{digest} - Get SBOM registration details
group.MapGet("/sboms/{digest}", async (
string digest,
ISbomRegistryService registryService,
[FromServices] ISbomRegistryService registryService,
CancellationToken ct) =>
{
var registration = await registryService.GetByDigestAsync(digest, ct).ConfigureAwait(false);
@@ -174,7 +174,7 @@ internal static class SbomEndpointExtensions
// DELETE /api/v1/sboms/{digest} - Unregister an SBOM
group.MapDelete("/sboms/{digest}", async (
string digest,
ISbomRegistryService registryService,
[FromServices] ISbomRegistryService registryService,
CancellationToken ct) =>
{
await registryService.UnregisterAsync(digest, ct).ConfigureAwait(false);
@@ -187,7 +187,7 @@ internal static class SbomEndpointExtensions
// POST /api/v1/sboms/{digest}/rematch - Rematch SBOM against current advisories
group.MapPost("/sboms/{digest}/rematch", async (
string digest,
ISbomRegistryService registryService,
[FromServices] ISbomRegistryService registryService,
CancellationToken ct) =>
{
try
@@ -216,7 +216,7 @@ internal static class SbomEndpointExtensions
group.MapPatch("/sboms/{digest}", async (
string digest,
[FromBody] SbomDeltaRequest request,
ISbomRegistryService registryService,
[FromServices] ISbomRegistryService registryService,
CancellationToken ct) =>
{
try
@@ -258,7 +258,7 @@ internal static class SbomEndpointExtensions
// GET /api/v1/sboms/stats - Get SBOM registry statistics
group.MapGet("/sboms/stats", async (
[FromQuery] string? tenantId,
ISbomRegistryService registryService,
[FromServices] ISbomRegistryService registryService,
CancellationToken ct) =>
{
var stats = await registryService.GetStatsAsync(tenantId, ct).ConfigureAwait(false);

View File

@@ -104,6 +104,10 @@ builder.Host.ConfigureAppConfiguration((context, cfg) =>
#pragma warning restore ASP0013
var JsonOptions = CreateJsonOptions();
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
});
builder.Configuration.AddStellaOpsDefaults(options =>
{
@@ -155,6 +159,26 @@ if (builder.Environment.IsEnvironment("Testing"))
}
ConcelierOptionsPostConfigure.Apply(concelierOptions, contentRootPath);
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.RequiredScopes ??= new List<string>();
concelierOptions.Authority.ClientScopes ??= new List<string>();
if (concelierOptions.Authority.RequiredScopes.Count == 0)
{
concelierOptions.Authority.RequiredScopes.Add(StellaOpsScopes.ConcelierJobsTrigger);
}
if (concelierOptions.Authority.ClientScopes.Count == 0)
{
foreach (var scope in concelierOptions.Authority.RequiredScopes)
{
concelierOptions.Authority.ClientScopes.Add(scope);
}
}
if (concelierOptions.Authority.ClientScopes.Count == 0)
{
concelierOptions.Authority.ClientScopes.Add(StellaOpsScopes.ConcelierJobsTrigger);
}
// Skip validation in Testing to allow factory-provided wiring.
}
else
@@ -473,6 +497,7 @@ builder.Services.RegisterPluginRoutines(builder.Configuration, pluginHostOptions
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
var swaggerEnabled = app.Configuration.GetValue<bool>("Swagger:Enabled");
app.Logger.LogWarning("Authority enabled: {AuthorityEnabled}, test signing secret configured: {HasTestSecret}", authorityConfigured, !string.IsNullOrWhiteSpace(concelierOptions.Authority?.TestSigningSecret));
@@ -514,6 +539,7 @@ app.MapConcelierMirrorEndpoints(authorityConfigured, enforceAuthority);
// Canonical advisory endpoints (Sprint 8200.0012.0003)
app.MapCanonicalAdvisoryEndpoints();
app.MapInterestScoreEndpoints();
app.MapGet("/.well-known/openapi", ([FromServices] OpenApiDiscoveryDocumentProvider provider, HttpContext context) =>
{
@@ -559,6 +585,53 @@ app.MapGet("/.well-known/openapi", ([FromServices] OpenApiDiscoveryDocumentProvi
}
}).WithName("GetConcelierOpenApiDocument");
if (swaggerEnabled)
{
app.MapGet("/swagger/v1/swagger.json", ([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 HttpResults.StatusCode(StatusCodes.Status304NotModified);
}
}
}
context.Response.Headers.ETag = etag;
context.Response.Headers.CacheControl = "public, max-age=300, immutable";
return HttpResults.Text(payload, "application/json");
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("GetConcelierSwaggerDocument");
}
var orchestratorGroup = app.MapGroup("/internal/orch");
if (authorityConfigured)
{

View File

@@ -272,7 +272,10 @@ internal sealed class AdvisoryChunkBuilder
AdvisoryStructuredFieldContent content,
AdvisoryProvenance provenance)
{
var fingerprint = string.Concat(documentId, '|', fieldPath);
var normalizedMask = NormalizeFieldMask(provenance.FieldMask);
var observationPath = normalizedMask.Count > 0 ? normalizedMask[0] : fieldPath;
var resolvedMask = normalizedMask.Count > 0 ? normalizedMask : new[] { fieldPath };
var fingerprint = string.Concat(documentId, '|', observationPath);
var chunkId = CreateChunkId(fingerprint);
return new AdvisoryStructuredFieldEntry(
@@ -281,16 +284,27 @@ internal sealed class AdvisoryChunkBuilder
content,
new AdvisoryStructuredFieldProvenance(
documentId,
fieldPath,
observationPath,
provenance.Source,
provenance.Kind,
provenance.Value,
provenance.RecordedAt,
NormalizeFieldMask(provenance.FieldMask)));
resolvedMask));
}
private static IReadOnlyList<string> NormalizeFieldMask(ImmutableArray<string> mask)
=> mask.IsDefaultOrEmpty ? Array.Empty<string>() : mask;
{
if (mask.IsDefaultOrEmpty)
{
return Array.Empty<string>();
}
return mask
.Select(static entry => entry?.Trim())
.Where(static entry => !string.IsNullOrWhiteSpace(entry))
.Select(static entry => entry!)
.ToArray();
}
private string CreateChunkId(string input)
{

View File

@@ -104,13 +104,26 @@ internal sealed class OpenApiDiscoveryDocumentProvider
pathsObject[path] = pathItem;
}
var components = new JsonObject
{
["securitySchemes"] = new JsonObject
{
["Bearer"] = new JsonObject
{
["type"] = "http",
["scheme"] = "bearer",
["bearerFormat"] = "JWT"
}
}
};
return new JsonObject
{
["openapi"] = "3.1.0",
["info"] = info,
["servers"] = servers,
["paths"] = pathsObject,
["components"] = new JsonObject() // ready for future schemas
["components"] = components
};
}

View File

@@ -0,0 +1,30 @@
# Concelier Astra Connector Charter
## Mission
Implement and maintain the Astra Linux advisory connector (OVAL fetch/parse/map).
## Responsibilities
- Maintain `StellaOps.Concelier.Connector.Astra`.
- Keep ingestion deterministic and offline-friendly.
- Surface open work on `TASKS.md`; update statuses (TODO/DOING/DONE/BLOCKED/REVIEW).
## Key Paths
- `AstraConnector.cs`
- `AstraConnectorPlugin.cs`
- `AstraTrustDefaults.cs`
- `Configuration/AstraOptions.cs`
## Coordination
- Concelier connector owners.
## Required Reading
- `docs/modules/concelier/architecture.md`
- `docs/modules/concelier/link-not-merge-schema.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`
## Working Agreement
- 1. Update task status to `DOING`/`DONE` in both corresponding sprint file and local `TASKS.md`.
- 2. Keep outputs deterministic (ordering, timestamps, IDs).
- 3. Avoid network in tests; use fixtures and cached payloads.
- 4. Log any cross-module edits in the sprint Execution Log.

View File

@@ -0,0 +1,10 @@
# Concelier Astra Connector Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0748-M | DONE | Revalidated 2026-01-07. |
| AUDIT-0748-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0748-A | DONE | Dependencies resolved; builds 0 warnings 2026-01-07. |

View File

@@ -0,0 +1,26 @@
# Concelier BackportProof Charter
## Mission
Define and maintain backport proof logic for Concelier evidence pipelines.
## Responsibilities
- Maintain `StellaOps.Concelier.BackportProof`.
- Keep outputs deterministic and offline-friendly.
- Surface open work on `TASKS.md`; update statuses (TODO/DOING/DONE/BLOCKED/REVIEW).
## Key Paths
- `StellaOps.Concelier.BackportProof.csproj`
## Coordination
- Concelier proof service owners.
## Required Reading
- `docs/modules/concelier/architecture.md`
- `docs/modules/concelier/link-not-merge-schema.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`
## Working Agreement
- 1. Update task status to `DOING`/`DONE` in both corresponding sprint file and local `TASKS.md`.
- 2. Keep outputs deterministic (ordering, timestamps, IDs).
- 3. Avoid cross-module edits without sprint notes.

View File

@@ -0,0 +1,10 @@
# Concelier BackportProof Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0749-M | DONE | Revalidated 2026-01-07. |
| AUDIT-0749-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0749-A | DONE | Already compliant with TreatWarningsAsErrors. |

View File

@@ -1,8 +1,11 @@
# Concelier Analyzer Tests Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
Source of truth: `docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0144-A | DONE | Tests for StellaOps.Concelier.Analyzers. |
| AUDIT-0750-M | DONE | Revalidated 2026-01-07 (test project). |
| AUDIT-0750-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0750-A | DONE | Waived (test project; revalidated 2026-01-07). |

View File

@@ -0,0 +1,27 @@
# Concelier Astra Connector Tests Charter
## Mission
Validate Astra connector configuration, plugin registration, and mapping scaffolding with deterministic tests.
## Responsibilities
- Maintain `StellaOps.Concelier.Connector.Astra.Tests`.
- Keep tests deterministic and offline-friendly.
- Surface open work on `TASKS.md`; update statuses (TODO/DOING/DONE/BLOCKED/REVIEW).
## Key Paths
- `AstraConnectorTests.cs`
## Coordination
- Concelier connector owners (StellaOps.Concelier.Connector.Astra).
## Required Reading
- `docs/modules/concelier/architecture.md`
- `docs/modules/concelier/link-not-merge-schema.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`
## Working Agreement
- 1. Update task status to `DOING`/`DONE` in both corresponding sprint file and local `TASKS.md`.
- 2. Keep tests deterministic (stable ordering, timestamps, IDs).
- 3. Avoid network in tests; use fixtures and cached payloads.
- 4. Log any cross-module edits in the sprint Execution Log.

View File

@@ -0,0 +1,10 @@
# Concelier Astra Connector Tests Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0751-M | DONE | Revalidated 2026-01-07 (test project). |
| AUDIT-0751-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0751-A | DONE | Waived (test project; revalidated 2026-01-07). |

View File

@@ -1,5 +1,5 @@
{
"advisoryKey": "CIAD-2024-0005",
"advisoryKey": "certin/CIAD-2024-0005",
"affectedPackages": [
{
"type": "ics-vendor",
@@ -33,16 +33,7 @@
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "cert-in",
"kind": "affected",
"value": "Example Gateway Technologies Pvt Ltd Organisation: Partner Systems Inc. CVE-2024-9990 and CVE-2024-9991 allow remote attackers to execute arbitrary commands. Further information is available from the",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
}
]
"provenance": []
}
],
"aliases": [
@@ -81,11 +72,11 @@
{
"kind": "advisory",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://cert-in.example/advisory/CIAD-2024-0005",
"source": "unknown",
"kind": "unspecified",
"value": null,
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"recordedAt": "1970-01-01T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "cert-in",
@@ -95,11 +86,11 @@
{
"kind": "reference",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://vendor.example.com/advisories/example-gateway-bulletin",
"source": "unknown",
"kind": "unspecified",
"value": null,
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"recordedAt": "1970-01-01T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": null,
@@ -109,11 +100,11 @@
{
"kind": "advisory",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://www.cve.org/CVERecord?id=CVE-2024-9990",
"source": "unknown",
"kind": "unspecified",
"value": null,
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"recordedAt": "1970-01-01T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "CVE-2024-9990",
@@ -123,11 +114,11 @@
{
"kind": "advisory",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://www.cve.org/CVERecord?id=CVE-2024-9991",
"source": "unknown",
"kind": "unspecified",
"value": null,
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"recordedAt": "1970-01-01T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "CVE-2024-9991",

View File

@@ -15,8 +15,8 @@ public sealed class CannedHttpMessageHandlerTests
handler.SetFallback(_ => new HttpResponseMessage(HttpStatusCode.NotFound));
using var client = handler.CreateClient();
var firstResponse = await client.GetAsync(requestUri);
var secondResponse = await client.GetAsync(new Uri("https://example.test/other"));
var firstResponse = await client.GetAsync(requestUri, TestContext.Current.CancellationToken);
var secondResponse = await client.GetAsync(new Uri("https://example.test/other"), TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, firstResponse.StatusCode);
Assert.Equal(HttpStatusCode.NotFound, secondResponse.StatusCode);
@@ -32,6 +32,6 @@ public sealed class CannedHttpMessageHandlerTests
handler.AddException(HttpMethod.Get, requestUri, new InvalidOperationException("boom"));
using var client = handler.CreateClient();
await Assert.ThrowsAsync<InvalidOperationException>(() => client.GetAsync(requestUri));
await Assert.ThrowsAsync<InvalidOperationException>(() => client.GetAsync(requestUri, TestContext.Current.CancellationToken));
}
}

View File

@@ -1,35 +1,29 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Concelier.InMemoryRunner;
using StellaOps.Concelier.Documents;
using StellaOps.Concelier.InMemoryDriver;
using StellaOps.Aoc;
using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Common.Http;
using StellaOps.Concelier.Core.Aoc;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.RawModels;
using StellaOps.Concelier.Storage;
using StellaOps.Cryptography;
using LegacyContracts = StellaOps.Concelier.Storage;
using StorageContracts = StellaOps.Concelier.Storage.Contracts;
namespace StellaOps.Concelier.Connector.Common.Tests;
public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
{
private readonly InMemoryDbRunner _runner;
private readonly IStorageDatabase _database;
private readonly RawDocumentStorage _rawStorage;
private readonly ICryptoHash _hash;
public SourceFetchServiceGuardTests()
{
_runner = InMemoryDbRunner.Start(singleNodeReplSet: true);
var client = new InMemoryClient(_runner.ConnectionString);
_database = client.GetDatabase($"source-fetch-guard-{Guid.NewGuid():N}");
_rawStorage = new RawDocumentStorage();
_hash = CryptoHashFactory.CreateDefault();
}
@@ -41,15 +35,15 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
var handler = new StaticHttpMessageHandler(() => CreateSuccessResponse(responsePayload));
var client = new HttpClient(handler, disposeHandler: false);
var httpClientFactory = new StaticHttpClientFactory(client);
var documentStore = new RecordingDocumentStore();
var documentStore = new RecordingStorageDocumentStore();
var legacyStore = new NoopDocumentStore();
var guard = new RecordingAdvisoryRawWriteGuard();
var jitter = new NoJitterSource();
var httpOptions = new TestOptionsMonitor<StellaOps.Concelier.Connector.Common.Http.SourceHttpClientOptions>(new StellaOps.Concelier.Connector.Common.Http.SourceHttpClientOptions());
var storageOptions = Options.Create(new StorageOptions
var storageOptions = Options.Create(new LegacyContracts.StorageOptions
{
ConnectionString = _runner.ConnectionString,
DatabaseName = _database.DatabaseNamespace.DatabaseName,
DefaultTenant = "tenant-default",
});
var linksetMapper = new NoopAdvisoryLinksetMapper();
@@ -57,6 +51,7 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
var service = new SourceFetchService(
httpClientFactory,
_rawStorage,
legacyStore,
documentStore,
NullLogger<SourceFetchService>.Instance,
jitter,
@@ -90,11 +85,11 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
Assert.True(documentStore.UpsertCount > 0);
Assert.Equal("msrc", documentStore.LastRecord!.Metadata!["source.vendor"]);
Assert.Equal("tenant-default", documentStore.LastRecord.Metadata!["tenant"]);
Assert.NotNull(documentStore.LastRecord.PayloadId);
Assert.NotNull(documentStore.LastRecord.Payload);
// verify raw payload stored
var filesCollection = _database.GetCollection<DocumentObject>("documents.files");
var count = await filesCollection.CountDocumentsAsync(FilterDefinition<DocumentObject>.Empty);
Assert.Equal(1, count);
var rawPayload = await _rawStorage.DownloadAsync(documentStore.LastRecord.PayloadId!.Value, CancellationToken.None);
Assert.Equal(responsePayload, Encoding.UTF8.GetString(rawPayload));
}
[Fact]
@@ -103,15 +98,15 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
var handler = new StaticHttpMessageHandler(() => CreateSuccessResponse("{\"id\":\"CVE-2025-2222\"}"));
var client = new HttpClient(handler, disposeHandler: false);
var httpClientFactory = new StaticHttpClientFactory(client);
var documentStore = new RecordingDocumentStore();
var documentStore = new RecordingStorageDocumentStore();
var legacyStore = new NoopDocumentStore();
var guard = new RecordingAdvisoryRawWriteGuard { ShouldThrow = true };
var jitter = new NoJitterSource();
var httpOptions = new TestOptionsMonitor<StellaOps.Concelier.Connector.Common.Http.SourceHttpClientOptions>(new StellaOps.Concelier.Connector.Common.Http.SourceHttpClientOptions());
var storageOptions = Options.Create(new StorageOptions
var storageOptions = Options.Create(new LegacyContracts.StorageOptions
{
ConnectionString = _runner.ConnectionString,
DatabaseName = _database.DatabaseNamespace.DatabaseName,
DefaultTenant = "tenant-default",
});
var linksetMapper = new NoopAdvisoryLinksetMapper();
@@ -119,6 +114,7 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
var service = new SourceFetchService(
httpClientFactory,
_rawStorage,
legacyStore,
documentStore,
NullLogger<SourceFetchService>.Instance,
jitter,
@@ -140,16 +136,14 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
await Assert.ThrowsAsync<ConcelierAocGuardException>(() => service.FetchAsync(request, CancellationToken.None));
Assert.Equal(0, documentStore.UpsertCount);
var filesCollection = _database.GetCollection<DocumentObject>("documents.files");
var count = await filesCollection.CountDocumentsAsync(FilterDefinition<DocumentObject>.Empty);
Assert.Equal(0, count);
var recordId = CreateDeterministicGuid($"{request.SourceName}:{request.RequestUri}");
await Assert.ThrowsAsync<FileNotFoundException>(() => _rawStorage.DownloadAsync(recordId, CancellationToken.None));
}
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
public ValueTask DisposeAsync()
{
_runner.Dispose();
return ValueTask.CompletedTask;
}
@@ -184,24 +178,59 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
=> Task.FromResult(_responseFactory());
}
private sealed class RecordingDocumentStore : IDocumentStore
private sealed class RecordingStorageDocumentStore : StorageContracts.IStorageDocumentStore
{
public DocumentRecord? LastRecord { get; private set; }
private readonly Dictionary<Guid, StorageContracts.StorageDocument> _byId = new();
private readonly Dictionary<(string Source, string Uri), StorageContracts.StorageDocument> _bySourceUri = new();
public StorageContracts.StorageDocument? LastRecord { get; private set; }
public int UpsertCount { get; private set; }
public Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken)
public Task<StorageContracts.StorageDocument?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
{
_bySourceUri.TryGetValue((sourceName, uri), out var record);
return Task.FromResult<StorageContracts.StorageDocument?>(record);
}
public Task<StorageContracts.StorageDocument?> FindAsync(Guid id, CancellationToken cancellationToken)
{
_byId.TryGetValue(id, out var record);
return Task.FromResult<StorageContracts.StorageDocument?>(record);
}
public Task<StorageContracts.StorageDocument> UpsertAsync(StorageContracts.StorageDocument record, CancellationToken cancellationToken)
{
UpsertCount++;
LastRecord = record;
_byId[record.Id] = record;
_bySourceUri[(record.SourceName, record.Uri)] = record;
return Task.FromResult(record);
}
public Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
=> Task.FromResult<DocumentRecord?>(null);
public Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken)
{
if (_byId.TryGetValue(id, out var existing))
{
var updated = existing with { Status = status };
_byId[id] = updated;
_bySourceUri[(updated.SourceName, updated.Uri)] = updated;
LastRecord = updated;
}
public Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken)
=> Task.FromResult<DocumentRecord?>(null);
return Task.CompletedTask;
}
}
private sealed class NoopDocumentStore : LegacyContracts.IDocumentStore
{
public Task<LegacyContracts.DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
=> Task.FromResult<LegacyContracts.DocumentRecord?>(null);
public Task<LegacyContracts.DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken)
=> Task.FromResult<LegacyContracts.DocumentRecord?>(null);
public Task<LegacyContracts.DocumentRecord> UpsertAsync(LegacyContracts.DocumentRecord record, CancellationToken cancellationToken)
=> Task.FromResult(record);
public Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken)
=> Task.CompletedTask;
@@ -254,6 +283,14 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
{
public RawLinkset Map(AdvisoryRawDocument document) => new();
}
private static Guid CreateDeterministicGuid(string value)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(value ?? string.Empty));
bytes[6] = (byte)((bytes[6] & 0x0F) | 0x50);
bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80);
return new Guid(bytes.AsSpan(0, 16));
}
}

View File

@@ -1,9 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using StellaOps.Concelier.InMemoryRunner;
using StellaOps.Concelier.Documents;
using StellaOps.Concelier.InMemoryDriver;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Concelier.Connector.Common;
@@ -16,9 +13,6 @@ namespace StellaOps.Concelier.Connector.Common.Tests;
public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
{
private readonly InMemoryDbRunner _runner;
private readonly InMemoryClient _client;
private readonly IStorageDatabase _database;
private readonly DocumentStore _documentStore;
private readonly RawDocumentStorage _rawStorage;
private readonly InMemorySourceStateRepository _stateRepository;
@@ -27,10 +21,7 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
public SourceStateSeedProcessorTests()
{
_runner = InMemoryDbRunner.Start(singleNodeReplSet: true);
_client = new InMemoryClient(_runner.ConnectionString);
_database = _client.GetDatabase($"source-state-seed-{Guid.NewGuid():N}");
_documentStore = new DocumentStore(_database, NullLogger<DocumentStore>.Instance);
_documentStore = new DocumentStore();
_rawStorage = new RawDocumentStorage();
_stateRepository = new InMemorySourceStateRepository();
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 28, 12, 0, 0, TimeSpan.Zero));
@@ -98,16 +89,16 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
Assert.NotNull(storedDocument.Metadata);
Assert.Equal("value", storedDocument.Metadata!["test.meta"]);
var filesCollection = _database.GetCollection<DocumentObject>("documents.files");
var fileCount = await filesCollection.CountDocumentsAsync(FilterDefinition<DocumentObject>.Empty);
Assert.Equal(1, fileCount);
var payload = await _rawStorage.DownloadAsync(storedDocument.PayloadId!.Value, CancellationToken.None);
Assert.Equal("{\"id\":\"ADV-1\"}", Encoding.UTF8.GetString(payload));
var state = await _stateRepository.TryGetAsync("vndr.test", CancellationToken.None);
Assert.NotNull(state);
var stateValue = state!;
Assert.Equal(_timeProvider.GetUtcNow().UtcDateTime, stateValue.LastSuccess);
var cursor = stateValue.Cursor;
Assert.NotNull(stateValue.Cursor);
var cursor = stateValue.Cursor!;
var pendingDocs = cursor["pendingDocuments"].AsDocumentArray.Select(v => Guid.Parse(v.AsString)).ToList();
Assert.Contains(documentId, pendingDocs);
@@ -156,9 +147,8 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
var previousGridId = existingRecord!.PayloadId;
Assert.NotNull(previousGridId);
var filesCollection = _database.GetCollection<DocumentObject>("documents.files");
var initialFiles = await filesCollection.Find(FilterDefinition<DocumentObject>.Empty).ToListAsync();
Assert.Single(initialFiles);
var initialPayload = await _rawStorage.DownloadAsync(previousGridId!.Value, CancellationToken.None);
Assert.Equal("{\"id\":\"ADV-2\",\"rev\":1}", Encoding.UTF8.GetString(initialPayload));
var updatedSpecification = new SourceStateSeedSpecification
{
@@ -190,11 +180,9 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
Assert.NotNull(refreshedRecord);
Assert.Equal(documentId, refreshedRecord!.Id);
Assert.NotNull(refreshedRecord.PayloadId);
Assert.NotEqual(previousGridId?.ToString(), refreshedRecord.PayloadId?.ToString());
var files = await filesCollection.Find(FilterDefinition<DocumentObject>.Empty).ToListAsync();
Assert.Single(files);
Assert.NotEqual(previousGridId?.ToString(), files[0]["_id"].AsObjectId.ToString());
var updatedPayload = await _rawStorage.DownloadAsync(refreshedRecord.PayloadId!.Value, CancellationToken.None);
Assert.Equal("{\"id\":\"ADV-2\",\"rev\":2}", Encoding.UTF8.GetString(updatedPayload));
}
private SourceStateSeedProcessor CreateProcessor()
@@ -210,8 +198,7 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
public async ValueTask DisposeAsync()
{
await _client.DropDatabaseAsync(_database.DatabaseNamespace.DatabaseName);
_runner.Dispose();
await Task.CompletedTask;
}
}

View File

@@ -8,3 +8,5 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| AUDIT-0160-M | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
| AUDIT-0160-T | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
| AUDIT-0160-A | DONE | Waived (test project; revalidated 2026-01-06). |
| AUDIT-0374-T | DONE | Revalidated 2026-01-08 (storage store + raw payload checks). |
| AUDIT-0374-A | DONE | Revalidated 2026-01-08 (storage store + raw payload checks). |

View File

@@ -96,7 +96,7 @@ public sealed class PostgresPatchRepositoryTests : IClassFixture<PostgresTestFix
// Assert
results.Should().NotBeEmpty();
results.First().CveId.Should().Be(cveId);
results.First().Method.Should().NotBe(default);
Enum.IsDefined(results.First().Method.GetType(), results.First().Method).Should().BeTrue();
results.First().FingerprintValue.Should().NotBeNullOrEmpty();
results.First().TargetBinary.Should().NotBeNullOrEmpty();
results.First().Metadata.Should().NotBeNull();

View File

@@ -8,3 +8,5 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| AUDIT-0234-M | DONE | Revalidated 2026-01-07. |
| AUDIT-0234-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0234-A | DONE | Waived (test project; revalidated 2026-01-07). |
| AUDIT-0411-T | DONE | Revalidated 2026-01-08 (fingerprint method assertion). |
| AUDIT-0411-A | DONE | Revalidated 2026-01-08 (fingerprint method assertion). |

View File

@@ -11,7 +11,13 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using System.Collections.Immutable;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.Core.Jobs;
using StellaOps.Concelier.Core.Observations;
using StellaOps.Concelier.Core.Raw;
using StellaOps.Concelier.Models.Observations;
using StellaOps.Concelier.RawModels;
using StellaOps.Concelier.WebService.Options;
namespace StellaOps.Concelier.WebService.Tests.Fixtures;
@@ -27,7 +33,7 @@ public class ConcelierApplicationFactory : WebApplicationFactory<Program>
public ConcelierApplicationFactory() : this(enableSwagger: true, enableOtel: false) { }
public ConcelierApplicationFactory(bool enableSwagger = true, bool enableOtel = false)
protected ConcelierApplicationFactory(bool enableSwagger = true, bool enableOtel = false)
{
_enableSwagger = enableSwagger;
_enableOtel = enableOtel;
@@ -67,6 +73,12 @@ public class ConcelierApplicationFactory : WebApplicationFactory<Program>
{
services.RemoveAll<ILeaseStore>();
services.AddSingleton<ILeaseStore, TestLeaseStore>();
services.RemoveAll<IAdvisoryRawRepository>();
services.AddSingleton<IAdvisoryRawRepository, InMemoryAdvisoryRawRepository>();
services.RemoveAll<IAdvisoryLinksetQueryService>();
services.AddSingleton<IAdvisoryLinksetQueryService, StubAdvisoryLinksetQueryService>();
services.RemoveAll<IAdvisoryObservationQueryService>();
services.AddSingleton<IAdvisoryObservationQueryService, StubAdvisoryObservationQueryService>();
services.AddSingleton<ConcelierOptions>(new ConcelierOptions
{
PostgresStorage = new ConcelierOptions.PostgresStorageOptions
@@ -101,4 +113,109 @@ public class ConcelierApplicationFactory : WebApplicationFactory<Program>
});
});
}
private sealed class InMemoryAdvisoryRawRepository : IAdvisoryRawRepository
{
private static readonly DateTimeOffset FixedTimestamp = DateTimeOffset.UnixEpoch;
private readonly object _lock = new();
private readonly List<AdvisoryRawRecord> _records = new();
public Task<AdvisoryRawUpsertResult> UpsertAsync(AdvisoryRawDocument document, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var record = new AdvisoryRawRecord(Guid.NewGuid().ToString("D"), document, FixedTimestamp, FixedTimestamp);
lock (_lock)
{
_records.Add(record);
}
return Task.FromResult(new AdvisoryRawUpsertResult(true, record));
}
public Task<AdvisoryRawRecord?> FindByIdAsync(string tenant, string id, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
AdvisoryRawRecord? record;
lock (_lock)
{
record = _records.FirstOrDefault(candidate =>
string.Equals(candidate.Id, id, StringComparison.Ordinal) &&
string.Equals(candidate.Document.Tenant, tenant, StringComparison.OrdinalIgnoreCase));
}
return Task.FromResult(record);
}
public Task<AdvisoryRawQueryResult> QueryAsync(AdvisoryRawQueryOptions options, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
List<AdvisoryRawRecord> records;
lock (_lock)
{
records = _records
.Where(candidate => string.Equals(candidate.Document.Tenant, options.Tenant, StringComparison.OrdinalIgnoreCase))
.Take(options.Limit)
.ToList();
}
return Task.FromResult(new AdvisoryRawQueryResult(records, null, false));
}
public Task<IReadOnlyList<AdvisoryRawRecord>> FindByAdvisoryKeyAsync(
string tenant,
IReadOnlyCollection<string> searchValues,
IReadOnlyCollection<string> sourceVendors,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult<IReadOnlyList<AdvisoryRawRecord>>(Array.Empty<AdvisoryRawRecord>());
}
public Task<IReadOnlyList<AdvisoryRawRecord>> ListForVerificationAsync(
string tenant,
DateTimeOffset since,
DateTimeOffset until,
IReadOnlyCollection<string> sourceVendors,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult<IReadOnlyList<AdvisoryRawRecord>>(Array.Empty<AdvisoryRawRecord>());
}
}
private sealed class StubAdvisoryLinksetQueryService : IAdvisoryLinksetQueryService
{
public Task<AdvisoryLinksetQueryResult> QueryAsync(AdvisoryLinksetQueryOptions options, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new AdvisoryLinksetQueryResult(ImmutableArray<AdvisoryLinkset>.Empty, null, false));
}
}
private sealed class StubAdvisoryObservationQueryService : IAdvisoryObservationQueryService
{
public ValueTask<AdvisoryObservationQueryResult> QueryAsync(
AdvisoryObservationQueryOptions options,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var emptyLinkset = new AdvisoryObservationLinksetAggregate(
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<AdvisoryObservationReference>.Empty);
return ValueTask.FromResult(new AdvisoryObservationQueryResult(
ImmutableArray<AdvisoryObservation>.Empty,
emptyLinkset,
null,
false));
}
}
}

View File

@@ -326,6 +326,14 @@ public sealed class InterestScoreEndpointTests : IClassFixture<InterestScoreEndp
IsReachable = true,
ScannedAt = DateTimeOffset.UtcNow
});
var scoringService = Services.GetRequiredService<IInterestScoringService>();
scoringService.RecordSbomMatchAsync(
canonicalId,
"sha256:test123",
"pkg:npm/lodash@4.17.21",
isReachable: true,
isDeployed: false).GetAwaiter().GetResult();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)

View File

@@ -5,9 +5,16 @@
// Description: Authorization tests for Concelier.WebService (deny-by-default, token expiry, scope enforcement)
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Net;
using FluentAssertions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Auth.Abstractions;
using StellaOps.Concelier.WebService.Tests.Fixtures;
using StellaOps.Concelier.WebService.Options;
using StellaOps.TestKit;
using Xunit;
@@ -19,11 +26,11 @@ namespace StellaOps.Concelier.WebService.Tests.Security;
/// </summary>
[Trait("Category", TestCategories.Security)]
[Collection("ConcelierWebService")]
public sealed class ConcelierAuthorizationTests : IClassFixture<ConcelierApplicationFactory>
public sealed class ConcelierAuthorizationTests : IClassFixture<ConcelierAuthorizationFactory>
{
private readonly ConcelierApplicationFactory _factory;
private readonly ConcelierAuthorizationFactory _factory;
public ConcelierAuthorizationTests(ConcelierApplicationFactory factory)
public ConcelierAuthorizationTests(ConcelierAuthorizationFactory factory)
{
_factory = factory;
}
@@ -266,3 +273,77 @@ public sealed class ConcelierAuthorizationTests : IClassFixture<ConcelierApplica
#endregion
}
public sealed class ConcelierAuthorizationFactory : ConcelierApplicationFactory
{
private const string TestIssuer = "https://authority.test";
private const string TestSigningSecret = "test-signing-secret";
private readonly string? _previousAuthorityEnabled;
private readonly string? _previousAllowAnonymousFallback;
private readonly string? _previousAuthorityIssuer;
private readonly string? _previousRequireHttps;
private readonly string? _previousSigningSecret;
public ConcelierAuthorizationFactory() : base(enableSwagger: true, enableOtel: false)
{
_previousAuthorityEnabled = Environment.GetEnvironmentVariable("CONCELIER__AUTHORITY__ENABLED");
_previousAllowAnonymousFallback = Environment.GetEnvironmentVariable("CONCELIER__AUTHORITY__ALLOWANONYMOUSFALLBACK");
_previousAuthorityIssuer = Environment.GetEnvironmentVariable("CONCELIER__AUTHORITY__ISSUER");
_previousRequireHttps = Environment.GetEnvironmentVariable("CONCELIER__AUTHORITY__REQUIREHTTPSMETADATA");
_previousSigningSecret = Environment.GetEnvironmentVariable("CONCELIER__AUTHORITY__TESTSIGNINGSECRET");
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ENABLED", "true");
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ALLOWANONYMOUSFALLBACK", "false");
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ISSUER", TestIssuer);
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__REQUIREHTTPSMETADATA", "false");
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__TESTSIGNINGSECRET", TestSigningSecret);
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.ConfigureAppConfiguration((_, config) =>
{
var overrides = new Dictionary<string, string?>
{
["Authority:Enabled"] = "true",
["Authority:AllowAnonymousFallback"] = "false",
["Authority:Issuer"] = TestIssuer,
["Authority:RequireHttpsMetadata"] = "false",
["Authority:TestSigningSecret"] = TestSigningSecret
};
config.AddInMemoryCollection(overrides);
});
builder.ConfigureServices(services =>
{
services.PostConfigure<ConcelierOptions>(options =>
{
options.Authority ??= new ConcelierOptions.AuthorityOptions();
options.Authority.Enabled = true;
options.Authority.AllowAnonymousFallback = false;
options.Authority.Issuer = TestIssuer;
options.Authority.RequireHttpsMetadata = false;
options.Authority.TestSigningSecret = TestSigningSecret;
options.Authority.RequiredScopes.Clear();
options.Authority.RequiredScopes.Add(StellaOpsScopes.ConcelierJobsTrigger);
options.Authority.ClientScopes.Clear();
options.Authority.ClientScopes.Add(StellaOpsScopes.ConcelierJobsTrigger);
});
});
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ENABLED", _previousAuthorityEnabled);
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ALLOWANONYMOUSFALLBACK", _previousAllowAnonymousFallback);
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ISSUER", _previousAuthorityIssuer);
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__REQUIREHTTPSMETADATA", _previousRequireHttps);
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__TESTSIGNINGSECRET", _previousSigningSecret);
}
}

View File

@@ -25,4 +25,9 @@
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
<ItemGroup>
<None Include="Contract\\Expected\\concelier-openapi.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>