more audit work
This commit is contained in:
@@ -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) =>
|
||||
|
||||
@@ -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) =>
|
||||
{
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) =>
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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. |
|
||||
@@ -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.
|
||||
@@ -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. |
|
||||
@@ -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). |
|
||||
|
||||
@@ -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.
|
||||
@@ -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). |
|
||||
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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). |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,4 +25,9 @@
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
<ItemGroup>
|
||||
<None Include="Contract\\Expected\\concelier-openapi.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user