feat: Implement advisory event replay API with conflict explainers
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Docs CI / lint-and-preview (push) Has been cancelled
				
			- Added `/concelier/advisories/{vulnerabilityKey}/replay` endpoint to return conflict summaries and explainers.
- Introduced `MergeConflictExplainerPayload` to structure conflict details including type, reason, and source rankings.
- Enhanced `MergeConflictSummary` to include structured explainer payloads and hashes for persisted conflicts.
- Updated `MirrorEndpointExtensions` to enforce rate limits and cache headers for mirror distribution endpoints.
- Refactored tests to cover new replay endpoint functionality and validate conflict explainers.
- Documented changes in TASKS.md, noting completion of mirror distribution endpoints and updated operational runbook.
			
			
This commit is contained in:
		| @@ -1,8 +1,9 @@ | ||||
| using System.Globalization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.Extensions.Options; | ||||
| using StellaOps.Concelier.WebService.Options; | ||||
| using StellaOps.Concelier.WebService.Services; | ||||
| using System.Globalization; | ||||
| using System.IO; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.Extensions.Options; | ||||
| using StellaOps.Concelier.WebService.Options; | ||||
| using StellaOps.Concelier.WebService.Services; | ||||
|  | ||||
| namespace StellaOps.Concelier.WebService.Extensions; | ||||
|  | ||||
| @@ -42,7 +43,7 @@ internal static class MirrorEndpointExtensions | ||||
|                 return Results.NotFound(); | ||||
|             } | ||||
|  | ||||
|             return await WriteFileAsync(path, context.Response, "application/json").ConfigureAwait(false); | ||||
|             return await WriteFileAsync(path, context.Response, "application/json").ConfigureAwait(false); | ||||
|         }); | ||||
|  | ||||
|         app.MapGet("/concelier/exports/{**relativePath}", async ( | ||||
| @@ -84,7 +85,7 @@ internal static class MirrorEndpointExtensions | ||||
|             } | ||||
|  | ||||
|             var contentType = ResolveContentType(path); | ||||
|             return await WriteFileAsync(path, context.Response, contentType).ConfigureAwait(false); | ||||
|             return await WriteFileAsync(path, context.Response, contentType).ConfigureAwait(false); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| @@ -111,12 +112,12 @@ internal static class MirrorEndpointExtensions | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     private static bool TryAuthorize(bool requireAuthentication, bool enforceAuthority, HttpContext context, bool authorityConfigured, out IResult result) | ||||
|     { | ||||
|         result = Results.Empty; | ||||
|         if (!requireAuthentication) | ||||
|         { | ||||
|             return true; | ||||
|     private static bool TryAuthorize(bool requireAuthentication, bool enforceAuthority, HttpContext context, bool authorityConfigured, out IResult result) | ||||
|     { | ||||
|         result = Results.Empty; | ||||
|         if (!requireAuthentication) | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         if (!enforceAuthority || !authorityConfigured) | ||||
| @@ -127,14 +128,15 @@ internal static class MirrorEndpointExtensions | ||||
|         if (context.User?.Identity?.IsAuthenticated == true) | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         result = Results.StatusCode(StatusCodes.Status401Unauthorized); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     private static Task<IResult> WriteFileAsync(string path, HttpResponse response, string contentType) | ||||
|     { | ||||
|         } | ||||
|  | ||||
|         context.Response.Headers.WWWAuthenticate = "Bearer realm=\"StellaOps Concelier Mirror\""; | ||||
|         result = Results.StatusCode(StatusCodes.Status401Unauthorized); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     private static Task<IResult> WriteFileAsync(string path, HttpResponse response, string contentType) | ||||
|     { | ||||
|         var fileInfo = new FileInfo(path); | ||||
|         if (!fileInfo.Exists) | ||||
|         { | ||||
| @@ -147,12 +149,12 @@ internal static class MirrorEndpointExtensions | ||||
|             FileAccess.Read, | ||||
|             FileShare.Read | FileShare.Delete); | ||||
|  | ||||
|         response.Headers.CacheControl = "public, max-age=60"; | ||||
|         response.Headers.LastModified = fileInfo.LastWriteTimeUtc.ToString("R", CultureInfo.InvariantCulture); | ||||
|         response.ContentLength = fileInfo.Length; | ||||
|         return Task.FromResult(Results.Stream(stream, contentType)); | ||||
|     } | ||||
|  | ||||
|         response.Headers.CacheControl = BuildCacheControlHeader(path); | ||||
|         response.Headers.LastModified = fileInfo.LastWriteTimeUtc.ToString("R", CultureInfo.InvariantCulture); | ||||
|         response.ContentLength = fileInfo.Length; | ||||
|         return Task.FromResult(Results.Stream(stream, contentType)); | ||||
|     } | ||||
|  | ||||
|     private static string ResolveContentType(string path) | ||||
|     { | ||||
|         if (path.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) | ||||
| @@ -176,6 +178,28 @@ internal static class MirrorEndpointExtensions | ||||
|         } | ||||
|  | ||||
|         var seconds = Math.Max((int)Math.Ceiling(retryAfter.Value.TotalSeconds), 1); | ||||
|         response.Headers.RetryAfter = seconds.ToString(CultureInfo.InvariantCulture); | ||||
|     } | ||||
| } | ||||
|         response.Headers.RetryAfter = seconds.ToString(CultureInfo.InvariantCulture); | ||||
|     } | ||||
|  | ||||
|     private static string BuildCacheControlHeader(string path) | ||||
|     { | ||||
|         var fileName = Path.GetFileName(path); | ||||
|         if (fileName is null) | ||||
|         { | ||||
|             return "public, max-age=60"; | ||||
|         } | ||||
|  | ||||
|         if (string.Equals(fileName, "index.json", StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|             return "public, max-age=60"; | ||||
|         } | ||||
|  | ||||
|         if (fileName.EndsWith(".json", StringComparison.OrdinalIgnoreCase) || | ||||
|             fileName.EndsWith(".jws", StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|             return "public, max-age=300, immutable"; | ||||
|         } | ||||
|  | ||||
|         return "public, max-age=300"; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -227,7 +227,8 @@ app.MapGet("/concelier/advisories/{vulnerabilityKey}/replay", async ( | ||||
|             ConflictHash = Convert.ToHexString(conflict.ConflictHash.ToArray()), | ||||
|             conflict.AsOf, | ||||
|             conflict.RecordedAt, | ||||
|             Details = conflict.CanonicalJson | ||||
|             Details = conflict.CanonicalJson, | ||||
|             Explainer = MergeConflictExplainerPayload.FromCanonicalJson(conflict.CanonicalJson) | ||||
|         }).ToArray() | ||||
|     }; | ||||
|  | ||||
|   | ||||
| @@ -1,27 +1,28 @@ | ||||
| # TASKS | ||||
| | Task | Owner(s) | Depends on | Notes | | ||||
| |---|---|---|---| | ||||
| |FEEDWEB-EVENTS-07-001 Advisory event replay API|Concelier WebService Guild|FEEDCORE-ENGINE-07-001|**DONE (2025-10-19)** – Added `/concelier/advisories/{vulnerabilityKey}/replay` endpoint with optional `asOf`, hex hashes, and conflict payloads; integration covered via `dotnet test src/StellaOps.Concelier.WebService.Tests/StellaOps.Concelier.WebService.Tests.csproj`.| | ||||
| |Bind & validate ConcelierOptions|BE-Base|WebService|DONE – options bound/validated with failure logging.| | ||||
| |Mongo service wiring|BE-Base|Storage.Mongo|DONE – wiring delegated to `AddMongoStorage`.| | ||||
| |Bootstrapper execution on start|BE-Base|Storage.Mongo|DONE – startup calls `MongoBootstrapper.InitializeAsync`.| | ||||
| |Plugin host options finalization|BE-Base|Plugins|DONE – default plugin directories/search patterns configured.| | ||||
| |Jobs API contract tests|QA|Core|DONE – WebServiceEndpointsTests now cover success payloads, filtering, and trigger outcome mapping.| | ||||
| |Health/Ready probes|DevOps|Ops|DONE – `/health` and `/ready` endpoints implemented.| | ||||
| |Serilog + OTEL integration hooks|BE-Base|Observability|DONE – `TelemetryExtensions` wires Serilog + OTEL with configurable exporters.| | ||||
| |Register built-in jobs (sources/exporters)|BE-Base|Core|DONE – AddBuiltInConcelierJobs adds fallback scheduler definitions for core connectors and exporters via reflection.| | ||||
| |HTTP problem details consistency|BE-Base|WebService|DONE – API errors now emit RFC7807 responses with trace identifiers and typed problem categories.| | ||||
| |Request logging and metrics|BE-Base|Observability|DONE – Serilog request logging enabled with enriched context and web.jobs counters published via OpenTelemetry.| | ||||
| |Endpoint smoke tests (health/ready/jobs error paths)|QA|WebService|DONE – WebServiceEndpointsTests assert success and problem responses for health, ready, and job trigger error paths.| | ||||
| |Batch job definition last-run lookup|BE-Base|Core|DONE – definitions endpoint now precomputes kinds array and reuses batched last-run dictionary; manual smoke verified via local GET `/jobs/definitions`.| | ||||
| |Add no-cache headers to health/readiness/jobs APIs|BE-Base|WebService|DONE – helper applies Cache-Control/Pragma/Expires on all health/ready/jobs endpoints; awaiting automated probe tests once connector fixtures stabilize.| | ||||
| |Authority configuration parity (FSR1)|DevEx/Concelier|Authority options schema|**DONE (2025-10-10)** – Options post-config loads clientSecretFile fallback, validators normalize scopes/audiences, and sample config documents issuer/credential/bypass settings.| | ||||
| |Document authority toggle & scope requirements|Docs/Concelier|Authority integration|**DOING (2025-10-10)** – Quickstart updated with staging flag, client credentials, env overrides; operator guide refresh pending Docs guild review.| | ||||
| |Plumb Authority client resilience options|BE-Base|Auth libraries LIB5|**DONE (2025-10-12)** – `Program.cs` wires `authority.resilience.*` + client scopes into `AddStellaOpsAuthClient`; new integration test asserts binding and retries.| | ||||
| |Author ops guidance for resilience tuning|Docs/Concelier|Plumb Authority client resilience options|**DONE (2025-10-12)** – `docs/21_INSTALL_GUIDE.md` + `docs/ops/concelier-authority-audit-runbook.md` document resilience profiles for connected vs air-gapped installs and reference monitoring cues.| | ||||
| |Document authority bypass logging patterns|Docs/Concelier|FSR3 logging|**DONE (2025-10-12)** – Updated operator guides clarify `Concelier.Authorization.Audit` fields (route/status/subject/clientId/scopes/bypass/remote) and SIEM triggers.| | ||||
| |Update Concelier operator guide for enforcement cutoff|Docs/Concelier|FSR1 rollout|**DONE (2025-10-12)** – Installation guide emphasises disabling `allowAnonymousFallback` before 2025-12-31 UTC and connects audit signals to the rollout checklist.|  | ||||
| |Rename plugin drop directory to namespaced path|BE-Base|Plugins|**DONE (2025-10-19)** – Build outputs now target `StellaOps.Concelier.PluginBinaries`/`StellaOps.Authority.PluginBinaries`, plugin host defaults updated, config/docs refreshed, and `dotnet test src/StellaOps.Concelier.WebService.Tests/StellaOps.Concelier.WebService.Tests.csproj --no-restore` covers the change.|  | ||||
| |Authority resilience adoption|Concelier WebService, Docs|Plumb Authority client resilience options|**BLOCKED (2025-10-10)** – Roll out retry/offline knobs to deployment docs and confirm CLI parity once LIB5 lands; unblock after resilience options wired and tested.|  | ||||
| |CONCELIER-WEB-08-201 – Mirror distribution endpoints|Concelier WebService Guild|CONCELIER-EXPORT-08-201, DEVOPS-MIRROR-08-001|DOING (2025-10-19) – HTTP endpoints wired (`/concelier/exports/index.json`, `/concelier/exports/mirror/*`), mirror options bound/validated, and integration tests added; pending auth docs + smoke in ops handbook.|  | ||||
| |Wave 0B readiness checkpoint|Team WebService & Authority|Wave 0A completion|BLOCKED (2025-10-19) – FEEDSTORAGE-MONGO-08-001 closed, but remaining Wave 0A items (AUTH-DPOP-11-001, AUTH-MTLS-11-002, PLUGIN-DI-08-001) still open; maintain current DOING workstreams only.|  | ||||
| # TASKS | ||||
| | Task | Owner(s) | Depends on | Notes | | ||||
| |---|---|---|---| | ||||
| |FEEDWEB-EVENTS-07-001 Advisory event replay API|Concelier WebService Guild|FEEDCORE-ENGINE-07-001|**DONE (2025-10-19)** – Added `/concelier/advisories/{vulnerabilityKey}/replay` endpoint with optional `asOf`, hex hashes, and conflict payloads; integration covered via `dotnet test src/StellaOps.Concelier.WebService.Tests/StellaOps.Concelier.WebService.Tests.csproj`.| | ||||
| |Bind & validate ConcelierOptions|BE-Base|WebService|DONE – options bound/validated with failure logging.| | ||||
| |Mongo service wiring|BE-Base|Storage.Mongo|DONE – wiring delegated to `AddMongoStorage`.| | ||||
| |Bootstrapper execution on start|BE-Base|Storage.Mongo|DONE – startup calls `MongoBootstrapper.InitializeAsync`.| | ||||
| |Plugin host options finalization|BE-Base|Plugins|DONE – default plugin directories/search patterns configured.| | ||||
| |Jobs API contract tests|QA|Core|DONE – WebServiceEndpointsTests now cover success payloads, filtering, and trigger outcome mapping.| | ||||
| |Health/Ready probes|DevOps|Ops|DONE – `/health` and `/ready` endpoints implemented.| | ||||
| |Serilog + OTEL integration hooks|BE-Base|Observability|DONE – `TelemetryExtensions` wires Serilog + OTEL with configurable exporters.| | ||||
| |Register built-in jobs (sources/exporters)|BE-Base|Core|DONE – AddBuiltInConcelierJobs adds fallback scheduler definitions for core connectors and exporters via reflection.| | ||||
| |HTTP problem details consistency|BE-Base|WebService|DONE – API errors now emit RFC7807 responses with trace identifiers and typed problem categories.| | ||||
| |Request logging and metrics|BE-Base|Observability|DONE – Serilog request logging enabled with enriched context and web.jobs counters published via OpenTelemetry.| | ||||
| |Endpoint smoke tests (health/ready/jobs error paths)|QA|WebService|DONE – WebServiceEndpointsTests assert success and problem responses for health, ready, and job trigger error paths.| | ||||
| |Batch job definition last-run lookup|BE-Base|Core|DONE – definitions endpoint now precomputes kinds array and reuses batched last-run dictionary; manual smoke verified via local GET `/jobs/definitions`.| | ||||
| |Add no-cache headers to health/readiness/jobs APIs|BE-Base|WebService|DONE – helper applies Cache-Control/Pragma/Expires on all health/ready/jobs endpoints; awaiting automated probe tests once connector fixtures stabilize.| | ||||
| |Authority configuration parity (FSR1)|DevEx/Concelier|Authority options schema|**DONE (2025-10-10)** – Options post-config loads clientSecretFile fallback, validators normalize scopes/audiences, and sample config documents issuer/credential/bypass settings.| | ||||
| |Document authority toggle & scope requirements|Docs/Concelier|Authority integration|**DOING (2025-10-10)** – Quickstart updated with staging flag, client credentials, env overrides; operator guide refresh pending Docs guild review.| | ||||
| |Plumb Authority client resilience options|BE-Base|Auth libraries LIB5|**DONE (2025-10-12)** – `Program.cs` wires `authority.resilience.*` + client scopes into `AddStellaOpsAuthClient`; new integration test asserts binding and retries.| | ||||
| |Author ops guidance for resilience tuning|Docs/Concelier|Plumb Authority client resilience options|**DONE (2025-10-12)** – `docs/21_INSTALL_GUIDE.md` + `docs/ops/concelier-authority-audit-runbook.md` document resilience profiles for connected vs air-gapped installs and reference monitoring cues.| | ||||
| |Document authority bypass logging patterns|Docs/Concelier|FSR3 logging|**DONE (2025-10-12)** – Updated operator guides clarify `Concelier.Authorization.Audit` fields (route/status/subject/clientId/scopes/bypass/remote) and SIEM triggers.| | ||||
| |Update Concelier operator guide for enforcement cutoff|Docs/Concelier|FSR1 rollout|**DONE (2025-10-12)** – Installation guide emphasises disabling `allowAnonymousFallback` before 2025-12-31 UTC and connects audit signals to the rollout checklist.|  | ||||
| |Rename plugin drop directory to namespaced path|BE-Base|Plugins|**DONE (2025-10-19)** – Build outputs now target `StellaOps.Concelier.PluginBinaries`/`StellaOps.Authority.PluginBinaries`, plugin host defaults updated, config/docs refreshed, and `dotnet test src/StellaOps.Concelier.WebService.Tests/StellaOps.Concelier.WebService.Tests.csproj --no-restore` covers the change.|  | ||||
| |Authority resilience adoption|Concelier WebService, Docs|Plumb Authority client resilience options|**BLOCKED (2025-10-10)** – Roll out retry/offline knobs to deployment docs and confirm CLI parity once LIB5 lands; unblock after resilience options wired and tested.|  | ||||
| |CONCELIER-WEB-08-201 – Mirror distribution endpoints|Concelier WebService Guild|CONCELIER-EXPORT-08-201, DEVOPS-MIRROR-08-001|**DONE (2025-10-20)** – Mirror endpoints now enforce per-domain rate limits, emit cache headers, honour Authority/WWW-Authenticate, and docs cover auth + smoke workflows.| | ||||
| > Remark (2025-10-20): Updated ops runbook with token/rate-limit checks and added API tests for Retry-After + unauthorized flows.| | ||||
| |Wave 0B readiness checkpoint|Team WebService & Authority|Wave 0A completion|BLOCKED (2025-10-19) – FEEDSTORAGE-MONGO-08-001 closed, but remaining Wave 0A items (AUTH-DPOP-11-001, AUTH-MTLS-11-002, PLUGIN-DI-08-001) still open; maintain current DOING workstreams only.|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user