using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.Metrics; using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Linq; using System.Net; using System.Net.Http.Json; using System.Net.Http.Headers; using System.Security.Claims; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Logging; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Concelier.InMemoryRunner; using StellaOps.Concelier.Documents; using StellaOps.Concelier.Documents.IO; using StellaOps.Concelier.InMemoryDriver; using StellaOps.Concelier.Core.Attestation; using static StellaOps.Concelier.WebService.Program; using StellaOps.Concelier.Core.Events; using StellaOps.Concelier.Core.Jobs; using StellaOps.Concelier.Models; using StellaOps.Concelier.Merge.Services; using StellaOps.Concelier.Storage; using StellaOps.Concelier.Storage.Advisories; using StellaOps.Concelier.Storage.Observations; using StellaOps.Concelier.Storage.Linksets; using StellaOps.Concelier.Core.Raw; using StellaOps.Concelier.Core.Observations; using StellaOps.Concelier.Core.Linksets; using StellaOps.Concelier.Models.Observations; using StellaOps.Concelier.Persistence.Postgres; using StellaOps.Concelier.RawModels; using StellaOps.Concelier.WebService.Jobs; using StellaOps.Concelier.WebService.Options; using StellaOps.Concelier.WebService.Contracts; using StellaOps.Concelier.WebService; using Xunit.Sdk; using StellaOps.Auth.Abstractions; using StellaOps.Auth.Client; using Xunit; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using StellaOps.Concelier.WebService.Diagnostics; using Microsoft.IdentityModel.Tokens; using StellaOps.Cryptography; using DsseProvenance = StellaOps.Provenance.DsseProvenance; using TrustInfo = StellaOps.Provenance.TrustInfo; using DocumentObject = StellaOps.Concelier.Documents.DocumentObject; namespace StellaOps.Concelier.WebService.Tests; public sealed class WebServiceEndpointsTests : IAsyncLifetime { private const string TestAuthorityIssuer = "https://authority.example"; private const string TestAuthorityAudience = "api://concelier"; private const string TestSigningSecret = "0123456789ABCDEF0123456789ABCDEF"; private static readonly SymmetricSecurityKey TestSigningKey = new(Encoding.UTF8.GetBytes(TestSigningSecret)); private readonly ITestOutputHelper _output; private InMemoryDbRunner _runner = null!; private ConcelierApplicationFactory _factory = null!; public WebServiceEndpointsTests(ITestOutputHelper output) { _output = output; } public ValueTask InitializeAsync() { _runner = InMemoryDbRunner.Start(); // Use an empty connection string - the factory sets a default Postgres connection string // and the stub services bypass actual database operations _factory = new ConcelierApplicationFactory(string.Empty); WarmupFactory(_factory); return ValueTask.CompletedTask; } public ValueTask DisposeAsync() { _factory.Dispose(); _runner.Dispose(); return ValueTask.CompletedTask; } [Fact] public async Task HealthAndReadyEndpointsRespond() { using var client = _factory.CreateClient(); var healthResponse = await client.GetAsync("/health"); if (!healthResponse.IsSuccessStatusCode) { var body = await healthResponse.Content.ReadAsStringAsync(); throw new Xunit.Sdk.XunitException($"/health failed: {(int)healthResponse.StatusCode} {body}"); } var readyResponse = await client.GetAsync("/ready"); if (!readyResponse.IsSuccessStatusCode) { var body = await readyResponse.Content.ReadAsStringAsync(); throw new Xunit.Sdk.XunitException($"/ready failed: {(int)readyResponse.StatusCode} {body}"); } var healthPayload = await healthResponse.Content.ReadFromJsonAsync(); Assert.NotNull(healthPayload); Assert.Equal("healthy", healthPayload!.Status); Assert.Equal("postgres", healthPayload.Storage.Backend); var readyPayload = await readyResponse.Content.ReadFromJsonAsync(); Assert.NotNull(readyPayload); Assert.True(readyPayload!.Status is "ready" or "degraded"); Assert.Equal("postgres", readyPayload.Storage.Backend); } [Fact] public async Task ObservationsEndpoint_ReturnsTenantScopedResults() { await SeedObservationDocumentsAsync(BuildSampleObservationDocuments()); using var client = _factory.CreateClient(); var response = await client.GetAsync("/concelier/observations?tenant=tenant-a&alias=CVE-2025-0001"); if (!response.IsSuccessStatusCode) { var body = await response.Content.ReadAsStringAsync(); throw new XunitException($"/concelier/observations failed: {(int)response.StatusCode} {body}"); } using var document = await response.Content.ReadFromJsonAsync(); Assert.NotNull(document); var root = document!.RootElement; var observations = root.GetProperty("observations").EnumerateArray().ToArray(); Assert.Equal(2, observations.Length); Assert.Equal("tenant-a:ghsa:beta:1", observations[0].GetProperty("observationId").GetString()); Assert.Equal("tenant-a:nvd:alpha:1", observations[1].GetProperty("observationId").GetString()); var linkset = root.GetProperty("linkset"); Assert.Equal(new[] { "cve-2025-0001", "ghsa-2025-xyz" }, linkset.GetProperty("aliases").EnumerateArray().Select(x => x.GetString()).ToArray()); Assert.Equal(new[] { "pkg:npm/demo@1.0.0", "pkg:npm/demo@1.1.0" }, linkset.GetProperty("purls").EnumerateArray().Select(x => x.GetString()).ToArray()); Assert.Equal(new[] { "cpe:/a:vendor:product:1.0", "cpe:/a:vendor:product:1.1" }, linkset.GetProperty("cpes").EnumerateArray().Select(x => x.GetString()).ToArray()); var references = linkset.GetProperty("references").EnumerateArray().ToArray(); Assert.Equal(2, references.Length); Assert.Equal("advisory", references[0].GetProperty("type").GetString()); Assert.Equal("https://example.test/advisory-1", references[0].GetProperty("url").GetString()); Assert.Equal("patch", references[1].GetProperty("type").GetString()); var confidence = linkset.GetProperty("confidence").GetDouble(); Assert.Equal(1.0, confidence); var conflicts = linkset.GetProperty("conflicts").EnumerateArray().ToArray(); Assert.Empty(conflicts); Assert.False(root.GetProperty("hasMore").GetBoolean()); Assert.True(root.GetProperty("nextCursor").ValueKind == JsonValueKind.Null); } [Fact] public async Task ObservationsEndpoint_AppliesObservationIdFilter() { await SeedObservationDocumentsAsync(BuildSampleObservationDocuments()); using var client = _factory.CreateClient(); var observationId = Uri.EscapeDataString("tenant-a:ghsa:beta:1"); var response = await client.GetAsync($"/concelier/observations?tenant=tenant-a&observationId={observationId}&cpe=cpe:/a:vendor:product:1.1"); if (!response.IsSuccessStatusCode) { var body = await response.Content.ReadAsStringAsync(); throw new XunitException($"/concelier/observations filter failed: {(int)response.StatusCode} {body}"); } using var document = await response.Content.ReadFromJsonAsync(); Assert.NotNull(document); var root = document!.RootElement; var observations = root.GetProperty("observations").EnumerateArray().ToArray(); Assert.Single(observations); Assert.Equal("tenant-a:ghsa:beta:1", observations[0].GetProperty("observationId").GetString()); Assert.Equal(new[] { "pkg:npm/demo@1.1.0" }, observations[0].GetProperty("linkset").GetProperty("purls").EnumerateArray().Select(x => x.GetString()).ToArray()); Assert.Equal(new[] { "cpe:/a:vendor:product:1.1" }, observations[0].GetProperty("linkset").GetProperty("cpes").EnumerateArray().Select(x => x.GetString()).ToArray()); Assert.False(root.GetProperty("hasMore").GetBoolean()); Assert.True(root.GetProperty("nextCursor").ValueKind == JsonValueKind.Null); } [Fact] public async Task ObservationsEndpoint_SupportsPagination() { await SeedObservationDocumentsAsync(BuildSampleObservationDocuments()); using var client = _factory.CreateClient(); var firstResponse = await client.GetAsync("/concelier/observations?tenant=tenant-a&limit=1"); firstResponse.EnsureSuccessStatusCode(); using var firstDocument = await firstResponse.Content.ReadFromJsonAsync(); Assert.NotNull(firstDocument); var firstRoot = firstDocument!.RootElement; var firstObservations = firstRoot.GetProperty("observations").EnumerateArray().ToArray(); Assert.Single(firstObservations); var nextCursor = firstRoot.GetProperty("nextCursor").GetString(); Assert.True(firstRoot.GetProperty("hasMore").GetBoolean()); Assert.False(string.IsNullOrWhiteSpace(nextCursor)); var secondResponse = await client.GetAsync($"/concelier/observations?tenant=tenant-a&limit=2&cursor={Uri.EscapeDataString(nextCursor!)}"); secondResponse.EnsureSuccessStatusCode(); using var secondDocument = await secondResponse.Content.ReadFromJsonAsync(); Assert.NotNull(secondDocument); var secondRoot = secondDocument!.RootElement; var secondObservations = secondRoot.GetProperty("observations").EnumerateArray().ToArray(); Assert.Single(secondObservations); Assert.False(secondRoot.GetProperty("hasMore").GetBoolean()); Assert.True(secondRoot.GetProperty("nextCursor").ValueKind == JsonValueKind.Null); Assert.Equal("tenant-a:nvd:alpha:1", secondObservations[0].GetProperty("observationId").GetString()); } [Fact] public async Task LinksetsEndpoint_ReturnsNormalizedLinksetsFromIngestion() { var tenant = "tenant-linkset-ingest"; using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Stella-Tenant", tenant); var firstIngest = await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest("sha256:linkset-1", "GHSA-LINK-001", purls: new[] { "pkg:npm/demo@1.0.0" })); firstIngest.EnsureSuccessStatusCode(); var secondIngest = await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest("sha256:linkset-2", "GHSA-LINK-002", purls: new[] { "pkg:npm/demo@2.0.0" })); secondIngest.EnsureSuccessStatusCode(); var response = await client.GetAsync("/linksets?tenant=tenant-linkset-ingest&limit=10"); response.EnsureSuccessStatusCode(); var payload = await response.Content.ReadFromJsonAsync(); Assert.NotNull(payload); Assert.Equal(2, payload!.Linksets.Length); var linksetAdvisoryIds = payload.Linksets.Select(ls => ls.AdvisoryId).OrderBy(id => id, StringComparer.Ordinal).ToArray(); Assert.Equal(new[] { "GHSA-LINK-001", "GHSA-LINK-002" }, linksetAdvisoryIds); var allPurls = payload.Linksets.SelectMany(ls => ls.Purls).OrderBy(p => p, StringComparer.Ordinal).ToArray(); Assert.Contains("pkg:npm/demo@1.0.0", allPurls); Assert.Contains("pkg:npm/demo@2.0.0", allPurls); var versions = payload.Linksets .SelectMany(ls => ls.Versions) .Distinct(StringComparer.Ordinal) .OrderBy(v => v, StringComparer.Ordinal) .ToArray(); Assert.Contains("1.0.0", versions); Assert.Contains("2.0.0", versions); Assert.False(payload.HasMore); Assert.True(string.IsNullOrEmpty(payload.NextCursor)); } [Fact] public async Task LinksetsEndpoint_SupportsCursorPagination() { var tenant = "tenant-linkset-page"; var documents = new[] { CreateLinksetDocument( tenant, "nvd", "ADV-002", new[] { "obs-2" }, new[] { "pkg:npm/demo@2.0.0" }, new[] { "2.0.0" }, new DateTime(2025, 1, 6, 0, 0, 0, DateTimeKind.Utc)), CreateLinksetDocument( tenant, "osv", "ADV-001", new[] { "obs-1" }, new[] { "pkg:npm/demo@1.0.0" }, new[] { "1.0.0" }, new DateTime(2025, 1, 5, 0, 0, 0, DateTimeKind.Utc)), CreateLinksetDocument( "tenant-other", "osv", "ADV-999", new[] { "obs-x" }, new[] { "pkg:npm/other@1.0.0" }, new[] { "1.0.0" }, new DateTime(2025, 1, 4, 0, 0, 0, DateTimeKind.Utc)) }; await SeedLinksetDocumentsAsync(documents); using var client = _factory.CreateClient(); var firstResponse = await client.GetAsync($"/linksets?tenant={tenant}&limit=1"); firstResponse.EnsureSuccessStatusCode(); var firstPayload = await firstResponse.Content.ReadFromJsonAsync(); Assert.NotNull(firstPayload); var first = Assert.Single(firstPayload!.Linksets); Assert.Equal("ADV-002", first.AdvisoryId); Assert.Equal(new[] { "pkg:npm/demo@2.0.0" }, first.Purls.ToArray()); Assert.Equal(new[] { "2.0.0" }, first.Versions.ToArray()); Assert.True(firstPayload.HasMore); Assert.False(string.IsNullOrWhiteSpace(firstPayload.NextCursor)); var secondResponse = await client.GetAsync($"/linksets?tenant={tenant}&limit=1&cursor={Uri.EscapeDataString(firstPayload.NextCursor!)}"); secondResponse.EnsureSuccessStatusCode(); var secondPayload = await secondResponse.Content.ReadFromJsonAsync(); Assert.NotNull(secondPayload); var second = Assert.Single(secondPayload!.Linksets); Assert.Equal("ADV-001", second.AdvisoryId); Assert.Equal(new[] { "pkg:npm/demo@1.0.0" }, second.Purls.ToArray()); Assert.Equal(new[] { "1.0.0" }, second.Versions.ToArray()); Assert.False(secondPayload.HasMore); Assert.True(string.IsNullOrEmpty(secondPayload.NextCursor)); } [Fact] public async Task LnmLinksetsEndpoints_ReturnFactOnlyLinksets() { var tenant = "tenant-lnm-list"; var documents = new[] { CreateLinksetDocument( tenant, "nvd", "ADV-002", new[] { "obs-2" }, new[] { "pkg:npm/demo@2.0.0" }, new[] { "2.0.0" }, new DateTime(2025, 1, 6, 0, 0, 0, DateTimeKind.Utc)), CreateLinksetDocument( tenant, "osv", "ADV-001", new[] { "obs-1" }, new[] { "pkg:npm/demo@1.0.0" }, new[] { "1.0.0" }, new DateTime(2025, 1, 5, 0, 0, 0, DateTimeKind.Utc)) }; await SeedLinksetDocumentsAsync(documents); using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Stella-Tenant", tenant); var listResponse = await client.GetAsync("/v1/lnm/linksets?pageSize=1&page=1"); listResponse.EnsureSuccessStatusCode(); var listPayload = await listResponse.Content.ReadFromJsonAsync(); var firstItem = listPayload.GetProperty("items").EnumerateArray().First(); Assert.Equal("ADV-002", firstItem.GetProperty("advisoryId").GetString()); Assert.Contains("pkg:npm/demo@2.0.0", firstItem.GetProperty("purl").EnumerateArray().Select(x => x.GetString())); Assert.True(firstItem.GetProperty("conflicts").EnumerateArray().Count() >= 0); Assert.Equal("created", firstItem.GetProperty("timeline").EnumerateArray().First().GetProperty("event").GetString()); Assert.Equal(DateTime.Parse("2025-01-06T00:00:00Z"), firstItem.GetProperty("publishedAt").GetDateTime()); var detailResponse = await client.GetAsync("/v1/lnm/linksets/ADV-001?source=osv&includeObservations=true"); detailResponse.EnsureSuccessStatusCode(); var detailPayload = await detailResponse.Content.ReadFromJsonAsync(); Assert.Equal("ADV-001", detailPayload.GetProperty("advisoryId").GetString()); Assert.Equal("osv", detailPayload.GetProperty("source").GetString()); Assert.Contains("pkg:npm/demo@1.0.0", detailPayload.GetProperty("purl").EnumerateArray().Select(x => x.GetString())); Assert.Contains("obs-1", detailPayload.GetProperty("observations").EnumerateArray().Select(x => x.GetString())); Assert.Equal(DateTime.Parse("2025-01-05T00:00:00Z"), detailPayload.GetProperty("publishedAt").GetDateTime()); } [Fact] public async Task ObservationsEndpoint_ReturnsBadRequestWhenTenantMissing() { using var client = _factory.CreateClient(); var response = await client.GetAsync("/concelier/observations"); var body = await response.Content.ReadAsStringAsync(); Assert.True(response.StatusCode == HttpStatusCode.BadRequest, $"Expected 400 but got {(int)response.StatusCode}: {body}"); } [Fact] public async Task AdvisoryChunksEndpoint_ReturnsParagraphAnchors() { var newestRaw = DocumentObject.Parse( """ { "summary": { "intro": "This is a deterministic summary paragraph describing CVE-2025-0001 with remediation context for Advisory AI consumers." }, "details": [ "Long-form remediation guidance that exceeds the minimum length threshold and mentions affected packages.", { "body": "Nested context that Advisory AI can cite when rendering downstream explanations." } ] } """); var olderRaw = DocumentObject.Parse( """ { "summary": { "intro": "Older paragraph that should be visible when no section filter applies." } } """); var newerCreatedAt = new DateTime(2025, 1, 7, 0, 0, 0, DateTimeKind.Utc); var olderCreatedAt = new DateTime(2025, 1, 5, 0, 0, 0, DateTimeKind.Utc); var newerHash = ComputeContentHash(newestRaw); var olderHash = ComputeContentHash(olderRaw); var documents = new[] { CreateChunkObservationDocument( id: "tenant-a:chunk:newest", tenant: "tenant-a", createdAt: newerCreatedAt, alias: "cve-2025-0001", rawDocument: newestRaw), CreateChunkObservationDocument( id: "tenant-a:chunk:older", tenant: "tenant-a", createdAt: olderCreatedAt, alias: "cve-2025-0001", rawDocument: olderRaw) }; await SeedObservationDocumentsAsync(documents); await SeedAdvisoryRawDocumentsAsync( CreateAdvisoryRawDocument("tenant-a", "nvd", "tenant-a:chunk:newest", newerHash, newestRaw.DeepClone().AsDocumentObject), CreateAdvisoryRawDocument("tenant-a", "nvd", "tenant-a:chunk:older", olderHash, olderRaw.DeepClone().AsDocumentObject)); await SeedCanonicalAdvisoriesAsync( CreateStructuredAdvisory("CVE-2025-0001", "GHSA-2025-0001", "tenant-a:chunk:newest", newerCreatedAt)); using var client = _factory.CreateClient(); var response = await client.GetAsync("/advisories/cve-2025-0001/chunks?tenant=tenant-a§ion=workaround"); response.EnsureSuccessStatusCode(); var payload = await response.Content.ReadAsStringAsync(); using var document = JsonDocument.Parse(payload); var root = document.RootElement; Assert.Equal("CVE-2025-0001", root.GetProperty("advisoryKey").GetString()); Assert.False(string.IsNullOrWhiteSpace(root.GetProperty("fingerprint").GetString())); Assert.Equal(1, root.GetProperty("total").GetInt32()); Assert.False(root.GetProperty("truncated").GetBoolean()); var entry = Assert.Single(root.GetProperty("entries").EnumerateArray()); Assert.Equal("workaround", entry.GetProperty("type").GetString()); Assert.False(string.IsNullOrWhiteSpace(entry.GetProperty("chunkId").GetString())); var content = entry.GetProperty("content"); Assert.Equal("Vendor guidance", content.GetProperty("title").GetString()); Assert.Equal("Apply configuration change immediately.", content.GetProperty("description").GetString()); Assert.Equal("https://vendor.example/workaround", content.GetProperty("url").GetString()); var provenance = entry.GetProperty("provenance"); Assert.Equal("tenant-a:chunk:newest", provenance.GetProperty("documentId").GetString()); Assert.Equal("/references/0", provenance.GetProperty("observationPath").GetString()); Assert.Equal("nvd", provenance.GetProperty("source").GetString()); Assert.Equal("workaround", provenance.GetProperty("kind").GetString()); Assert.Equal("tenant-a:chunk:newest", provenance.GetProperty("value").GetString()); Assert.Contains( "/references/0", provenance.GetProperty("fieldMask").EnumerateArray().Select(element => element.GetString())); } [Fact] public async Task AdvisoryChunksEndpoint_ReturnsNotFoundWhenAdvisoryMissing() { await SeedObservationDocumentsAsync(BuildSampleObservationDocuments()); await SeedCanonicalAdvisoriesAsync(); using var client = _factory.CreateClient(); var response = await client.GetAsync("/advisories/cve-2099-9999/chunks?tenant=tenant-a"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); var payload = await response.Content.ReadAsStringAsync(); using var document = JsonDocument.Parse(payload); var root = document.RootElement; Assert.Equal("https://stellaops.org/problems/not-found", root.GetProperty("type").GetString()); Assert.Equal("Advisory not found", root.GetProperty("title").GetString()); Assert.Contains("cve-2099-9999", root.GetProperty("detail").GetString(), StringComparison.OrdinalIgnoreCase); } [Fact] public async Task AdvisoryIngestEndpoint_PersistsDocumentAndSupportsReadback() { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-ingest"); const string upstreamId = "GHSA-INGEST-0001"; var ingestRequest = BuildAdvisoryIngestRequest( contentHash: null, upstreamId: upstreamId); var ingestResponse = await client.PostAsJsonAsync("/ingest/advisory", ingestRequest); if (ingestResponse.StatusCode != HttpStatusCode.Created) { WriteProgramLogs(); } Assert.Equal(HttpStatusCode.Created, ingestResponse.StatusCode); var ingestPayload = await ingestResponse.Content.ReadFromJsonAsync(); Assert.NotNull(ingestPayload); Assert.True(ingestPayload!.Inserted); Assert.False(string.IsNullOrWhiteSpace(ingestPayload.Id)); Assert.Equal("tenant-ingest", ingestPayload.Tenant); Assert.Equal(ComputeDeterministicContentHash(upstreamId), ingestPayload.ContentHash); Assert.NotNull(ingestResponse.Headers.Location); var locationValue = ingestResponse.Headers.Location!.ToString(); Assert.False(string.IsNullOrWhiteSpace(locationValue)); var lastSlashIndex = locationValue.LastIndexOf('/'); var idSegment = lastSlashIndex >= 0 ? locationValue[(lastSlashIndex + 1)..] : locationValue; var decodedSegment = Uri.UnescapeDataString(idSegment); Assert.Equal(ingestPayload.Id, decodedSegment); var duplicateResponse = await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest( contentHash: null, upstreamId: upstreamId)); Assert.Equal(HttpStatusCode.OK, duplicateResponse.StatusCode); var duplicatePayload = await duplicateResponse.Content.ReadFromJsonAsync(); Assert.NotNull(duplicatePayload); Assert.False(duplicatePayload!.Inserted); using (var getRequest = new HttpRequestMessage(HttpMethod.Get, $"/advisories/raw/{ingestPayload.Id}")) { getRequest.Headers.Add("X-Stella-Tenant", "tenant-ingest"); var getResponse = await client.SendAsync(getRequest); getResponse.EnsureSuccessStatusCode(); var record = await getResponse.Content.ReadFromJsonAsync(); Assert.NotNull(record); Assert.Equal(ingestPayload.Id, record!.Id); Assert.Equal("tenant-ingest", record.Tenant); Assert.Equal(ComputeDeterministicContentHash(upstreamId), record.Document.Upstream.ContentHash); } using (var listRequest = new HttpRequestMessage(HttpMethod.Get, "/advisories/raw?limit=10")) { listRequest.Headers.Add("X-Stella-Tenant", "tenant-ingest"); var listResponse = await client.SendAsync(listRequest); listResponse.EnsureSuccessStatusCode(); var listPayload = await listResponse.Content.ReadFromJsonAsync(); Assert.NotNull(listPayload); var record = Assert.Single(listPayload!.Records); Assert.Equal(ingestPayload.Id, record.Id); } } [Fact] public async Task AocVerifyEndpoint_ReturnsSummaryForTenant() { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-verify"); await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest( contentHash: "sha256:verify-1", upstreamId: "GHSA-VERIFY-001")); var verifyResponse = await client.PostAsJsonAsync("/aoc/verify", new AocVerifyRequest(null, null, null, null, null)); verifyResponse.EnsureSuccessStatusCode(); var verifyPayload = await verifyResponse.Content.ReadFromJsonAsync(); Assert.NotNull(verifyPayload); Assert.Equal("tenant-verify", verifyPayload!.Tenant); Assert.True(verifyPayload.Checked.Advisories >= 1); Assert.Equal(0, verifyPayload.Checked.Vex); Assert.True(verifyPayload.Metrics.IngestionWriteTotal >= verifyPayload.Checked.Advisories); Assert.Empty(verifyPayload.Violations); Assert.False(verifyPayload.Truncated); } [Fact] public async Task AocVerifyEndpoint_ReturnsViolationsForGuardFailures() { await SeedAdvisoryRawDocumentsAsync( CreateAdvisoryRawDocument( tenant: "tenant-verify-violations", vendor: "osv", upstreamId: "GHSA-VERIFY-ERR", contentHash: "sha256:verify-err", raw: new DocumentObject { { "id", "GHSA-VERIFY-ERR" }, { "severity", "critical" } })); using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-verify-violations"); var verifyResponse = await client.PostAsJsonAsync("/aoc/verify", new AocVerifyRequest(null, null, null, null, null)); verifyResponse.EnsureSuccessStatusCode(); var verifyPayload = await verifyResponse.Content.ReadFromJsonAsync(); Assert.NotNull(verifyPayload); Assert.Equal("tenant-verify-violations", verifyPayload!.Tenant); Assert.True(verifyPayload.Checked.Advisories >= 1); var violation = Assert.Single(verifyPayload.Violations); Assert.Equal("ERR_AOC_001", violation.Code); Assert.True(violation.Count >= 1); Assert.NotEmpty(violation.Examples); } [Fact] public async Task AdvisoryRawListEndpoint_SupportsCursorPagination() { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-list"); await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest("sha256:list-1", "GHSA-LIST-001")); await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest("sha256:list-2", "GHSA-LIST-002")); await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest("sha256:list-3", "GHSA-LIST-003")); using var firstRequest = new HttpRequestMessage(HttpMethod.Get, "/advisories/raw?limit=2"); firstRequest.Headers.Add("X-Stella-Tenant", "tenant-list"); var firstResponse = await client.SendAsync(firstRequest); firstResponse.EnsureSuccessStatusCode(); var firstPage = await firstResponse.Content.ReadFromJsonAsync(); Assert.NotNull(firstPage); Assert.Equal(2, firstPage!.Records.Count); Assert.True(firstPage.HasMore); Assert.False(string.IsNullOrWhiteSpace(firstPage.NextCursor)); using var secondRequest = new HttpRequestMessage(HttpMethod.Get, $"/advisories/raw?cursor={Uri.EscapeDataString(firstPage.NextCursor!)}"); secondRequest.Headers.Add("X-Stella-Tenant", "tenant-list"); var secondResponse = await client.SendAsync(secondRequest); secondResponse.EnsureSuccessStatusCode(); var secondPage = await secondResponse.Content.ReadFromJsonAsync(); Assert.NotNull(secondPage); Assert.Single(secondPage!.Records); Assert.False(secondPage.HasMore); Assert.Null(secondPage.NextCursor); var firstIds = firstPage.Records.Select(record => record.Id).ToArray(); var secondIds = secondPage.Records.Select(record => record.Id).ToArray(); Assert.Empty(firstIds.Intersect(secondIds)); } [Fact] public async Task AdvisoryEvidenceEndpoint_ReturnsDocumentsForCanonicalKey() { await SeedAdvisoryRawDocumentsAsync( CreateAdvisoryRawDocument("tenant-a", "vendor-x", "GHSA-2025-0001", "sha256:001", new DocumentObject("id", "GHSA-2025-0001:1")), CreateAdvisoryRawDocument("tenant-a", "vendor-y", "GHSA-2025-0001", "sha256:002", new DocumentObject("id", "GHSA-2025-0001:2")), CreateAdvisoryRawDocument("tenant-b", "vendor-x", "GHSA-2025-0001", "sha256:003", new DocumentObject("id", "GHSA-2025-0001:3"))); using var client = _factory.CreateClient(); var response = await client.GetAsync("/vuln/evidence/advisories/ghsa-2025-0001?tenant=tenant-a"); response.EnsureSuccessStatusCode(); var evidence = await response.Content.ReadFromJsonAsync(); Assert.NotNull(evidence); Assert.Equal("GHSA-2025-0001", evidence!.AdvisoryKey); Assert.Equal(2, evidence.Records.Count); Assert.All(evidence.Records, record => Assert.Equal("tenant-a", record.Tenant)); Assert.Null(evidence.Attestation); } [Fact] public async Task AdvisoryEvidenceEndpoint_AttachesAttestationWhenBundleProvided() { await SeedAdvisoryRawDocumentsAsync( CreateAdvisoryRawDocument("tenant-a", "vendor-x", "GHSA-2025-0003", "sha256:201", new DocumentObject("id", "GHSA-2025-0003:1"))); var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..", "..")); var sampleDir = Path.Combine(repoRoot, "docs", "samples", "evidence-bundle"); var tarPath = Path.Combine(sampleDir, "evidence-bundle-m0.tar.gz"); var manifestPath = Path.Combine(sampleDir, "manifest.json"); var transparencyPath = Path.Combine(sampleDir, "transparency.json"); using var client = _factory.CreateClient(); var requestUri = $"/vuln/evidence/advisories/GHSA-2025-0003?tenant=tenant-a&bundlePath={Uri.EscapeDataString(tarPath)}&manifestPath={Uri.EscapeDataString(manifestPath)}&transparencyPath={Uri.EscapeDataString(transparencyPath)}&pipelineVersion=git:test-sha"; var response = await client.GetAsync(requestUri); response.EnsureSuccessStatusCode(); var evidence = await response.Content.ReadFromJsonAsync(); Assert.NotNull(evidence); Assert.NotNull(evidence!.Attestation); Assert.Equal("evidence-bundle-m0", evidence.Attestation!.SubjectName); Assert.Equal("git:test-sha", evidence.Attestation.PipelineVersion); Assert.Equal(tarPath, evidence.Attestation.EvidenceBundlePath); } [Fact] [Trait("Category", "Attestation")] public async Task InternalAttestationVerify_ReturnsClaims() { var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..", "..")); var sampleDir = Path.Combine(repoRoot, "docs", "samples", "evidence-bundle"); var tarPath = Path.Combine(sampleDir, "evidence-bundle-m0.tar.gz"); var manifestPath = Path.Combine(sampleDir, "manifest.json"); var transparencyPath = Path.Combine(sampleDir, "transparency.json"); using var scope = _factory.Services.CreateScope(); var concOptions = scope.ServiceProvider.GetRequiredService>().Value; _output.WriteLine($"EvidenceRoot={concOptions.Evidence.RootAbsolute}"); Assert.StartsWith(concOptions.Evidence.RootAbsolute, tarPath, StringComparison.OrdinalIgnoreCase); using var client = _factory.CreateClient(); var request = new VerifyAttestationRequest(tarPath, manifestPath, transparencyPath, "git:test-sha"); var response = await client.PostAsJsonAsync("/internal/attestations/verify?tenant=demo", request); var responseBody = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, $"Attestation verify failed: {(int)response.StatusCode} {response.StatusCode} ยท {responseBody}"); var claims = JsonSerializer.Deserialize( responseBody, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); Assert.NotNull(claims); Assert.Equal("evidence-bundle-m0", claims!.SubjectName); Assert.Equal("git:test-sha", claims.PipelineVersion); Assert.Equal(tarPath, claims.EvidenceBundlePath); } [Fact] public async Task EvidenceBatch_ReturnsEmptyCollectionsWhenUnknown() { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add(TenantHeaderName, "demo"); var request = new EvidenceBatchRequest( new[] { new EvidenceBatchItemRequest("component-a", new[] { "pkg:purl/example@1.0.0" }, new[] { "ALIAS-1" }) }, ObservationLimit: 5, LinksetLimit: 5); var response = await client.PostAsJsonAsync("/v1/evidence/batch", request); response.EnsureSuccessStatusCode(); var payload = await response.Content.ReadFromJsonAsync(); Assert.NotNull(payload); var item = Assert.Single(payload!.Items); Assert.Equal("component-a", item.ComponentId); Assert.Empty(item.Observations); Assert.Empty(item.Linksets); Assert.False(item.HasMore); } [Fact] public async Task AdvisoryEvidenceEndpoint_FiltersByVendor() { await SeedAdvisoryRawDocumentsAsync( CreateAdvisoryRawDocument("tenant-a", "vendor-x", "GHSA-2025-0002", "sha256:101", new DocumentObject("id", "GHSA-2025-0002:1")), CreateAdvisoryRawDocument("tenant-a", "vendor-y", "GHSA-2025-0002", "sha256:102", new DocumentObject("id", "GHSA-2025-0002:2"))); using var client = _factory.CreateClient(); var response = await client.GetAsync("/vuln/evidence/advisories/GHSA-2025-0002?tenant=tenant-a&vendor=vendor-y"); response.EnsureSuccessStatusCode(); var evidence = await response.Content.ReadFromJsonAsync(); Assert.NotNull(evidence); var record = Assert.Single(evidence!.Records); Assert.Equal("vendor-y", record.Document.Source.Vendor); } [Fact] public async Task AdvisoryEvidenceEndpoint_ReturnsNotFoundWhenMissing() { await SeedAdvisoryRawDocumentsAsync(); using var client = _factory.CreateClient(); var response = await client.GetAsync("/vuln/evidence/advisories/CVE-2099-9999?tenant=tenant-a"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task AdvisoryChunksEndpoint_EmitsRequestAndCacheMetrics() { await SeedObservationDocumentsAsync(BuildSampleObservationDocuments()); await SeedCanonicalAdvisoriesAsync( CreateStructuredAdvisory( "CVE-2025-0001", "GHSA-2025-0001", "tenant-a:nvd:alpha:1", new DateTimeOffset(2025, 1, 5, 0, 0, 0, TimeSpan.Zero))); using var client = _factory.CreateClient(); long expectedSegments = 0; string expectedTruncatedTag = "false"; var metrics = await CaptureMetricsAsync( AdvisoryAiMetrics.MeterName, new[] { "advisory_ai_chunk_requests_total", "advisory_ai_chunk_cache_hits_total", "advisory_ai_chunk_latency_milliseconds", "advisory_ai_chunk_segments", "advisory_ai_chunk_sources" }, async () => { const string url = "/advisories/CVE-2025-0001/chunks?tenant=tenant-a"; var first = await client.GetAsync(url); first.EnsureSuccessStatusCode(); using (var firstDocument = await first.Content.ReadFromJsonAsync()) { Assert.NotNull(firstDocument); expectedSegments = firstDocument!.RootElement.GetProperty("entries").GetArrayLength(); expectedTruncatedTag = firstDocument.RootElement.GetProperty("truncated").GetBoolean() ? "true" : "false"; } var second = await client.GetAsync(url); second.EnsureSuccessStatusCode(); }); Assert.True(metrics.TryGetValue("advisory_ai_chunk_requests_total", out var requests)); Assert.NotNull(requests); Assert.Equal(2, requests!.Count); Assert.Contains(requests!, measurement => string.Equals(GetTagValue(measurement, "cache"), "miss", StringComparison.Ordinal)); Assert.Contains(requests!, measurement => string.Equals(GetTagValue(measurement, "cache"), "hit", StringComparison.Ordinal)); Assert.True(metrics.TryGetValue("advisory_ai_chunk_cache_hits_total", out var cacheHitMeasurements)); var cacheHit = Assert.Single(cacheHitMeasurements!); Assert.Equal(1, cacheHit.Value); Assert.Equal("hit", GetTagValue(cacheHit, "result")); Assert.True(metrics.TryGetValue("advisory_ai_chunk_latency_milliseconds", out var latencyMeasurements)); Assert.Equal(2, latencyMeasurements!.Count); Assert.All(latencyMeasurements!, measurement => Assert.True(measurement.Value > 0)); Assert.True(metrics.TryGetValue("advisory_ai_chunk_segments", out var segmentMeasurements)); Assert.Equal(2, segmentMeasurements!.Count); Assert.All(segmentMeasurements!, measurement => { Assert.Equal(expectedSegments, measurement.Value); Assert.Equal(expectedTruncatedTag, GetTagValue(measurement, "truncated")); }); Assert.True(metrics.TryGetValue("advisory_ai_chunk_sources", out var sourceMeasurements)); Assert.Equal(2, sourceMeasurements!.Count); } [Fact] public async Task AdvisoryChunksEndpoint_EmitsGuardrailMetrics() { var raw = DocumentObject.Parse("{\"details\":\"tiny\"}"); var document = CreateChunkObservationDocument( "tenant-a:chunk:1", "tenant-a", new DateTime(2025, 2, 1, 0, 0, 0, DateTimeKind.Utc), "CVE-2025-GUARD", raw); await SeedObservationDocumentsAsync(new[] { document }); await SeedCanonicalAdvisoriesAsync( CreateStructuredAdvisory( "CVE-2025-GUARD", "GHSA-2025-GUARD", "tenant-a:chunk:1", new DateTimeOffset(2025, 2, 1, 0, 0, 0, TimeSpan.Zero))); using var client = _factory.CreateClient(); var guardrailMetrics = await CaptureMetricsAsync( AdvisoryAiMetrics.MeterName, "advisory_ai_guardrail_blocks_total", async () => { var response = await client.GetAsync("/advisories/CVE-2025-GUARD/chunks?tenant=tenant-a"); response.EnsureSuccessStatusCode(); }); var measurement = Assert.Single(guardrailMetrics); Assert.True(measurement.Value >= 1); Assert.Equal("below_minimum_length", GetTagValue(measurement, "reason")); } [Fact] public async Task AdvisoryIngestEndpoint_EmitsMetricsWithExpectedTags() { var measurements = await CaptureMetricsAsync( IngestionMetrics.MeterName, "ingestion_write_total", async () => { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-metrics"); await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest("sha256:metric-1", "GHSA-METRIC-001")); await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest("sha256:metric-1", "GHSA-METRIC-001")); }); Assert.Equal(2, measurements.Count); var inserted = measurements.FirstOrDefault(measurement => string.Equals(GetTagValue(measurement, "tenant"), "tenant-metrics", StringComparison.Ordinal) && string.Equals(GetTagValue(measurement, "result"), "inserted", StringComparison.Ordinal)); Assert.NotNull(inserted); Assert.Equal(1, inserted!.Value); Assert.Equal("osv", GetTagValue(inserted, "source")); var duplicate = measurements.FirstOrDefault(measurement => string.Equals(GetTagValue(measurement, "tenant"), "tenant-metrics", StringComparison.Ordinal) && string.Equals(GetTagValue(measurement, "result"), "duplicate", StringComparison.Ordinal)); Assert.NotNull(duplicate); Assert.Equal(1, duplicate!.Value); Assert.Equal("osv", GetTagValue(duplicate, "source")); } [Fact] public async Task AocVerifyEndpoint_EmitsVerificationMetric() { var measurements = await CaptureMetricsAsync( IngestionMetrics.MeterName, "verify_runs_total", async () => { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-verify-metrics"); await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest("sha256:verify-metric", "GHSA-VERIFY-METRIC")); var verifyResponse = await client.PostAsJsonAsync("/aoc/verify", new AocVerifyRequest(null, null, null, null, null)); verifyResponse.EnsureSuccessStatusCode(); }); var measurement = Assert.Single(measurements); Assert.Equal("tenant-verify-metrics", GetTagValue(measurement, "tenant")); Assert.Equal("ok", GetTagValue(measurement, "result")); Assert.Equal(1, measurement.Value); } [Fact] public async Task AdvisoryIngestEndpoint_RejectsCrossTenantWhenAuthenticated() { IdentityModelEventSource.ShowPII = true; var environment = new Dictionary { ["CONCELIER_AUTHORITY__ENABLED"] = "true", ["CONCELIER_AUTHORITY__ALLOWANONYMOUSFALLBACK"] = "false", ["CONCELIER_AUTHORITY__ISSUER"] = TestAuthorityIssuer, ["CONCELIER_AUTHORITY__REQUIREHTTPSMETADATA"] = "false", ["CONCELIER_AUTHORITY__AUDIENCES__0"] = TestAuthorityAudience, ["CONCELIER_AUTHORITY__CLIENTID"] = "webservice-tests", ["CONCELIER_AUTHORITY__CLIENTSECRET"] = "unused", }; using var factory = new ConcelierApplicationFactory( _runner.ConnectionString, authority => { authority.Enabled = true; authority.AllowAnonymousFallback = false; authority.Issuer = TestAuthorityIssuer; authority.RequireHttpsMetadata = false; authority.Audiences.Clear(); authority.Audiences.Add(TestAuthorityAudience); authority.ClientId = "webservice-tests"; authority.ClientSecret = "unused"; }, environment); using var client = factory.CreateClient(); var schemes = await factory.Services.GetRequiredService().GetAllSchemesAsync(); _output.WriteLine("Schemes => " + string.Join(',', schemes.Select(s => s.Name))); var token = CreateTestToken("tenant-auth", StellaOpsScopes.AdvisoryIngest); _output.WriteLine("token => " + token); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-auth"); var ingestResponse = await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest("sha256:auth-1", "GHSA-AUTH-001")); if (ingestResponse.StatusCode != HttpStatusCode.Created) { var body = await ingestResponse.Content.ReadAsStringAsync(); _output.WriteLine($"ingestResponse => {(int)ingestResponse.StatusCode} {ingestResponse.StatusCode}: {body}"); var authLogs = factory.LoggerProvider.Snapshot("Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler"); foreach (var entry in authLogs) { _output.WriteLine($"authLog => {entry.Level}: {entry.Message} ({entry.Exception?.Message})"); } var programLogs = factory.LoggerProvider.Snapshot("StellaOps.Concelier.WebService.Program"); foreach (var entry in programLogs) { _output.WriteLine($"programLog => {entry.Level}: {entry.Message}"); } } Assert.Equal(HttpStatusCode.Created, ingestResponse.StatusCode); client.DefaultRequestHeaders.Remove("X-Stella-Tenant"); client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-other"); var crossTenantResponse = await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest("sha256:auth-2", "GHSA-AUTH-002")); Assert.Equal(HttpStatusCode.Forbidden, crossTenantResponse.StatusCode); } [Fact] public async Task AdvisoryIngestEndpoint_RejectsTenantOutsideAllowlist() { var environment = new Dictionary { ["CONCELIER_AUTHORITY__ENABLED"] = "true", ["CONCELIER_AUTHORITY__ALLOWANONYMOUSFALLBACK"] = "false", ["CONCELIER_AUTHORITY__ISSUER"] = TestAuthorityIssuer, ["CONCELIER_AUTHORITY__REQUIREHTTPSMETADATA"] = "false", ["CONCELIER_AUTHORITY__AUDIENCES__0"] = TestAuthorityAudience, ["CONCELIER_AUTHORITY__CLIENTID"] = "webservice-tests", ["CONCELIER_AUTHORITY__CLIENTSECRET"] = "unused", ["CONCELIER_AUTHORITY__REQUIREDTENANTS__0"] = "tenant-auth" }; using var factory = new ConcelierApplicationFactory( _runner.ConnectionString, authority => { authority.Enabled = true; authority.AllowAnonymousFallback = false; authority.Issuer = TestAuthorityIssuer; authority.RequireHttpsMetadata = false; authority.Audiences.Clear(); authority.Audiences.Add(TestAuthorityAudience); authority.ClientId = "webservice-tests"; authority.ClientSecret = "unused"; authority.RequiredTenants.Clear(); authority.RequiredTenants.Add("tenant-auth"); }, environment); using var client = factory.CreateClient(); var allowedToken = CreateTestToken("tenant-auth", StellaOpsScopes.AdvisoryIngest); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", allowedToken); client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-auth"); var allowedResponse = await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest("sha256:allow-1", "GHSA-ALLOW-001")); Assert.Equal(HttpStatusCode.Created, allowedResponse.StatusCode); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", CreateTestToken("tenant-blocked", StellaOpsScopes.AdvisoryIngest)); client.DefaultRequestHeaders.Remove("X-Stella-Tenant"); client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-blocked"); var forbiddenResponse = await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest("sha256:allow-2", "GHSA-ALLOW-002")); Assert.Equal(HttpStatusCode.Forbidden, forbiddenResponse.StatusCode); } [Fact] public async Task AdvisoryIngestEndpoint_ReturnsGuardViolationWhenContentHashMissing() { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-violation"); var invalidRequest = BuildAdvisoryIngestRequest( contentHash: string.Empty, upstreamId: "GHSA-INVALID-1", enforceContentHash: false); var response = await client.PostAsJsonAsync("/ingest/advisory", invalidRequest); Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode); var problemJson = await response.Content.ReadAsStringAsync(); using var document = JsonDocument.Parse(problemJson); var root = document.RootElement; Assert.Equal("Aggregation-Only Contract violation", root.GetProperty("title").GetString()); Assert.Equal(422, root.GetProperty("status").GetInt32()); Assert.True(root.TryGetProperty("violations", out var violations), "Problem response missing violations payload."); Assert.True(root.TryGetProperty("code", out var codeElement), "Problem response missing code payload."); Assert.Equal("ERR_AOC_004", codeElement.GetString()); var violation = Assert.Single(violations.EnumerateArray()); Assert.Equal("ERR_AOC_004", violation.GetProperty("code").GetString()); } [Fact] public async Task JobsEndpointsReturnExpectedStatuses() { using var client = _factory.CreateClient(); var definitions = await client.GetAsync("/jobs/definitions"); if (!definitions.IsSuccessStatusCode) { var body = await definitions.Content.ReadAsStringAsync(); throw new Xunit.Sdk.XunitException($"/jobs/definitions failed: {(int)definitions.StatusCode} {body}"); } var trigger = await client.PostAsync("/jobs/unknown", new StringContent("{}", System.Text.Encoding.UTF8, "application/json")); if (trigger.StatusCode != HttpStatusCode.NotFound) { var payload = await trigger.Content.ReadAsStringAsync(); throw new Xunit.Sdk.XunitException($"/jobs/unknown expected 404, got {(int)trigger.StatusCode}: {payload}"); } var problem = await trigger.Content.ReadFromJsonAsync(); Assert.NotNull(problem); Assert.Equal("https://stellaops.org/problems/not-found", problem!.Type); Assert.Equal(404, problem.Status); } [Fact] public async Task JobRunEndpointReturnsProblemWhenNotFound() { using var client = _factory.CreateClient(); var response = await client.GetAsync($"/jobs/{Guid.NewGuid()}"); if (response.StatusCode != HttpStatusCode.NotFound) { var body = await response.Content.ReadAsStringAsync(); throw new Xunit.Sdk.XunitException($"/jobs/{{id}} expected 404, got {(int)response.StatusCode}: {body}"); } var problem = await response.Content.ReadFromJsonAsync(); Assert.NotNull(problem); Assert.Equal("https://stellaops.org/problems/not-found", problem!.Type); } [Fact] public async Task JobTriggerMapsCoordinatorOutcomes() { var handler = _factory.Services.GetRequiredService(); using var client = _factory.CreateClient(); handler.NextResult = JobTriggerResult.AlreadyRunning("busy"); var conflict = await client.PostAsync("/jobs/test", JsonContent.Create(new JobTriggerRequest())); if (conflict.StatusCode != HttpStatusCode.Conflict) { var payload = await conflict.Content.ReadAsStringAsync(); throw new Xunit.Sdk.XunitException($"Conflict path expected 409, got {(int)conflict.StatusCode}: {payload}"); } var conflictProblem = await conflict.Content.ReadFromJsonAsync(); Assert.NotNull(conflictProblem); Assert.Equal("https://stellaops.org/problems/conflict", conflictProblem!.Type); handler.NextResult = JobTriggerResult.Accepted(new JobRunSnapshot(Guid.NewGuid(), "demo", JobRunStatus.Pending, DateTimeOffset.UtcNow, null, null, "api", null, null, null, null, new Dictionary())); var accepted = await client.PostAsync("/jobs/test", JsonContent.Create(new JobTriggerRequest())); if (accepted.StatusCode != HttpStatusCode.Accepted) { var payload = await accepted.Content.ReadAsStringAsync(); throw new Xunit.Sdk.XunitException($"Accepted path expected 202, got {(int)accepted.StatusCode}: {payload}"); } Assert.NotNull(accepted.Headers.Location); var acceptedPayload = await accepted.Content.ReadFromJsonAsync(); Assert.NotNull(acceptedPayload); handler.NextResult = JobTriggerResult.Failed(new JobRunSnapshot(Guid.NewGuid(), "demo", JobRunStatus.Failed, DateTimeOffset.UtcNow, null, DateTimeOffset.UtcNow, "api", null, "err", null, null, new Dictionary()), "boom"); var failed = await client.PostAsync("/jobs/test", JsonContent.Create(new JobTriggerRequest())); if (failed.StatusCode != HttpStatusCode.InternalServerError) { var payload = await failed.Content.ReadAsStringAsync(); throw new Xunit.Sdk.XunitException($"Failed path expected 500, got {(int)failed.StatusCode}: {payload}"); } var failureProblem = await failed.Content.ReadFromJsonAsync(); Assert.NotNull(failureProblem); Assert.Equal("https://stellaops.org/problems/job-failure", failureProblem!.Type); } [Fact] public async Task JobsEndpointsExposeJobData() { var handler = _factory.Services.GetRequiredService(); var now = DateTimeOffset.UtcNow; var run = new JobRunSnapshot( Guid.NewGuid(), "demo", JobRunStatus.Succeeded, now, now, now.AddSeconds(2), "api", "hash", null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(1), new Dictionary { ["key"] = "value" }); handler.Definitions = new[] { new JobDefinition("demo", typeof(DemoJob), TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(1), "*/5 * * * *", true) }; handler.LastRuns["demo"] = run; handler.RecentRuns = new[] { run }; handler.ActiveRuns = Array.Empty(); handler.Runs[run.RunId] = run; try { using var client = _factory.CreateClient(); var definitions = await client.GetFromJsonAsync>("/jobs/definitions"); Assert.NotNull(definitions); Assert.Single(definitions!); Assert.Equal("demo", definitions![0].Kind); Assert.NotNull(definitions[0].LastRun); Assert.Equal(run.RunId, definitions[0].LastRun!.RunId); var runPayload = await client.GetFromJsonAsync($"/jobs/{run.RunId}"); Assert.NotNull(runPayload); Assert.Equal(run.RunId, runPayload!.RunId); Assert.Equal("Succeeded", runPayload.Status); var runs = await client.GetFromJsonAsync>("/jobs?kind=demo&limit=5"); Assert.NotNull(runs); Assert.Single(runs!); Assert.Equal(run.RunId, runs![0].RunId); var runsByDefinition = await client.GetFromJsonAsync>("/jobs/definitions/demo/runs"); Assert.NotNull(runsByDefinition); Assert.Single(runsByDefinition!); var active = await client.GetFromJsonAsync>("/jobs/active"); Assert.NotNull(active); Assert.Empty(active!); } finally { handler.Definitions = Array.Empty(); handler.RecentRuns = Array.Empty(); handler.ActiveRuns = Array.Empty(); handler.Runs.Clear(); handler.LastRuns.Clear(); } } [Fact] public async Task AdvisoryReplayEndpointReturnsLatestStatement() { var vulnerabilityKey = "CVE-2025-9000"; var advisory = new Advisory( advisoryKey: vulnerabilityKey, title: "Replay Test", summary: "Example summary", language: "en", published: DateTimeOffset.Parse("2025-01-01T00:00:00Z", CultureInfo.InvariantCulture), modified: DateTimeOffset.Parse("2025-01-02T00:00:00Z", CultureInfo.InvariantCulture), severity: "medium", exploitKnown: false, aliases: new[] { vulnerabilityKey }, references: Array.Empty(), affectedPackages: Array.Empty(), cvssMetrics: Array.Empty(), provenance: Array.Empty()); var statementId = Guid.NewGuid(); using (var scope = _factory.Services.CreateScope()) { var eventLog = scope.ServiceProvider.GetRequiredService(); var appendRequest = new AdvisoryEventAppendRequest(new[] { new AdvisoryStatementInput( vulnerabilityKey, advisory, advisory.Modified ?? advisory.Published ?? DateTimeOffset.UtcNow, Array.Empty(), StatementId: statementId, AdvisoryKey: advisory.AdvisoryKey) }); await eventLog.AppendAsync(appendRequest, CancellationToken.None); } using var client = _factory.CreateClient(); var response = await client.GetAsync($"/concelier/advisories/{vulnerabilityKey}/replay"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var payload = await response.Content.ReadFromJsonAsync(); Assert.NotNull(payload); Assert.Equal(vulnerabilityKey, payload!.VulnerabilityKey, ignoreCase: true); var statement = Assert.Single(payload.Statements); Assert.Equal(statementId, statement.StatementId); Assert.Equal(advisory.AdvisoryKey, statement.Advisory.AdvisoryKey); Assert.False(string.IsNullOrWhiteSpace(statement.StatementHash)); Assert.True(payload.Conflicts is null || payload.Conflicts!.Count == 0); } [Fact] public async Task AdvisoryReplayEndpointReturnsConflictExplainer() { var vulnerabilityKey = "CVE-2025-9100"; var statementId = Guid.NewGuid(); var conflictId = Guid.NewGuid(); var recordedAt = DateTimeOffset.Parse("2025-02-01T00:00:00Z", CultureInfo.InvariantCulture); using (var scope = _factory.Services.CreateScope()) { var eventLog = scope.ServiceProvider.GetRequiredService(); var advisory = new Advisory( advisoryKey: vulnerabilityKey, title: "Base advisory", summary: "Baseline summary", language: "en", published: recordedAt.AddDays(-1), modified: recordedAt, severity: "critical", exploitKnown: false, aliases: new[] { vulnerabilityKey }, references: Array.Empty(), affectedPackages: Array.Empty(), cvssMetrics: Array.Empty(), provenance: Array.Empty()); var statementInput = new AdvisoryStatementInput( vulnerabilityKey, advisory, recordedAt, Array.Empty(), StatementId: statementId, AdvisoryKey: advisory.AdvisoryKey); await eventLog.AppendAsync(new AdvisoryEventAppendRequest(new[] { statementInput }), CancellationToken.None); var explainer = new MergeConflictExplainerPayload( Type: "severity", Reason: "mismatch", PrimarySources: new[] { "vendor" }, PrimaryRank: 1, SuppressedSources: new[] { "nvd" }, SuppressedRank: 5, PrimaryValue: "CRITICAL", SuppressedValue: "MEDIUM"); using var conflictDoc = JsonDocument.Parse(explainer.ToCanonicalJson()); var conflictInput = new AdvisoryConflictInput( vulnerabilityKey, conflictDoc, recordedAt, new[] { statementId }, ConflictId: conflictId); await eventLog.AppendAsync(new AdvisoryEventAppendRequest(Array.Empty(), new[] { conflictInput }), CancellationToken.None); } using var client = _factory.CreateClient(); var response = await client.GetAsync($"/concelier/advisories/{vulnerabilityKey}/replay"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var payload = await response.Content.ReadFromJsonAsync(); Assert.NotNull(payload); var conflicts = payload!.Conflicts ?? throw new XunitException("Conflicts was null"); var conflict = Assert.Single(conflicts); Assert.Equal(conflictId, conflict.ConflictId); Assert.Equal("severity", conflict.Explainer.Type); Assert.Equal("mismatch", conflict.Explainer.Reason); Assert.Equal("CRITICAL", conflict.Explainer.PrimaryValue); Assert.Equal("MEDIUM", conflict.Explainer.SuppressedValue); Assert.Equal(conflict.Explainer.ComputeHashHex(), conflict.ConflictHash); } [Fact] public async Task MirrorEndpointsServeConfiguredArtifacts() { using var temp = new TempDirectory(); var exportId = "20251019T120000Z"; var exportRoot = Path.Combine(temp.Path, exportId); var mirrorRoot = Path.Combine(exportRoot, "mirror"); var domainRoot = Path.Combine(mirrorRoot, "primary"); Directory.CreateDirectory(domainRoot); await File.WriteAllTextAsync( Path.Combine(mirrorRoot, "index.json"), """{"schemaVersion":1,"domains":[]}"""); await File.WriteAllTextAsync( Path.Combine(domainRoot, "manifest.json"), """{"domainId":"primary"}"""); await File.WriteAllTextAsync( Path.Combine(domainRoot, "bundle.json"), """{"advisories":[]}"""); await File.WriteAllTextAsync( Path.Combine(domainRoot, "bundle.json.jws"), "test-signature"); var environment = new Dictionary { ["CONCELIER_MIRROR__ENABLED"] = "true", ["CONCELIER_MIRROR__EXPORTROOT"] = temp.Path, ["CONCELIER_MIRROR__ACTIVEEXPORTID"] = exportId, ["CONCELIER_MIRROR__DOMAINS__0__ID"] = "primary", ["CONCELIER_MIRROR__DOMAINS__0__DISPLAYNAME"] = "Primary", ["CONCELIER_MIRROR__DOMAINS__0__REQUIREAUTHENTICATION"] = "false", ["CONCELIER_MIRROR__DOMAINS__0__MAXDOWNLOADREQUESTSPERHOUR"] = "5", ["CONCELIER_MIRROR__MAXINDEXREQUESTSPERHOUR"] = "5" }; using var factory = new ConcelierApplicationFactory(_runner.ConnectionString, environmentOverrides: environment); using var client = factory.CreateClient(); var indexResponse = await client.GetAsync("/concelier/exports/index.json"); Assert.Equal(HttpStatusCode.OK, indexResponse.StatusCode); var indexContent = await indexResponse.Content.ReadAsStringAsync(); Assert.Contains(@"""schemaVersion"":1", indexContent, StringComparison.Ordinal); var manifestResponse = await client.GetAsync("/concelier/exports/mirror/primary/manifest.json"); Assert.Equal(HttpStatusCode.OK, manifestResponse.StatusCode); var manifestContent = await manifestResponse.Content.ReadAsStringAsync(); Assert.Contains(@"""domainId"":""primary""", manifestContent, StringComparison.Ordinal); var bundleResponse = await client.GetAsync("/concelier/exports/mirror/primary/bundle.json.jws"); Assert.Equal(HttpStatusCode.OK, bundleResponse.StatusCode); var signatureContent = await bundleResponse.Content.ReadAsStringAsync(); Assert.Equal("test-signature", signatureContent); } [Fact] public async Task MirrorEndpointsEnforceAuthenticationForProtectedDomains() { using var temp = new TempDirectory(); var exportId = "20251019T120000Z"; var secureRoot = Path.Combine(temp.Path, exportId, "mirror", "secure"); Directory.CreateDirectory(secureRoot); await File.WriteAllTextAsync( Path.Combine(temp.Path, exportId, "mirror", "index.json"), """{"schemaVersion":1,"domains":[]}"""); await File.WriteAllTextAsync( Path.Combine(secureRoot, "manifest.json"), """{"domainId":"secure"}"""); var environment = new Dictionary { ["CONCELIER_MIRROR__ENABLED"] = "true", ["CONCELIER_MIRROR__EXPORTROOT"] = temp.Path, ["CONCELIER_MIRROR__ACTIVEEXPORTID"] = exportId, ["CONCELIER_MIRROR__DOMAINS__0__ID"] = "secure", ["CONCELIER_MIRROR__DOMAINS__0__REQUIREAUTHENTICATION"] = "true", ["CONCELIER_MIRROR__DOMAINS__0__MAXDOWNLOADREQUESTSPERHOUR"] = "5", ["CONCELIER_AUTHORITY__ENABLED"] = "true", ["CONCELIER_AUTHORITY__ALLOWANONYMOUSFALLBACK"] = "false", ["CONCELIER_AUTHORITY__ISSUER"] = "https://authority.example", ["CONCELIER_AUTHORITY__REQUIREHTTPSMETADATA"] = "false", ["CONCELIER_AUTHORITY__AUDIENCES__0"] = "api://concelier", ["CONCELIER_AUTHORITY__REQUIREDSCOPES__0"] = StellaOpsScopes.ConcelierJobsTrigger, ["CONCELIER_AUTHORITY__CLIENTID"] = "concelier-jobs", ["CONCELIER_AUTHORITY__CLIENTSECRET"] = "secret", ["CONCELIER_AUTHORITY__CLIENTSCOPES__0"] = StellaOpsScopes.ConcelierJobsTrigger }; using var factory = new ConcelierApplicationFactory( _runner.ConnectionString, authority => { authority.Enabled = true; authority.AllowAnonymousFallback = false; authority.Issuer = "https://authority.example"; authority.RequireHttpsMetadata = false; authority.Audiences.Clear(); authority.Audiences.Add("api://concelier"); authority.RequiredScopes.Clear(); authority.RequiredScopes.Add(StellaOpsScopes.ConcelierJobsTrigger); authority.ClientId = "concelier-jobs"; authority.ClientSecret = "secret"; }, environment); using var client = factory.CreateClient(); var response = await client.GetAsync("/concelier/exports/mirror/secure/manifest.json"); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); var authHeader = Assert.Single(response.Headers.WwwAuthenticate); Assert.Equal("Bearer", authHeader.Scheme); } [Fact] public async Task MirrorEndpointsRespectRateLimits() { using var temp = new TempDirectory(); var exportId = "20251019T130000Z"; var exportRoot = Path.Combine(temp.Path, exportId); var mirrorRoot = Path.Combine(exportRoot, "mirror"); Directory.CreateDirectory(mirrorRoot); await File.WriteAllTextAsync( Path.Combine(mirrorRoot, "index.json"), """{\"schemaVersion\":1,\"domains\":[]}""" ); var environment = new Dictionary { ["CONCELIER_MIRROR__ENABLED"] = "true", ["CONCELIER_MIRROR__EXPORTROOT"] = temp.Path, ["CONCELIER_MIRROR__ACTIVEEXPORTID"] = exportId, ["CONCELIER_MIRROR__MAXINDEXREQUESTSPERHOUR"] = "1", ["CONCELIER_MIRROR__DOMAINS__0__ID"] = "primary", ["CONCELIER_MIRROR__DOMAINS__0__REQUIREAUTHENTICATION"] = "false", ["CONCELIER_MIRROR__DOMAINS__0__MAXDOWNLOADREQUESTSPERHOUR"] = "1" }; using var factory = new ConcelierApplicationFactory(_runner.ConnectionString, environmentOverrides: environment); using var client = factory.CreateClient(); var okResponse = await client.GetAsync("/concelier/exports/index.json"); Assert.Equal(HttpStatusCode.OK, okResponse.StatusCode); var limitedResponse = await client.GetAsync("/concelier/exports/index.json"); Assert.Equal((HttpStatusCode)429, limitedResponse.StatusCode); Assert.NotNull(limitedResponse.Headers.RetryAfter); Assert.True(limitedResponse.Headers.RetryAfter!.Delta.HasValue); Assert.True(limitedResponse.Headers.RetryAfter!.Delta!.Value.TotalSeconds > 0); } [Fact] public void MergeModuleDisabledByDefault() { using var factory = new ConcelierApplicationFactory( _runner.ConnectionString, authorityConfigure: null, environmentOverrides: null); using var scope = factory.Services.CreateScope(); var provider = scope.ServiceProvider; #pragma warning disable CS0618, CONCELIER0001, CONCELIER0002 // Checking deprecated service registration state. Assert.Null(provider.GetService()); #pragma warning restore CS0618, CONCELIER0001, CONCELIER0002 var schedulerOptions = provider.GetRequiredService>().Value; Assert.DoesNotContain("merge:reconcile", schedulerOptions.Definitions.Keys); } [Fact] public void MergeModuleReenabledWhenFeatureFlagCleared() { var environment = new Dictionary { ["CONCELIER_FEATURES__NOMERGEENABLED"] = "false" }; using var factory = new ConcelierApplicationFactory( _runner.ConnectionString, authorityConfigure: null, environmentOverrides: environment); using var scope = factory.Services.CreateScope(); var provider = scope.ServiceProvider; #pragma warning disable CS0618, CONCELIER0001, CONCELIER0002 // Checking deprecated service registration state. Assert.NotNull(provider.GetService()); #pragma warning restore CS0618, CONCELIER0001, CONCELIER0002 var schedulerOptions = provider.GetRequiredService>().Value; Assert.Contains("merge:reconcile", schedulerOptions.Definitions.Keys); } [Fact] public void MergeJobRemovedWhenAllowlistExcludes() { var environment = new Dictionary { ["CONCELIER_FEATURES__NOMERGEENABLED"] = "false", ["CONCELIER_FEATURES__MERGEJOBALLOWLIST__0"] = "export:json" }; using var factory = new ConcelierApplicationFactory( _runner.ConnectionString, authorityConfigure: null, environmentOverrides: environment); using var scope = factory.Services.CreateScope(); var provider = scope.ServiceProvider; #pragma warning disable CS0618, CONCELIER0001, CONCELIER0002 // Checking deprecated service registration state. Assert.NotNull(provider.GetService()); #pragma warning restore CS0618, CONCELIER0001, CONCELIER0002 var schedulerOptions = provider.GetRequiredService>().Value; Assert.DoesNotContain("merge:reconcile", schedulerOptions.Definitions.Keys); } [Fact] public void MergeJobRemainsWhenAllowlisted() { var environment = new Dictionary { ["CONCELIER_FEATURES__NOMERGEENABLED"] = "false", ["CONCELIER_FEATURES__MERGEJOBALLOWLIST__0"] = "merge:reconcile" }; using var factory = new ConcelierApplicationFactory( _runner.ConnectionString, authorityConfigure: null, environmentOverrides: environment); using var scope = factory.Services.CreateScope(); var provider = scope.ServiceProvider; #pragma warning disable CS0618, CONCELIER0001, CONCELIER0002 // Checking deprecated service registration state. Assert.NotNull(provider.GetService()); #pragma warning restore CS0618, CONCELIER0001, CONCELIER0002 var schedulerOptions = provider.GetRequiredService>().Value; Assert.Contains("merge:reconcile", schedulerOptions.Definitions.Keys); } [Fact] public async Task JobsEndpointsAllowBypassWhenAuthorityEnabled() { var environment = new Dictionary { ["CONCELIER_AUTHORITY__ENABLED"] = "true", ["CONCELIER_AUTHORITY__ALLOWANONYMOUSFALLBACK"] = "false", ["CONCELIER_AUTHORITY__ISSUER"] = "https://authority.example", ["CONCELIER_AUTHORITY__REQUIREHTTPSMETADATA"] = "false", ["CONCELIER_AUTHORITY__AUDIENCES__0"] = "api://concelier", ["CONCELIER_AUTHORITY__REQUIREDSCOPES__0"] = StellaOpsScopes.ConcelierJobsTrigger, ["CONCELIER_AUTHORITY__BYPASSNETWORKS__0"] = "127.0.0.1/32", ["CONCELIER_AUTHORITY__BYPASSNETWORKS__1"] = "::1/128", ["CONCELIER_AUTHORITY__CLIENTID"] = "concelier-jobs", ["CONCELIER_AUTHORITY__CLIENTSECRET"] = "test-secret", ["CONCELIER_AUTHORITY__CLIENTSCOPES__0"] = StellaOpsScopes.ConcelierJobsTrigger, }; using var factory = new ConcelierApplicationFactory( _runner.ConnectionString, authority => { authority.Enabled = true; authority.AllowAnonymousFallback = false; authority.Issuer = "https://authority.example"; authority.RequireHttpsMetadata = false; authority.Audiences.Clear(); authority.Audiences.Add("api://concelier"); authority.RequiredScopes.Clear(); authority.RequiredScopes.Add(StellaOpsScopes.ConcelierJobsTrigger); authority.BypassNetworks.Clear(); authority.BypassNetworks.Add("127.0.0.1/32"); authority.BypassNetworks.Add("::1/128"); authority.ClientId = "concelier-jobs"; authority.ClientSecret = "test-secret"; }, environment); var handler = factory.Services.GetRequiredService(); handler.Definitions = new[] { new JobDefinition("demo", typeof(DemoJob), TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(1), null, true) }; using var client = factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Test-RemoteAddr", "127.0.0.1"); var response = await client.GetAsync("/jobs/definitions"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var auditLogs = factory.LoggerProvider.Snapshot("Concelier.Authorization.Audit"); var bypassLog = Assert.Single(auditLogs, entry => entry.TryGetState("Bypass", out var state) && state is bool flag && flag); Assert.True(bypassLog.TryGetState("RemoteAddress", out var remoteObj) && string.Equals(remoteObj?.ToString(), "127.0.0.1", StringComparison.Ordinal)); Assert.True(bypassLog.TryGetState("StatusCode", out var statusObj) && Convert.ToInt32(statusObj) == (int)HttpStatusCode.OK); } [Fact] public async Task JobsEndpointsRequireAuthWhenFallbackDisabled() { var enforcementEnvironment = new Dictionary { ["CONCELIER_AUTHORITY__ENABLED"] = "true", ["CONCELIER_AUTHORITY__ALLOWANONYMOUSFALLBACK"] = "false", ["CONCELIER_AUTHORITY__ISSUER"] = "https://authority.example", ["CONCELIER_AUTHORITY__REQUIREHTTPSMETADATA"] = "false", ["CONCELIER_AUTHORITY__AUDIENCES__0"] = "api://concelier", ["CONCELIER_AUTHORITY__REQUIREDSCOPES__0"] = StellaOpsScopes.ConcelierJobsTrigger, ["CONCELIER_AUTHORITY__CLIENTID"] = "concelier-jobs", ["CONCELIER_AUTHORITY__CLIENTSECRET"] = "test-secret", ["CONCELIER_AUTHORITY__CLIENTSCOPES__0"] = StellaOpsScopes.ConcelierJobsTrigger, }; using var factory = new ConcelierApplicationFactory( _runner.ConnectionString, authority => { authority.Enabled = true; authority.AllowAnonymousFallback = false; authority.Issuer = "https://authority.example"; authority.RequireHttpsMetadata = false; authority.Audiences.Clear(); authority.Audiences.Add("api://concelier"); authority.RequiredScopes.Clear(); authority.RequiredScopes.Add(StellaOpsScopes.ConcelierJobsTrigger); authority.BypassNetworks.Clear(); authority.ClientId = "concelier-jobs"; authority.ClientSecret = "test-secret"; }, enforcementEnvironment); var resolved = factory.Services.GetRequiredService>().Value; Assert.False(resolved.Authority.AllowAnonymousFallback); using var client = factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Test-RemoteAddr", "127.0.0.1"); var response = await client.GetAsync("/jobs/definitions"); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); var auditLogs = factory.LoggerProvider.Snapshot("Concelier.Authorization.Audit"); var enforcementLog = Assert.Single(auditLogs); Assert.True(enforcementLog.TryGetState("BypassAllowed", out var bypassAllowedObj) && bypassAllowedObj is bool bypassAllowed && bypassAllowed == false); Assert.True(enforcementLog.TryGetState("HasPrincipal", out var principalObj) && principalObj is bool hasPrincipal && hasPrincipal == false); } [Fact] public void AuthorityClientResilienceOptionsAreBound() { var environment = new Dictionary { ["CONCELIER_AUTHORITY__ENABLED"] = "true", ["CONCELIER_AUTHORITY__ISSUER"] = "https://authority.example", ["CONCELIER_AUTHORITY__REQUIREHTTPSMETADATA"] = "false", ["CONCELIER_AUTHORITY__AUDIENCES__0"] = "api://concelier", ["CONCELIER_AUTHORITY__REQUIREDSCOPES__0"] = StellaOpsScopes.ConcelierJobsTrigger, ["CONCELIER_AUTHORITY__CLIENTSCOPES__0"] = StellaOpsScopes.ConcelierJobsTrigger, ["CONCELIER_AUTHORITY__BACKCHANNELTIMEOUTSECONDS"] = "45", ["CONCELIER_AUTHORITY__RESILIENCE__ENABLERETRIES"] = "true", ["CONCELIER_AUTHORITY__RESILIENCE__RETRYDELAYS__0"] = "00:00:02", ["CONCELIER_AUTHORITY__RESILIENCE__RETRYDELAYS__1"] = "00:00:04", ["CONCELIER_AUTHORITY__RESILIENCE__ALLOWOFFLINECACHEFALLBACK"] = "false", ["CONCELIER_AUTHORITY__RESILIENCE__OFFLINECACHETOLERANCE"] = "00:02:30" }; using var factory = new ConcelierApplicationFactory( _runner.ConnectionString, authority => { authority.Enabled = true; authority.Issuer = "https://authority.example"; authority.RequireHttpsMetadata = false; authority.Audiences.Clear(); authority.Audiences.Add("api://concelier"); authority.RequiredScopes.Clear(); authority.RequiredScopes.Add(StellaOpsScopes.ConcelierJobsTrigger); authority.ClientScopes.Clear(); authority.ClientScopes.Add(StellaOpsScopes.ConcelierJobsTrigger); authority.BackchannelTimeoutSeconds = 45; }, environment); var monitor = factory.Services.GetRequiredService>(); var options = monitor.CurrentValue; Assert.Equal("https://authority.example", options.Authority); Assert.Equal(TimeSpan.FromSeconds(45), options.HttpTimeout); Assert.Equal(new[] { StellaOpsScopes.ConcelierJobsTrigger }, options.NormalizedScopes); Assert.Equal(new[] { TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(4) }, options.NormalizedRetryDelays); Assert.False(options.AllowOfflineCacheFallback); Assert.Equal(TimeSpan.FromSeconds(150), options.OfflineCacheTolerance); } private async Task SeedObservationDocumentsAsync(IEnumerable documents) { var client = new InMemoryClient(_runner.ConnectionString); var database = client.GetDatabase(StorageDefaults.DefaultDatabaseName); var collection = database.GetCollection(StorageDefaults.Collections.AdvisoryObservations); try { await database.DropCollectionAsync(StorageDefaults.Collections.AdvisoryObservations); } catch (StorageCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase)) { // Collection does not exist yet; ignore. } var snapshot = documents?.ToArray() ?? Array.Empty(); if (snapshot.Length == 0) { await collection.InsertManyAsync(snapshot); return; } await collection.InsertManyAsync(snapshot); var rawDocuments = snapshot .Select(doc => CreateAdvisoryRawDocument( doc.Tenant, doc.Source.Vendor, doc.Id, doc.Upstream.ContentHash, doc.Content.Raw.DeepClone().AsDocumentObject)) .ToArray(); await SeedAdvisoryRawDocumentsAsync(rawDocuments); } private async Task SeedLinksetDocumentsAsync(IEnumerable documents) { var client = new InMemoryClient(_runner.ConnectionString); var database = client.GetDatabase(StorageDefaults.DefaultDatabaseName); var collection = database.GetCollection(StorageDefaults.Collections.AdvisoryLinksets); try { await database.DropCollectionAsync(StorageDefaults.Collections.AdvisoryLinksets); } catch (StorageCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase)) { // Collection not created yet; safe to ignore. } var snapshot = documents?.ToArray() ?? Array.Empty(); if (snapshot.Length > 0) { await collection.InsertManyAsync(snapshot); } } private static AdvisoryLinksetDocument CreateLinksetDocument( string tenant, string source, string advisoryId, IEnumerable observationIds, IEnumerable purls, IEnumerable versions, DateTime createdAtUtc) { return new AdvisoryLinksetDocument { TenantId = tenant, Source = source, AdvisoryId = advisoryId, Observations = observationIds.ToList(), CreatedAt = DateTime.SpecifyKind(createdAtUtc, DateTimeKind.Utc), Normalized = new AdvisoryLinksetNormalizedDocument { Purls = purls.ToList(), Versions = versions.ToList() } }; } private static AdvisoryObservationDocument[] BuildSampleObservationDocuments() { return new[] { CreateObservationDocument( id: "tenant-a:nvd:alpha:1", tenant: "tenant-a", createdAt: new DateTime(2025, 1, 5, 0, 0, 0, DateTimeKind.Utc), aliases: new[] { "cve-2025-0001" }, purls: new[] { "pkg:npm/demo@1.0.0" }, cpes: new[] { "cpe:/a:vendor:product:1.0" }, references: new[] { ("advisory", "https://example.test/advisory-1") }), CreateObservationDocument( id: "tenant-a:ghsa:beta:1", tenant: "tenant-a", createdAt: new DateTime(2025, 1, 6, 0, 0, 0, DateTimeKind.Utc), aliases: new[] { "ghsa-2025-xyz", "cve-2025-0001" }, purls: new[] { "pkg:npm/demo@1.1.0" }, cpes: new[] { "cpe:/a:vendor:product:1.1" }, references: new[] { ("patch", "https://example.test/patch-1") }), CreateObservationDocument( id: "tenant-b:nvd:alpha:1", tenant: "tenant-b", createdAt: new DateTime(2025, 1, 7, 0, 0, 0, DateTimeKind.Utc), aliases: new[] { "cve-2025-0001" }, purls: new[] { "pkg:npm/demo@2.0.0" }, cpes: new[] { "cpe:/a:vendor:product:2.0" }, references: new[] { ("advisory", "https://example.test/advisory-2") }) }; } private static AdvisoryObservationDocument CreateObservationDocument( string id, string tenant, DateTime createdAt, IEnumerable? aliases = null, IEnumerable? purls = null, IEnumerable? cpes = null, IEnumerable<(string Type, string Url)>? references = null) { return new AdvisoryObservationDocument { Id = id, Tenant = tenant.ToLowerInvariant(), CreatedAt = createdAt, Source = new AdvisoryObservationSourceDocument { Vendor = "nvd", Stream = "feed", Api = "https://example.test/api" }, Upstream = new AdvisoryObservationUpstreamDocument { UpstreamId = id, DocumentVersion = null, FetchedAt = createdAt, ReceivedAt = createdAt, ContentHash = $"sha256:{id}", Signature = new AdvisoryObservationSignatureDocument { Present = false }, Metadata = new Dictionary(StringComparer.Ordinal) }, Content = new AdvisoryObservationContentDocument { Format = "csaf", SpecVersion = "2.0", Raw = DocumentObject.Parse("""{"observation":"%ID%"}""".Replace("%ID%", id)), Metadata = new Dictionary(StringComparer.Ordinal) }, Linkset = new AdvisoryObservationLinksetDocument { Aliases = aliases?.Where(value => value is not null).ToList(), Purls = purls?.Where(value => value is not null).ToList(), Cpes = cpes?.Where(value => value is not null).ToList(), References = references is null ? new List() : references .Select(reference => new AdvisoryObservationReferenceDocument { Type = reference.Type, Url = reference.Url }) .ToList() }, Attributes = new Dictionary(StringComparer.Ordinal) }; } private static AdvisoryObservationDocument CreateChunkObservationDocument( string id, string tenant, DateTime createdAt, string alias, DocumentObject rawDocument) { var document = CreateObservationDocument( id, tenant, createdAt, aliases: new[] { alias }); var clone = rawDocument.DeepClone().AsDocumentObject; document.Content.Raw = clone; document.Upstream.ContentHash = ComputeContentHash(clone); return document; } private static readonly DateTimeOffset DefaultIngestTimestamp = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); private static readonly ICryptoHash Hash = CryptoHashFactory.CreateDefault(); private static string ComputeContentHash(DocumentObject rawDocument) { var canonical = rawDocument.ToJson(new JsonWriterSettings { OutputMode = JsonOutputMode.RelaxedExtendedJson }); var digest = Hash.ComputeHashHex(Encoding.UTF8.GetBytes(canonical), HashAlgorithms.Sha256); return $"sha256:{digest}"; } private static string ComputeDeterministicContentHash(string upstreamId) { var raw = CreateJsonElement($@"{{""id"":""{upstreamId}"",""modified"":""{DefaultIngestTimestamp:O}""}}"); return NormalizeContentHash(null, raw, enforceContentHash: true); } private static string NormalizeContentHash(string? value, JsonElement raw, bool enforceContentHash) { if (!enforceContentHash) { return value ?? string.Empty; } if (!string.IsNullOrWhiteSpace(value)) { return value.Trim(); } var digest = Hash.ComputeHashHex(Encoding.UTF8.GetBytes(raw.GetRawText()), HashAlgorithms.Sha256); return $"sha256:{digest}"; } private sealed record ReplayResponse( string VulnerabilityKey, DateTimeOffset? AsOf, List Statements, List? Conflicts); private sealed record ReplayStatement( Guid StatementId, string VulnerabilityKey, string AdvisoryKey, Advisory Advisory, string StatementHash, DateTimeOffset AsOf, DateTimeOffset RecordedAt, IReadOnlyList InputDocumentIds); private sealed record ReplayConflict( Guid ConflictId, string VulnerabilityKey, IReadOnlyList StatementIds, string ConflictHash, DateTimeOffset AsOf, DateTimeOffset RecordedAt, string Details, MergeConflictExplainerPayload Explainer); private sealed class ConcelierApplicationFactory : WebApplicationFactory { private readonly string _connectionString; private readonly string? _previousPgDsn; private readonly string? _previousPgEnabled; private readonly string? _previousPgTimeout; private readonly string? _previousPgSchema; private readonly string? _previousTelemetryEnabled; private readonly string? _previousTelemetryLogging; private readonly string? _previousTelemetryTracing; private readonly string? _previousTelemetryMetrics; private readonly Action? _authorityConfigure; private readonly IDictionary _additionalPreviousEnvironment = new Dictionary(StringComparer.OrdinalIgnoreCase); public CollectingLoggerProvider LoggerProvider { get; } = new(); public ConcelierApplicationFactory( string connectionString, Action? authorityConfigure = null, IDictionary? environmentOverrides = null) { var defaultPostgresDsn = "Host=localhost;Port=5432;Database=concelier_test;Username=postgres;Password=postgres"; _connectionString = string.IsNullOrWhiteSpace(connectionString) ? defaultPostgresDsn : connectionString; _authorityConfigure = authorityConfigure; _previousPgDsn = Environment.GetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__CONNECTIONSTRING"); _previousPgEnabled = Environment.GetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__ENABLED"); _previousPgTimeout = Environment.GetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__COMMANDTIMEOUTSECONDS"); _previousPgSchema = Environment.GetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__SCHEMANAME"); _previousTelemetryEnabled = Environment.GetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLED"); _previousTelemetryLogging = Environment.GetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLELOGGING"); _previousTelemetryTracing = Environment.GetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLETRACING"); _previousTelemetryMetrics = Environment.GetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLEMETRICS"); var opensslPath = ResolveOpenSsl11Path(); if (!string.IsNullOrEmpty(opensslPath)) { var currentLd = Environment.GetEnvironmentVariable("LD_LIBRARY_PATH"); var merged = string.IsNullOrWhiteSpace(currentLd) ? opensslPath : string.Join(':', opensslPath, currentLd); Environment.SetEnvironmentVariable("LD_LIBRARY_PATH", merged); } Environment.SetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__CONNECTIONSTRING", _connectionString); Environment.SetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__ENABLED", "true"); Environment.SetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__COMMANDTIMEOUTSECONDS", "30"); Environment.SetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__SCHEMANAME", "vuln"); Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLED", "false"); Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLELOGGING", "false"); Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLETRACING", "false"); Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLEMETRICS", "false"); Environment.SetEnvironmentVariable("CONCELIER_SKIP_OPTIONS_VALIDATION", "1"); Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Testing"); Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing"); const string EvidenceRootKey = "CONCELIER_EVIDENCE__ROOT"; var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..", "..")); _additionalPreviousEnvironment[EvidenceRootKey] = Environment.GetEnvironmentVariable(EvidenceRootKey); Environment.SetEnvironmentVariable(EvidenceRootKey, repoRoot); const string TestSecretKey = "CONCELIER_AUTHORITY__TESTSIGNINGSECRET"; if (environmentOverrides is null || !environmentOverrides.ContainsKey(TestSecretKey)) { var previousSecret = Environment.GetEnvironmentVariable(TestSecretKey); _additionalPreviousEnvironment[TestSecretKey] = previousSecret; Environment.SetEnvironmentVariable(TestSecretKey, TestSigningSecret); } if (environmentOverrides is not null) { foreach (var kvp in environmentOverrides) { var previous = Environment.GetEnvironmentVariable(kvp.Key); _additionalPreviousEnvironment[kvp.Key] = previous; Environment.SetEnvironmentVariable(kvp.Key, kvp.Value); } } } private static string? ResolveOpenSsl11Path() { var current = AppContext.BaseDirectory; for (var i = 0; i < 8; i++) { var candidate = Path.GetFullPath(Path.Combine(current, "tests", "native", "openssl-1.1", "linux-x64")); if (Directory.Exists(candidate)) { return candidate; } current = Path.GetFullPath(Path.Combine(current, "..")); } return null; } protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment("Testing"); builder.ConfigureAppConfiguration((context, configurationBuilder) => { var settings = new Dictionary { ["Plugins:Directory"] = Path.GetFullPath(Path.Combine(context.HostingEnvironment.ContentRootPath, "..", "..", "..", "plugins", "concelier")), }; configurationBuilder.AddInMemoryCollection(settings!); }); builder.ConfigureLogging(logging => { logging.AddProvider(LoggerProvider); }); builder.ConfigureServices(services => { // Remove ConcelierDataSource to skip Postgres initialization during tests // This allows tests to run without a real database connection services.RemoveAll(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); // Register stubs for services required by AdvisoryRawService and AdvisoryObservationQueryService services.RemoveAll(); services.AddSingleton(); services.RemoveAll(); services.AddSingleton(); services.RemoveAll(); services.AddSingleton(); // Register stubs for storage and event log services services.RemoveAll(); services.AddSingleton(new StorageDatabase("test")); services.RemoveAll(); services.AddSingleton(); services.RemoveAll(); services.AddSingleton(); services.PostConfigure(options => { options.PostgresStorage ??= new ConcelierOptions.PostgresStorageOptions(); options.PostgresStorage.ConnectionString = _connectionString; options.PostgresStorage.CommandTimeoutSeconds = 30; options.Plugins.Directory ??= Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..", "..", "plugins", "concelier")); options.Telemetry.Enabled = false; options.Telemetry.EnableLogging = false; options.Telemetry.EnableTracing = false; options.Telemetry.EnableMetrics = false; options.Authority ??= new ConcelierOptions.AuthorityOptions(); _authorityConfigure?.Invoke(options.Authority); // Point evidence root at the repo so sample bundles under docs/samples/evidence-bundle resolve without 400. var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..", "..")); options.Evidence.Root = repoRoot; options.Evidence.RootAbsolute = repoRoot; }); // Ensure content root + wwwroot exist so host startup does not throw when WebService bin output isn't present. var contentRoot = AppContext.BaseDirectory; var wwwroot = Path.Combine(contentRoot, "wwwroot"); Directory.CreateDirectory(wwwroot); }); builder.ConfigureTestServices(services => { services.AddSingleton(); services.PostConfigure(StellaOpsAuthenticationDefaults.AuthenticationScheme, options => { options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = TestSigningKey, ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = false, NameClaimType = ClaimTypes.Name, RoleClaimType = ClaimTypes.Role, ClockSkew = TimeSpan.Zero }; var issuer = string.IsNullOrWhiteSpace(options.Authority) ? TestAuthorityIssuer : options.Authority; options.ConfigurationManager = new StaticConfigurationManager(new OpenIdConnectConfiguration { Issuer = issuer }); }); }); } protected override void Dispose(bool disposing) { base.Dispose(disposing); Environment.SetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__CONNECTIONSTRING", _previousPgDsn); Environment.SetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__ENABLED", _previousPgEnabled); Environment.SetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__COMMANDTIMEOUTSECONDS", _previousPgTimeout); Environment.SetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__SCHEMANAME", _previousPgSchema); Environment.SetEnvironmentVariable("CONCELIER_SKIP_OPTIONS_VALIDATION", null); Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLED", _previousTelemetryEnabled); Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLELOGGING", _previousTelemetryLogging); Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLETRACING", _previousTelemetryTracing); Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLEMETRICS", _previousTelemetryMetrics); foreach (var kvp in _additionalPreviousEnvironment) { Environment.SetEnvironmentVariable(kvp.Key, kvp.Value); } LoggerProvider.Dispose(); } private sealed class RemoteIpStartupFilter : IStartupFilter { public Action Configure(Action next) { return app => { app.Use(async (context, nextMiddleware) => { if (context.Request.Headers.TryGetValue("X-Test-RemoteAddr", out var values) && values.Count > 0 && IPAddress.TryParse(values[0], out var remote)) { context.Connection.RemoteIpAddress = remote; } await nextMiddleware(); }); next(app); }; } } public sealed record LogEntry( string LoggerName, LogLevel Level, EventId EventId, string? Message, Exception? Exception, IReadOnlyList> State) { public bool TryGetState(string name, out object? value) { foreach (var kvp in State) { if (string.Equals(kvp.Key, name, StringComparison.Ordinal)) { value = kvp.Value; return true; } } value = null; return false; } } public sealed class CollectingLoggerProvider : ILoggerProvider { private readonly object syncRoot = new(); private readonly List entries = new(); private bool disposed; public ILogger CreateLogger(string categoryName) => new CollectingLogger(categoryName, this); public IReadOnlyList Snapshot(string loggerName) { lock (syncRoot) { return entries .Where(entry => string.Equals(entry.LoggerName, loggerName, StringComparison.Ordinal)) .ToArray(); } } public void Dispose() { disposed = true; lock (syncRoot) { entries.Clear(); } } private void Append(LogEntry entry) { if (disposed) { return; } lock (syncRoot) { entries.Add(entry); } } private sealed class CollectingLogger : ILogger { private readonly string categoryName; private readonly CollectingLoggerProvider provider; public CollectingLogger(string categoryName, CollectingLoggerProvider provider) { this.categoryName = categoryName; this.provider = provider; } public IDisposable? BeginScope(TState state) where TState : notnull => NullScope.Instance; public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (formatter is null) { throw new ArgumentNullException(nameof(formatter)); } var message = formatter(state, exception); var kvps = ExtractState(state); var entry = new LogEntry(categoryName, logLevel, eventId, message, exception, kvps); provider.Append(entry); } private static IReadOnlyList> ExtractState(TState state) { if (state is IReadOnlyList> list) { return list; } if (state is IEnumerable> enumerable) { return enumerable.ToArray(); } if (state is null) { return Array.Empty>(); } return new[] { new KeyValuePair("State", state) }; } } private sealed class NullScope : IDisposable { public static readonly NullScope Instance = new(); public void Dispose() { } } } private sealed class StubAdvisoryRawService : IAdvisoryRawService { public Task IngestAsync(AdvisoryRawDocument document, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var record = new AdvisoryRawRecord(Guid.NewGuid().ToString("D"), document, DateTimeOffset.UnixEpoch, DateTimeOffset.UnixEpoch); return Task.FromResult(new AdvisoryRawUpsertResult(true, record)); } public Task FindByIdAsync(string tenant, string id, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); return Task.FromResult(null); } public Task QueryAsync(AdvisoryRawQueryOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); return Task.FromResult(new AdvisoryRawQueryResult(Array.Empty(), null, false)); } public Task> FindByAdvisoryKeyAsync( string tenant, string advisoryKey, IReadOnlyCollection sourceVendors, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); return Task.FromResult>(Array.Empty()); } public Task VerifyAsync(AdvisoryRawVerificationRequest request, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); return Task.FromResult(new AdvisoryRawVerificationResult( request.Tenant, request.Since, request.Until, 0, Array.Empty(), false)); } } private sealed class StubAdvisoryObservationLookup : IAdvisoryObservationLookup { public ValueTask> ListByTenantAsync( string tenant, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); return ValueTask.FromResult>(Array.Empty()); } public ValueTask> FindByFiltersAsync( string tenant, IReadOnlyCollection observationIds, IReadOnlyCollection aliases, IReadOnlyCollection purls, IReadOnlyCollection cpes, AdvisoryObservationCursor? cursor, int limit, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); return ValueTask.FromResult>(Array.Empty()); } } private sealed class StubAdvisoryObservationQueryService : IAdvisoryObservationQueryService { public ValueTask QueryAsync( AdvisoryObservationQueryOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var emptyLinkset = new AdvisoryObservationLinksetAggregate( System.Collections.Immutable.ImmutableArray.Empty, System.Collections.Immutable.ImmutableArray.Empty, System.Collections.Immutable.ImmutableArray.Empty, System.Collections.Immutable.ImmutableArray.Empty); return ValueTask.FromResult(new AdvisoryObservationQueryResult( System.Collections.Immutable.ImmutableArray.Empty, emptyLinkset, null, false)); } } private sealed class StubAdvisoryEventLog : IAdvisoryEventLog { private readonly ConcurrentDictionary> _statements = new(StringComparer.OrdinalIgnoreCase); public ValueTask AppendAsync(AdvisoryEventAppendRequest request, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); foreach (var statement in request.Statements) { var list = _statements.GetOrAdd(statement.VulnerabilityKey, _ => new List()); lock (list) { list.Add(statement); } } return ValueTask.CompletedTask; } public ValueTask ReplayAsync(string vulnerabilityKey, DateTimeOffset? asOf, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (_statements.TryGetValue(vulnerabilityKey, out var statements) && statements.Count > 0) { var snapshots = statements .Select(s => new AdvisoryStatementSnapshot( s.StatementId ?? Guid.NewGuid(), s.VulnerabilityKey, s.AdvisoryKey ?? s.Advisory.AdvisoryKey, s.Advisory, System.Collections.Immutable.ImmutableArray.Empty, s.AsOf, DateTimeOffset.UtcNow, System.Collections.Immutable.ImmutableArray.Empty)) .ToImmutableArray(); return ValueTask.FromResult(new AdvisoryReplay( vulnerabilityKey, asOf, snapshots, System.Collections.Immutable.ImmutableArray.Empty)); } return ValueTask.FromResult(new AdvisoryReplay( vulnerabilityKey, asOf, System.Collections.Immutable.ImmutableArray.Empty, System.Collections.Immutable.ImmutableArray.Empty)); } public ValueTask AttachStatementProvenanceAsync(Guid statementId, DsseProvenance provenance, TrustInfo trust, CancellationToken cancellationToken) => ValueTask.CompletedTask; } private sealed class StubAdvisoryStore : IAdvisoryStore { private readonly ConcurrentDictionary _advisories = new(StringComparer.OrdinalIgnoreCase); public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); _advisories[advisory.AdvisoryKey] = advisory; return Task.CompletedTask; } public Task FindAsync(string advisoryKey, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); _advisories.TryGetValue(advisoryKey, out var advisory); return Task.FromResult(advisory); } public Task> GetRecentAsync(int limit, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var result = _advisories.Values .OrderByDescending(a => a.Modified ?? a.Published ?? DateTimeOffset.MinValue) .Take(limit) .ToArray(); return Task.FromResult>(result); } public async IAsyncEnumerable StreamAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { foreach (var advisory in _advisories.Values.OrderBy(a => a.AdvisoryKey, StringComparer.OrdinalIgnoreCase)) { cancellationToken.ThrowIfCancellationRequested(); yield return advisory; await Task.Yield(); } } } } [Fact] public async Task StatementProvenanceEndpointAttachesMetadata() { var tenant = "tenant-provenance"; var vulnerabilityKey = "CVE-2025-9200"; var statementId = Guid.NewGuid(); var recordedAt = DateTimeOffset.Parse("2025-03-01T00:00:00Z", CultureInfo.InvariantCulture); using (var scope = _factory.Services.CreateScope()) { var eventLog = scope.ServiceProvider.GetRequiredService(); var advisory = new Advisory( advisoryKey: vulnerabilityKey, title: "Provenance seed", summary: "Ready for DSSE metadata", language: "en", published: recordedAt.AddDays(-1), modified: recordedAt, severity: "high", exploitKnown: false, aliases: new[] { vulnerabilityKey }, references: Array.Empty(), affectedPackages: Array.Empty(), cvssMetrics: Array.Empty(), provenance: Array.Empty()); var statementInput = new AdvisoryStatementInput( vulnerabilityKey, advisory, recordedAt, InputDocumentIds: Array.Empty(), StatementId: statementId, AdvisoryKey: advisory.AdvisoryKey); await eventLog.AppendAsync(new AdvisoryEventAppendRequest(new[] { statementInput }), CancellationToken.None); } try { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Stella-Tenant", tenant); var response = await client.PostAsync( $"/events/statements/{statementId}/provenance?tenant={tenant}", new StringContent(BuildProvenancePayload(), Encoding.UTF8, "application/json")); Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); using var validationScope = _factory.Services.CreateScope(); var database = validationScope.ServiceProvider.GetRequiredService(); var statements = database.GetCollection(StorageDefaults.Collections.AdvisoryStatements); var stored = await statements .Find(Builders.Filter.Eq("_id", statementId.ToString())) .FirstOrDefaultAsync(); Assert.NotNull(stored); var dsse = stored!["provenance"].AsDocumentObject["dsse"].AsDocumentObject; Assert.Equal("sha256:feedface", dsse["envelopeDigest"].AsString); var trustDoc = stored["trust"].AsDocumentObject; Assert.True(trustDoc["verified"].AsBoolean); Assert.Equal("Authority@stella", trustDoc["verifier"].AsString); } finally { using var cleanupScope = _factory.Services.CreateScope(); var database = cleanupScope.ServiceProvider.GetRequiredService(); var statements = database.GetCollection(StorageDefaults.Collections.AdvisoryStatements); await statements.DeleteOneAsync(Builders.Filter.Eq("_id", statementId.ToString())); } } private static string BuildProvenancePayload() { var payload = new { dsse = new { envelopeDigest = "sha256:feedface", payloadType = "application/vnd.in-toto+json", key = new { keyId = "cosign:SHA256-PKIX:fixture", issuer = "Authority@stella", algo = "Ed25519" }, rekor = new { logIndex = 1337, uuid = "11111111-2222-3333-4444-555555555555", integratedTime = 1731081600 } }, trust = new { verified = true, verifier = "Authority@stella", witnesses = 1, policyScore = 1.0 } }; return JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web)); } private sealed class TempDirectory : IDisposable { public string Path { get; } public TempDirectory() { Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "concelier-mirror-" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); Directory.CreateDirectory(Path); } public void Dispose() { try { if (Directory.Exists(Path)) { Directory.Delete(Path, recursive: true); } } catch { // best effort cleanup } } } private sealed record HealthPayload(string Status, DateTimeOffset StartedAt, double UptimeSeconds, StoragePayload Storage, TelemetryPayload Telemetry); private sealed record StoragePayload(string Backend, bool Ready, DateTimeOffset? CheckedAt, double? LatencyMs, string? Error); private sealed record TelemetryPayload(bool Enabled, bool Tracing, bool Metrics, bool Logging); private sealed record ReadyPayload(string Status, DateTimeOffset StartedAt, double UptimeSeconds, StoragePayload Storage); private sealed record JobDefinitionPayload(string Kind, bool Enabled, string? CronExpression, TimeSpan Timeout, TimeSpan LeaseDuration, JobRunPayload? LastRun); private sealed record JobRunPayload(Guid RunId, string Kind, string Status, string Trigger, DateTimeOffset CreatedAt, DateTimeOffset? StartedAt, DateTimeOffset? CompletedAt, string? Error, TimeSpan? Duration, Dictionary Parameters); private sealed record ProblemDocument(string? Type, string? Title, int? Status, string? Detail, string? Instance); private async Task SeedCanonicalAdvisoriesAsync(params Advisory[] advisories) { using var scope = _factory.Services.CreateScope(); var database = scope.ServiceProvider.GetRequiredService(); await DropCollectionIfExistsAsync(database, StorageDefaults.Collections.Advisory); await DropCollectionIfExistsAsync(database, StorageDefaults.Collections.Alias); if (advisories.Length == 0) { return; } var store = scope.ServiceProvider.GetRequiredService(); foreach (var advisory in advisories) { await store.UpsertAsync(advisory, CancellationToken.None); } } private static async Task DropCollectionIfExistsAsync(IStorageDatabase database, string collectionName) { try { await database.DropCollectionAsync(collectionName); } catch (StorageCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase)) { } } private static Advisory CreateStructuredAdvisory( string advisoryKey, string alias, string observationId, DateTimeOffset recordedAt) { const string WorkaroundTitle = "Vendor guidance"; const string WorkaroundSummary = "Apply configuration change immediately."; const string WorkaroundUrl = "https://vendor.example/workaround"; var reference = new AdvisoryReference( WorkaroundUrl, kind: "workaround", sourceTag: WorkaroundTitle, summary: WorkaroundSummary, new AdvisoryProvenance( "nvd", "workaround", observationId, recordedAt, new[] { "/references/0" })); var affectedRange = new AffectedVersionRange( rangeKind: "semver", introducedVersion: "1.0.0", fixedVersion: "1.1.0", lastAffectedVersion: null, rangeExpression: ">=1.0.0,<1.1.0", new AdvisoryProvenance( "nvd", "affected", observationId, recordedAt, new[] { "/affectedPackages/0/versionRanges/0" })); var affectedPackage = new AffectedPackage( type: AffectedPackageTypes.SemVer, identifier: "pkg:npm/demo", versionRanges: new[] { affectedRange }, statuses: Array.Empty(), provenance: new[] { new AdvisoryProvenance( "nvd", "affected", observationId, recordedAt, new[] { "/affectedPackages/0" }) }, normalizedVersions: Array.Empty()); var cvss = new CvssMetric( "3.1", "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", 9.8, "critical", new AdvisoryProvenance( "nvd", "cvss", observationId, recordedAt, new[] { "/cvssMetrics/0" })); var advisory = new Advisory( advisoryKey, title: "Fixture advisory", summary: "Structured payload fixture", language: "en", published: recordedAt, modified: recordedAt, severity: "critical", exploitKnown: false, aliases: string.IsNullOrWhiteSpace(alias) ? new[] { advisoryKey } : new[] { advisoryKey, alias }, references: new[] { reference }, affectedPackages: new[] { affectedPackage }, cvssMetrics: new[] { cvss }, provenance: new[] { new AdvisoryProvenance("nvd", "advisory", observationId, recordedAt) }); return advisory; } private async Task SeedAdvisoryRawDocumentsAsync(params DocumentObject[] documents) { var client = new InMemoryClient(_runner.ConnectionString); var database = client.GetDatabase(StorageDefaults.DefaultDatabaseName); var collection = database.GetCollection(StorageDefaults.Collections.AdvisoryRaw); await collection.DeleteManyAsync(FilterDefinition.Empty); if (documents.Length > 0) { await collection.InsertManyAsync(documents); } } private static DocumentObject CreateAdvisoryRawDocument( string tenant, string vendor, string upstreamId, string contentHash, DocumentObject? raw = null, string? supersedes = null) { var now = DateTime.UtcNow; return new DocumentObject { { "_id", BuildRawDocumentId(vendor, upstreamId, contentHash) }, { "tenant", tenant }, { "source", new DocumentObject { { "vendor", vendor }, { "connector", "test-connector" }, { "version", "1.0.0" } } }, { "upstream", new DocumentObject { { "upstream_id", upstreamId }, { "document_version", "1" }, { "retrieved_at", now }, { "content_hash", contentHash }, { "signature", new DocumentObject { { "present", false } } }, { "provenance", new DocumentObject { { "api", "https://example.test" } } } } }, { "content", new DocumentObject { { "format", "osv" }, { "raw", raw ?? new DocumentObject("id", upstreamId) } } }, { "identifiers", new DocumentObject { { "aliases", new DocumentArray(new[] { upstreamId }) }, { "primary", upstreamId } } }, { "linkset", new DocumentObject { { "aliases", new DocumentArray() }, { "purls", new DocumentArray() }, { "cpes", new DocumentArray() }, { "references", new DocumentArray() }, { "reconciled_from", new DocumentArray() }, { "notes", new DocumentObject() } } }, { "advisory_key", upstreamId.ToUpperInvariant() }, { "links", new DocumentArray { new DocumentObject { { "scheme", "PRIMARY" }, { "value", upstreamId.ToUpperInvariant() } } } }, { "supersedes", supersedes is null ? DocumentNull.Value : supersedes }, { "ingested_at", now }, { "created_at", now } }; } private static string BuildRawDocumentId(string vendor, string upstreamId, string contentHash) { static string Sanitize(string value) { if (string.IsNullOrWhiteSpace(value)) { return "unknown"; } var buffer = new char[value.Length]; var index = 0; foreach (var ch in value.Trim().ToLowerInvariant()) { buffer[index++] = char.IsLetterOrDigit(ch) ? ch : '-'; } var sanitized = new string(buffer, 0, index).Trim('-'); return string.IsNullOrEmpty(sanitized) ? "unknown" : sanitized; } var vendorSegment = Sanitize(vendor); var upstreamSegment = Sanitize(upstreamId); var hashSegment = Sanitize(contentHash.Replace(":", "-")); return $"advisory_raw:{vendorSegment}:{upstreamSegment}:{hashSegment}"; } private void WriteProgramLogs() { var entries = _factory.LoggerProvider.Snapshot("StellaOps.Concelier.WebService.Program"); foreach (var entry in entries) { _output.WriteLine($"[PROGRAM LOG] {entry.Level}: {entry.Message}"); } } private static void WarmupFactory(WebApplicationFactory factory) { using var client = factory.CreateClient(); } private static AdvisoryIngestRequest BuildAdvisoryIngestRequest( string? contentHash, string upstreamId, bool enforceContentHash = true, IReadOnlyList? purls = null, IReadOnlyList? notes = null) { var raw = CreateJsonElement($@"{{""id"":""{upstreamId}"",""modified"":""{DefaultIngestTimestamp:O}""}}"); var normalizedContentHash = NormalizeContentHash(contentHash, raw, enforceContentHash); var resolvedPurls = purls ?? new[] { "pkg:npm/demo@1.0.0" }; var resolvedNotes = notes ?? Array.Empty(); var references = new[] { new AdvisoryLinksetReferenceRequest("advisory", $"https://example.test/advisories/{upstreamId}", null) }; return new AdvisoryIngestRequest( new AdvisorySourceRequest("osv", "osv-connector", "1.0.0", "feed"), new AdvisoryUpstreamRequest( upstreamId, "2025-01-01T00:00:00Z", DateTimeOffset.UtcNow, normalizedContentHash, new AdvisorySignatureRequest(false, null, null, null, null, null), new Dictionary { ["http.method"] = "GET" }), new AdvisoryContentRequest("osv", "1.3.0", raw, null), new AdvisoryIdentifiersRequest( upstreamId, new[] { upstreamId, $"{upstreamId}-ALIAS" }), new AdvisoryLinksetRequest( new[] { upstreamId }, resolvedPurls, Array.Empty(), Array.Empty(), Array.Empty(), references, resolvedNotes, new Dictionary { ["note"] = "ingest-test" })); } private static JsonElement CreateJsonElement(string json) { using var document = JsonDocument.Parse(json); return document.RootElement.Clone(); } private static async Task> CaptureMetricsAsync(string meterName, string instrumentName, Func action) { var map = await CaptureMetricsAsync(meterName, new[] { instrumentName }, action).ConfigureAwait(false); return map.TryGetValue(instrumentName, out var measurements) ? measurements : Array.Empty(); } private static async Task>> CaptureMetricsAsync( string meterName, IReadOnlyCollection instrumentNames, Func action) { var measurementMap = instrumentNames.ToDictionary( name => name, _ => new List(), StringComparer.Ordinal); var instrumentSet = new HashSet(instrumentNames, StringComparer.Ordinal); var listener = new MeterListener(); listener.InstrumentPublished += (instrument, currentListener) => { if (string.Equals(instrument.Meter.Name, meterName, StringComparison.Ordinal) && instrumentSet.Contains(instrument.Name)) { currentListener.EnableMeasurementEvents(instrument); } }; void RecordMeasurement(Instrument instrument, double measurement, ReadOnlySpan> tags) { if (!measurementMap.TryGetValue(instrument.Name, out var list)) { return; } var tagDictionary = new Dictionary(StringComparer.Ordinal); foreach (var tag in tags) { tagDictionary[tag.Key] = tag.Value; } list.Add(new MetricMeasurement(instrument.Name, measurement, tagDictionary)); } listener.SetMeasurementEventCallback((instrument, measurement, tags, state) => RecordMeasurement(instrument, measurement, tags)); listener.SetMeasurementEventCallback((instrument, measurement, tags, state) => RecordMeasurement(instrument, measurement, tags)); listener.Start(); try { await action().ConfigureAwait(false); } finally { listener.Dispose(); } return measurementMap.ToDictionary( kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value); } private static string? GetTagValue(MetricMeasurement measurement, string tag) { if (measurement.Tags.TryGetValue(tag, out var value)) { return value?.ToString(); } return null; } private static string CreateTestToken(string tenant, params string[] scopes) { var normalizedTenant = string.IsNullOrWhiteSpace(tenant) ? "default" : tenant.Trim().ToLowerInvariant(); var scopeSet = scopes is { Length: > 0 } ? scopes .Select(StellaOpsScopes.Normalize) .Where(static scope => !string.IsNullOrEmpty(scope)) .Select(static scope => scope!) .Distinct(StringComparer.Ordinal) .ToArray() : Array.Empty(); var claims = new List { new Claim(StellaOpsClaimTypes.Subject, "test-user"), new Claim(StellaOpsClaimTypes.Tenant, normalizedTenant), new Claim(StellaOpsClaimTypes.Scope, string.Join(' ', scopeSet)) }; foreach (var scope in scopeSet) { claims.Add(new Claim(StellaOpsClaimTypes.ScopeItem, scope)); } var credentials = new SigningCredentials(TestSigningKey, SecurityAlgorithms.HmacSha256); var now = DateTime.UtcNow; var token = new JwtSecurityToken( issuer: TestAuthorityIssuer, audience: TestAuthorityAudience, claims: claims, notBefore: now.AddMinutes(-5), expires: now.AddMinutes(30), signingCredentials: credentials); return new JwtSecurityTokenHandler().WriteToken(token); } private sealed record MetricMeasurement(string Instrument, double Value, IReadOnlyDictionary Tags); private sealed class DemoJob : IJob { public Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken) => Task.CompletedTask; } private sealed class StubJobCoordinator : IJobCoordinator { public JobTriggerResult NextResult { get; set; } = JobTriggerResult.NotFound("not set"); public IReadOnlyList Definitions { get; set; } = Array.Empty(); public IReadOnlyList RecentRuns { get; set; } = Array.Empty(); public IReadOnlyList ActiveRuns { get; set; } = Array.Empty(); public Dictionary Runs { get; } = new(); public Dictionary LastRuns { get; } = new(StringComparer.Ordinal); public Task TriggerAsync(string kind, IReadOnlyDictionary? parameters, string trigger, CancellationToken cancellationToken) => Task.FromResult(NextResult); public Task> GetDefinitionsAsync(CancellationToken cancellationToken) => Task.FromResult(Definitions); public Task> GetRecentRunsAsync(string? kind, int limit, CancellationToken cancellationToken) { IEnumerable query = RecentRuns; if (!string.IsNullOrWhiteSpace(kind)) { query = query.Where(run => string.Equals(run.Kind, kind, StringComparison.Ordinal)); } return Task.FromResult>(query.Take(limit).ToArray()); } public Task> GetActiveRunsAsync(CancellationToken cancellationToken) => Task.FromResult(ActiveRuns); public Task GetRunAsync(Guid runId, CancellationToken cancellationToken) => Task.FromResult(Runs.TryGetValue(runId, out var run) ? run : null); public Task GetLastRunAsync(string kind, CancellationToken cancellationToken) => Task.FromResult(LastRuns.TryGetValue(kind, out var run) ? run : null); public Task> GetLastRunsAsync(IEnumerable kinds, CancellationToken cancellationToken) { var map = new Dictionary(StringComparer.Ordinal); foreach (var kind in kinds) { if (kind is null) { continue; } if (LastRuns.TryGetValue(kind, out var run) && run is not null) { map[kind] = run; } } return Task.FromResult>(map); } } }