Files
git.stella-ops.org/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/WebServiceEndpointsTests.cs
2026-01-28 02:30:48 +02:00

3428 lines
153 KiB
C#

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()
{
// Reset shared in-memory database state before each test
InMemoryClient.ResetSharedState();
_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();
// Clear shared state after test completes
InMemoryClient.ResetSharedState();
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<HealthPayload>();
Assert.NotNull(healthPayload);
Assert.Equal("healthy", healthPayload!.Status);
Assert.Equal("postgres", healthPayload.Storage.Backend);
var readyPayload = await readyResponse.Content.ReadFromJsonAsync<ReadyPayload>();
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<JsonDocument>();
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();
// Real query service computes confidence based on data consistency between observations.
// Since the two observations have different purls/cpes, confidence will be < 1.0
Assert.InRange(confidence, 0.0, 1.0);
var conflicts = linkset.GetProperty("conflicts").EnumerateArray().ToArray();
// Real query service detects conflicts between observations with differing linkset data
// (conflicts are expected when observations have different purls/cpes for same alias)
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<JsonDocument>();
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<JsonDocument>();
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<JsonDocument>();
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<AdvisoryLinksetQueryResponse>();
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<AdvisoryLinksetQueryResponse>();
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<AdvisoryLinksetQueryResponse>();
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<JsonElement>();
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<JsonElement>();
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&section=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<AdvisoryIngestResponse>();
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<AdvisoryIngestResponse>();
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<AdvisoryRawRecordResponse>();
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<AdvisoryRawListResponse>();
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<AocVerifyResponse>();
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<AocVerifyResponse>();
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<AdvisoryRawListResponse>();
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<AdvisoryRawListResponse>();
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<AdvisoryEvidenceResponse>();
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<AdvisoryEvidenceResponse>();
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<IOptions<ConcelierOptions>>().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<AttestationClaims>(
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<EvidenceBatchResponse>();
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<AdvisoryEvidenceResponse>();
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<JsonDocument>())
{
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<string, string?>
{
["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<IAuthenticationSchemeProvider>().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<string, string?>
{
["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<ProblemDocument>();
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<ProblemDocument>();
Assert.NotNull(problem);
Assert.Equal("https://stellaops.org/problems/not-found", problem!.Type);
}
[Fact]
public async Task JobTriggerMapsCoordinatorOutcomes()
{
var handler = _factory.Services.GetRequiredService<StubJobCoordinator>();
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<ProblemDocument>();
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<string, object?>()));
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<JobRunPayload>();
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<string, object?>()), "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<ProblemDocument>();
Assert.NotNull(failureProblem);
Assert.Equal("https://stellaops.org/problems/job-failure", failureProblem!.Type);
}
[Fact]
public async Task JobsEndpointsExposeJobData()
{
var handler = _factory.Services.GetRequiredService<StubJobCoordinator>();
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<string, object?> { ["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<JobRunSnapshot>();
handler.Runs[run.RunId] = run;
try
{
using var client = _factory.CreateClient();
var definitions = await client.GetFromJsonAsync<List<JobDefinitionPayload>>("/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<JobRunPayload>($"/jobs/{run.RunId}");
Assert.NotNull(runPayload);
Assert.Equal(run.RunId, runPayload!.RunId);
Assert.Equal("Succeeded", runPayload.Status);
var runs = await client.GetFromJsonAsync<List<JobRunPayload>>("/jobs?kind=demo&limit=5");
Assert.NotNull(runs);
Assert.Single(runs!);
Assert.Equal(run.RunId, runs![0].RunId);
var runsByDefinition = await client.GetFromJsonAsync<List<JobRunPayload>>("/jobs/definitions/demo/runs");
Assert.NotNull(runsByDefinition);
Assert.Single(runsByDefinition!);
var active = await client.GetFromJsonAsync<List<JobRunPayload>>("/jobs/active");
Assert.NotNull(active);
Assert.Empty(active!);
}
finally
{
handler.Definitions = Array.Empty<JobDefinition>();
handler.RecentRuns = Array.Empty<JobRunSnapshot>();
handler.ActiveRuns = Array.Empty<JobRunSnapshot>();
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<AdvisoryReference>(),
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: Array.Empty<AdvisoryProvenance>());
var statementId = Guid.NewGuid();
using (var scope = _factory.Services.CreateScope())
{
var eventLog = scope.ServiceProvider.GetRequiredService<IAdvisoryEventLog>();
var appendRequest = new AdvisoryEventAppendRequest(new[]
{
new AdvisoryStatementInput(
vulnerabilityKey,
advisory,
advisory.Modified ?? advisory.Published ?? DateTimeOffset.UtcNow,
Array.Empty<Guid>(),
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<ReplayResponse>();
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<IAdvisoryEventLog>();
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<AdvisoryReference>(),
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: Array.Empty<AdvisoryProvenance>());
var statementInput = new AdvisoryStatementInput(
vulnerabilityKey,
advisory,
recordedAt,
Array.Empty<Guid>(),
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<AdvisoryStatementInput>(), 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<ReplayResponse>();
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<string, string?>
{
["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<string, string?>
{
["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<string, string?>
{
["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<AdvisoryMergeService>());
#pragma warning restore CS0618, CONCELIER0001, CONCELIER0002
var schedulerOptions = provider.GetRequiredService<IOptions<JobSchedulerOptions>>().Value;
Assert.DoesNotContain("merge:reconcile", schedulerOptions.Definitions.Keys);
}
[Fact]
public void MergeModuleReenabledWhenFeatureFlagCleared()
{
var environment = new Dictionary<string, string?>
{
["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<AdvisoryMergeService>());
#pragma warning restore CS0618, CONCELIER0001, CONCELIER0002
var schedulerOptions = provider.GetRequiredService<IOptions<JobSchedulerOptions>>().Value;
Assert.Contains("merge:reconcile", schedulerOptions.Definitions.Keys);
}
[Fact]
public void MergeJobRemovedWhenAllowlistExcludes()
{
var environment = new Dictionary<string, string?>
{
["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<AdvisoryMergeService>());
#pragma warning restore CS0618, CONCELIER0001, CONCELIER0002
var schedulerOptions = provider.GetRequiredService<IOptions<JobSchedulerOptions>>().Value;
Assert.DoesNotContain("merge:reconcile", schedulerOptions.Definitions.Keys);
}
[Fact]
public void MergeJobRemainsWhenAllowlisted()
{
var environment = new Dictionary<string, string?>
{
["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<AdvisoryMergeService>());
#pragma warning restore CS0618, CONCELIER0001, CONCELIER0002
var schedulerOptions = provider.GetRequiredService<IOptions<JobSchedulerOptions>>().Value;
Assert.Contains("merge:reconcile", schedulerOptions.Definitions.Keys);
}
[Fact]
public async Task JobsEndpointsAllowBypassWhenAuthorityEnabled()
{
var environment = new Dictionary<string, string?>
{
["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<StubJobCoordinator>();
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<string, string?>
{
["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<IOptions<ConcelierOptions>>().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<string, string?>
{
["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<IOptionsMonitor<StellaOpsAuthClientOptions>>();
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<AdvisoryObservationDocument> documents)
{
var client = new InMemoryClient(_runner.ConnectionString);
var database = client.GetDatabase(StorageDefaults.DefaultDatabaseName);
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.
}
// Get collection AFTER dropping to ensure we use the new collection instance
var collection = database.GetCollection<AdvisoryObservationDocument>(StorageDefaults.Collections.AdvisoryObservations);
var snapshot = documents?.ToArray() ?? Array.Empty<AdvisoryObservationDocument>();
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<AdvisoryLinksetDocument> documents)
{
var client = new InMemoryClient(_runner.ConnectionString);
var database = client.GetDatabase(StorageDefaults.DefaultDatabaseName);
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.
}
// Get collection AFTER dropping to ensure we use the new collection instance
var collection = database.GetCollection<AdvisoryLinksetDocument>(StorageDefaults.Collections.AdvisoryLinksets);
var snapshot = documents?.ToArray() ?? Array.Empty<AdvisoryLinksetDocument>();
if (snapshot.Length > 0)
{
await collection.InsertManyAsync(snapshot);
}
}
private static AdvisoryLinksetDocument CreateLinksetDocument(
string tenant,
string source,
string advisoryId,
IEnumerable<string> observationIds,
IEnumerable<string> purls,
IEnumerable<string> 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<string>? aliases = null,
IEnumerable<string>? purls = null,
IEnumerable<string>? 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<string, string>(StringComparer.Ordinal)
},
Content = new AdvisoryObservationContentDocument
{
Format = "csaf",
SpecVersion = "2.0",
Raw = DocumentObject.Parse("""{"observation":"%ID%"}""".Replace("%ID%", id)),
Metadata = new Dictionary<string, string>(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<AdvisoryObservationReferenceDocument>()
: references
.Select(reference => new AdvisoryObservationReferenceDocument
{
Type = reference.Type,
Url = reference.Url
})
.ToList()
},
Attributes = new Dictionary<string, string>(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<ReplayStatement> Statements,
List<ReplayConflict>? Conflicts);
private sealed record ReplayStatement(
Guid StatementId,
string VulnerabilityKey,
string AdvisoryKey,
Advisory Advisory,
string StatementHash,
DateTimeOffset AsOf,
DateTimeOffset RecordedAt,
IReadOnlyList<Guid> InputDocumentIds);
private sealed record ReplayConflict(
Guid ConflictId,
string VulnerabilityKey,
IReadOnlyList<Guid> StatementIds,
string ConflictHash,
DateTimeOffset AsOf,
DateTimeOffset RecordedAt,
string Details,
MergeConflictExplainerPayload Explainer);
private sealed class ConcelierApplicationFactory : WebApplicationFactory<Program>
{
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<ConcelierOptions.AuthorityOptions>? _authorityConfigure;
private readonly IDictionary<string, string?> _additionalPreviousEnvironment = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
public CollectingLoggerProvider LoggerProvider { get; } = new();
public ConcelierApplicationFactory(
string connectionString,
Action<ConcelierOptions.AuthorityOptions>? authorityConfigure = null,
IDictionary<string, string?>? 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<string, string?>
{
["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<ConcelierDataSource>();
services.AddSingleton<ILeaseStore, Fixtures.TestLeaseStore>();
services.AddSingleton<StubJobCoordinator>();
services.AddSingleton<IJobCoordinator>(sp => sp.GetRequiredService<StubJobCoordinator>());
// Register in-memory lookups that query the shared in-memory database
services.RemoveAll<IAdvisoryRawService>();
services.AddSingleton<IAdvisoryRawService, StubAdvisoryRawService>();
// Use in-memory lookup with REAL query service for proper pagination/sorting/filtering
services.RemoveAll<IAdvisoryObservationLookup>();
services.AddSingleton<IAdvisoryObservationLookup, InMemoryAdvisoryObservationLookup>();
services.RemoveAll<IAdvisoryObservationQueryService>();
services.AddSingleton<IAdvisoryObservationQueryService, AdvisoryObservationQueryService>();
// Register stubs for storage and event log services
services.RemoveAll<IStorageDatabase>();
services.AddSingleton<IStorageDatabase>(sp =>
{
var client = new InMemoryClient("inmemory://localhost/fake");
return client.GetDatabase(StorageDefaults.DefaultDatabaseName);
});
services.RemoveAll<IAdvisoryStore>();
services.AddSingleton<IAdvisoryStore, StubAdvisoryStore>();
services.RemoveAll<IAdvisoryEventLog>();
services.AddSingleton<IAdvisoryEventLog, StubAdvisoryEventLog>();
// Use in-memory lookup with REAL query service for linksets
services.RemoveAll<IAdvisoryLinksetLookup>();
services.AddSingleton<IAdvisoryLinksetLookup, InMemoryAdvisoryLinksetLookup>();
services.RemoveAll<IAdvisoryLinksetQueryService>();
services.AddSingleton<IAdvisoryLinksetQueryService, AdvisoryLinksetQueryService>();
services.RemoveAll<IAdvisoryLinksetStore>();
services.AddSingleton<IAdvisoryLinksetStore, InMemoryAdvisoryLinksetStore>();
services.PostConfigure<ConcelierOptions>(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<IStartupFilter, RemoteIpStartupFilter>();
services.PostConfigure<JwtBearerOptions>(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<OpenIdConnectConfiguration>(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<IApplicationBuilder> Configure(Action<IApplicationBuilder> 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<KeyValuePair<string, object?>> 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<LogEntry> entries = new();
private bool disposed;
public ILogger CreateLogger(string categoryName) => new CollectingLogger(categoryName, this);
public IReadOnlyList<LogEntry> 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>(TState state) where TState : notnull => NullScope.Instance;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> 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<KeyValuePair<string, object?>> ExtractState<TState>(TState state)
{
if (state is IReadOnlyList<KeyValuePair<string, object?>> list)
{
return list;
}
if (state is IEnumerable<KeyValuePair<string, object?>> enumerable)
{
return enumerable.ToArray();
}
if (state is null)
{
return Array.Empty<KeyValuePair<string, object?>>();
}
return new[] { new KeyValuePair<string, object?>("State", state) };
}
}
private sealed class NullScope : IDisposable
{
public static readonly NullScope Instance = new();
public void Dispose()
{
}
}
}
private sealed class StubAdvisoryRawService : IAdvisoryRawService
{
public Task<AdvisoryRawUpsertResult> 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<AdvisoryRawRecord?> FindByIdAsync(string tenant, string id, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult<AdvisoryRawRecord?>(null);
}
public Task<AdvisoryRawQueryResult> QueryAsync(AdvisoryRawQueryOptions options, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new AdvisoryRawQueryResult(Array.Empty<AdvisoryRawRecord>(), null, false));
}
public Task<IReadOnlyList<AdvisoryRawRecord>> FindByAdvisoryKeyAsync(
string tenant,
string advisoryKey,
IReadOnlyCollection<string> sourceVendors,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult<IReadOnlyList<AdvisoryRawRecord>>(Array.Empty<AdvisoryRawRecord>());
}
public Task<AdvisoryRawVerificationResult> VerifyAsync(AdvisoryRawVerificationRequest request, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new AdvisoryRawVerificationResult(
request.Tenant,
request.Since,
request.Until,
0,
Array.Empty<AdvisoryRawVerificationViolation>(),
false));
}
}
/// <summary>
/// In-memory implementation of IAdvisoryObservationLookup that queries the shared in-memory database.
/// Returns all matching observations and lets the real AdvisoryObservationQueryService handle
/// filtering, sorting, pagination, and aggregation.
/// </summary>
private sealed class InMemoryAdvisoryObservationLookup : IAdvisoryObservationLookup
{
public async ValueTask<IReadOnlyList<AdvisoryObservation>> ListByTenantAsync(
string tenant,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var docs = await GetAllDocumentsAsync(cancellationToken);
return docs
.Where(d => string.Equals(d.Tenant, tenant, StringComparison.OrdinalIgnoreCase))
.Select(MapToObservation)
.ToList();
}
public async ValueTask<IReadOnlyList<AdvisoryObservation>> FindByFiltersAsync(
string tenant,
IReadOnlyCollection<string> observationIds,
IReadOnlyCollection<string> aliases,
IReadOnlyCollection<string> purls,
IReadOnlyCollection<string> cpes,
AdvisoryObservationCursor? cursor,
int limit,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var docs = await GetAllDocumentsAsync(cancellationToken);
// Filter by tenant
var observations = docs
.Where(d => string.Equals(d.Tenant, tenant, StringComparison.OrdinalIgnoreCase))
.Select(MapToObservation)
.ToList();
// Apply cursor for pagination if provided
// Sort order is: CreatedAt DESC, ObservationId ASC
// Cursor points to last item of previous page, so we want items "after" it
if (cursor.HasValue)
{
var cursorCreatedAt = cursor.Value.CreatedAt;
var cursorObsId = cursor.Value.ObservationId;
observations = observations
.Where(obs => IsBeyondCursor(obs, cursorCreatedAt, cursorObsId))
.ToList();
}
return observations;
}
private static bool IsBeyondCursor(AdvisoryObservation obs, DateTimeOffset cursorCreatedAt, string cursorObsId)
{
// For DESC CreatedAt, ASC ObservationId sorting:
// Return true if this observation should appear AFTER the cursor position
// "After" means: older (smaller CreatedAt), or same time but later in alpha order
if (obs.CreatedAt < cursorCreatedAt)
{
return true;
}
if (obs.CreatedAt == cursorCreatedAt &&
string.Compare(obs.ObservationId, cursorObsId, StringComparison.Ordinal) > 0)
{
return true;
}
return false;
}
private static async Task<List<AdvisoryObservationDocument>> GetAllDocumentsAsync(CancellationToken cancellationToken)
{
var client = new InMemoryClient("inmemory://localhost/fake");
var database = client.GetDatabase(StorageDefaults.DefaultDatabaseName);
var collection = database.GetCollection<AdvisoryObservationDocument>(StorageDefaults.Collections.AdvisoryObservations);
var cursor = await collection.FindAsync(FilterDefinition<AdvisoryObservationDocument>.Empty, null, cancellationToken);
var docs = new List<AdvisoryObservationDocument>();
while (await cursor.MoveNextAsync(cancellationToken))
{
docs.AddRange(cursor.Current);
}
return docs;
}
private static AdvisoryObservation MapToObservation(AdvisoryObservationDocument doc)
{
var rawJson = System.Text.Json.JsonSerializer.SerializeToNode(doc.Content.Raw) ?? System.Text.Json.Nodes.JsonNode.Parse("{}")!;
var linkset = new AdvisoryObservationLinkset(
doc.Linkset.Aliases,
doc.Linkset.Purls,
doc.Linkset.Cpes,
doc.Linkset.References?.Select(r => new AdvisoryObservationReference(r.Type, r.Url)));
var rawLinkset = new RawLinkset
{
Aliases = doc.Linkset.Aliases?.ToImmutableArray() ?? ImmutableArray<string>.Empty,
PackageUrls = doc.Linkset.Purls?.ToImmutableArray() ?? ImmutableArray<string>.Empty,
Cpes = doc.Linkset.Cpes?.ToImmutableArray() ?? ImmutableArray<string>.Empty
};
return new AdvisoryObservation(
doc.Id,
doc.Tenant,
new AdvisoryObservationSource(doc.Source.Vendor, doc.Source.Stream, doc.Source.Api),
new AdvisoryObservationUpstream(
doc.Upstream.UpstreamId,
doc.Upstream.DocumentVersion,
new DateTimeOffset(doc.Upstream.FetchedAt, TimeSpan.Zero),
new DateTimeOffset(doc.Upstream.ReceivedAt, TimeSpan.Zero),
doc.Upstream.ContentHash,
new AdvisoryObservationSignature(
doc.Upstream.Signature.Present,
doc.Upstream.Signature.Format,
doc.Upstream.Signature.KeyId,
doc.Upstream.Signature.Signature),
doc.Upstream.Metadata.ToImmutableDictionary()),
new AdvisoryObservationContent(
doc.Content.Format,
doc.Content.SpecVersion,
rawJson,
doc.Content.Metadata.ToImmutableDictionary()),
linkset,
rawLinkset,
new DateTimeOffset(doc.CreatedAt, TimeSpan.Zero),
doc.Attributes.ToImmutableDictionary());
}
}
private sealed class StubAdvisoryEventLog : IAdvisoryEventLog
{
private readonly ConcurrentDictionary<string, List<AdvisoryStatementInput>> _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<AdvisoryStatementInput>());
lock (list)
{
list.Add(statement);
}
}
return ValueTask.CompletedTask;
}
public ValueTask<AdvisoryReplay> 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<byte>.Empty,
s.AsOf,
DateTimeOffset.UtcNow,
System.Collections.Immutable.ImmutableArray<Guid>.Empty))
.ToImmutableArray();
return ValueTask.FromResult(new AdvisoryReplay(
vulnerabilityKey,
asOf,
snapshots,
System.Collections.Immutable.ImmutableArray<AdvisoryConflictSnapshot>.Empty));
}
return ValueTask.FromResult(new AdvisoryReplay(
vulnerabilityKey,
asOf,
System.Collections.Immutable.ImmutableArray<AdvisoryStatementSnapshot>.Empty,
System.Collections.Immutable.ImmutableArray<AdvisoryConflictSnapshot>.Empty));
}
public ValueTask AttachStatementProvenanceAsync(Guid statementId, DsseProvenance provenance, TrustInfo trust, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
}
private sealed class StubAdvisoryStore : IAdvisoryStore
{
private readonly ConcurrentDictionary<string, Advisory> _advisories = new(StringComparer.OrdinalIgnoreCase);
public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
_advisories[advisory.AdvisoryKey] = advisory;
return Task.CompletedTask;
}
public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
_advisories.TryGetValue(advisoryKey, out var advisory);
return Task.FromResult<Advisory?>(advisory);
}
public Task<IReadOnlyList<Advisory>> 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<IReadOnlyList<Advisory>>(result);
}
public async IAsyncEnumerable<Advisory> 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();
}
}
}
/// <summary>
/// In-memory implementation of IAdvisoryLinksetLookup that queries the shared in-memory database.
/// Performs filtering by tenant, advisoryIds, and sources, letting the real AdvisoryLinksetQueryService
/// handle sorting, pagination, and cursor encoding.
/// </summary>
private sealed class InMemoryAdvisoryLinksetLookup : IAdvisoryLinksetLookup
{
public async Task<IReadOnlyList<AdvisoryLinkset>> FindByTenantAsync(
string tenantId,
IEnumerable<string>? advisoryIds,
IEnumerable<string>? sources,
AdvisoryLinksetCursor? cursor,
int limit,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var client = new InMemoryClient("inmemory://localhost/fake");
var database = client.GetDatabase(StorageDefaults.DefaultDatabaseName);
var collection = database.GetCollection<AdvisoryLinksetDocument>(StorageDefaults.Collections.AdvisoryLinksets);
var dbCursor = await collection.FindAsync(FilterDefinition<AdvisoryLinksetDocument>.Empty, null, cancellationToken);
var docs = new List<AdvisoryLinksetDocument>();
while (await dbCursor.MoveNextAsync(cancellationToken))
{
docs.AddRange(dbCursor.Current);
}
var advisoryIdsList = advisoryIds?.ToList();
var sourcesList = sources?.ToList();
// Filter by tenant, advisoryIds, and sources
var filtered = docs
.Where(d => string.Equals(d.TenantId, tenantId, StringComparison.OrdinalIgnoreCase))
.Where(d => advisoryIdsList == null || !advisoryIdsList.Any() ||
advisoryIdsList.Any(id => string.Equals(d.AdvisoryId, id, StringComparison.OrdinalIgnoreCase)))
.Where(d => sourcesList == null || !sourcesList.Any() ||
sourcesList.Any(s => string.Equals(d.Source, s, StringComparison.OrdinalIgnoreCase)))
.Select(MapToLinkset)
.ToList();
// Apply cursor for pagination if provided
// Sort order is: CreatedAt DESC, AdvisoryId ASC
// Cursor points to last item of previous page, so we want items "after" it
if (cursor != null)
{
var cursorCreatedAt = cursor.CreatedAt;
var cursorAdvisoryId = cursor.AdvisoryId;
filtered = filtered
.Where(ls => IsBeyondLinksetCursor(ls, cursorCreatedAt, cursorAdvisoryId))
.ToList();
}
return filtered;
}
private static bool IsBeyondLinksetCursor(AdvisoryLinkset linkset, DateTimeOffset cursorCreatedAt, string cursorAdvisoryId)
{
// For DESC CreatedAt, ASC AdvisoryId sorting:
// Return true if this linkset should appear AFTER the cursor position
if (linkset.CreatedAt < cursorCreatedAt)
{
return true;
}
if (linkset.CreatedAt == cursorCreatedAt &&
string.Compare(linkset.AdvisoryId, cursorAdvisoryId, StringComparison.Ordinal) > 0)
{
return true;
}
return false;
}
private static AdvisoryLinkset MapToLinkset(AdvisoryLinksetDocument doc)
{
return new AdvisoryLinkset(
doc.TenantId,
doc.Source,
doc.AdvisoryId,
doc.Observations.ToImmutableArray(),
new AdvisoryLinksetNormalized(
doc.Normalized.Purls.ToList(),
null, // Cpes
doc.Normalized.Versions.ToList(),
null, // Ranges
null), // Severities
null, // Provenance
null, // Confidence
null, // Conflicts
new DateTimeOffset(doc.CreatedAt, TimeSpan.Zero),
null); // BuiltByJobId
}
}
private sealed class InMemoryAdvisoryLinksetStore : IAdvisoryLinksetStore
{
public async Task<IReadOnlyList<AdvisoryLinkset>> FindByTenantAsync(
string tenantId,
IEnumerable<string>? advisoryIds,
IEnumerable<string>? sources,
AdvisoryLinksetCursor? cursor,
int limit,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var client = new InMemoryClient("inmemory://localhost/fake");
var database = client.GetDatabase(StorageDefaults.DefaultDatabaseName);
var collection = database.GetCollection<AdvisoryLinksetDocument>(StorageDefaults.Collections.AdvisoryLinksets);
var dbCursor = await collection.FindAsync(FilterDefinition<AdvisoryLinksetDocument>.Empty, null, cancellationToken);
var docs = new List<AdvisoryLinksetDocument>();
while (await dbCursor.MoveNextAsync(cancellationToken))
{
docs.AddRange(dbCursor.Current);
}
var advisoryIdsList = advisoryIds?.ToList();
var sourcesList = sources?.ToList();
var filtered = docs
.Where(d => string.Equals(d.TenantId, tenantId, StringComparison.OrdinalIgnoreCase))
.Where(d => advisoryIdsList == null || !advisoryIdsList.Any() ||
advisoryIdsList.Any(id => string.Equals(d.AdvisoryId, id, StringComparison.OrdinalIgnoreCase)))
.Where(d => sourcesList == null || !sourcesList.Any() ||
sourcesList.Any(s => string.Equals(d.Source, s, StringComparison.OrdinalIgnoreCase)))
.OrderByDescending(d => d.CreatedAt)
.Take(limit)
.Select(MapToLinkset)
.ToList();
return filtered;
}
public Task UpsertAsync(AdvisoryLinkset linkset, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.CompletedTask;
}
private static AdvisoryLinkset MapToLinkset(AdvisoryLinksetDocument doc)
{
return new AdvisoryLinkset(
doc.TenantId,
doc.Source,
doc.AdvisoryId,
doc.Observations.ToImmutableArray(),
new AdvisoryLinksetNormalized(
doc.Normalized.Purls.ToList(),
null, // Cpes
doc.Normalized.Versions.ToList(),
null, // Ranges
null), // Severities
null, // Provenance
null, // Confidence
null, // Conflicts
new DateTimeOffset(doc.CreatedAt, TimeSpan.Zero),
null); // BuiltByJobId
}
}
}
[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<IAdvisoryEventLog>();
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<AdvisoryReference>(),
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: Array.Empty<AdvisoryProvenance>());
var statementInput = new AdvisoryStatementInput(
vulnerabilityKey,
advisory,
recordedAt,
InputDocumentIds: Array.Empty<Guid>(),
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<IStorageDatabase>();
var statements = database.GetCollection<DocumentObject>(StorageDefaults.Collections.AdvisoryStatements);
var stored = await statements
.Find(Builders<DocumentObject>.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<IStorageDatabase>();
var statements = database.GetCollection<DocumentObject>(StorageDefaults.Collections.AdvisoryStatements);
await statements.DeleteOneAsync(Builders<DocumentObject>.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<string, object?> 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<IStorageDatabase>();
await DropCollectionIfExistsAsync(database, StorageDefaults.Collections.Advisory);
await DropCollectionIfExistsAsync(database, StorageDefaults.Collections.Alias);
if (advisories.Length == 0)
{
return;
}
var store = scope.ServiceProvider.GetRequiredService<IAdvisoryStore>();
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<AffectedPackageStatus>(),
provenance: new[]
{
new AdvisoryProvenance(
"nvd",
"affected",
observationId,
recordedAt,
new[] { "/affectedPackages/0" })
},
normalizedVersions: Array.Empty<NormalizedVersionRule>());
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<DocumentObject>(StorageDefaults.Collections.AdvisoryRaw);
await collection.DeleteManyAsync(FilterDefinition<DocumentObject>.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<Program> factory)
{
using var client = factory.CreateClient();
}
private static AdvisoryIngestRequest BuildAdvisoryIngestRequest(
string? contentHash,
string upstreamId,
bool enforceContentHash = true,
IReadOnlyList<string>? purls = null,
IReadOnlyList<string>? 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<string>();
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<string, string> { ["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<AdvisoryLinksetRelationshipRequest>(),
Array.Empty<string>(),
Array.Empty<string>(),
references,
resolvedNotes,
new Dictionary<string, string> { ["note"] = "ingest-test" }));
}
private static JsonElement CreateJsonElement(string json)
{
using var document = JsonDocument.Parse(json);
return document.RootElement.Clone();
}
private static async Task<IReadOnlyList<MetricMeasurement>> CaptureMetricsAsync(string meterName, string instrumentName, Func<Task> action)
{
var map = await CaptureMetricsAsync(meterName, new[] { instrumentName }, action).ConfigureAwait(false);
return map.TryGetValue(instrumentName, out var measurements)
? measurements
: Array.Empty<MetricMeasurement>();
}
private static async Task<Dictionary<string, IReadOnlyList<MetricMeasurement>>> CaptureMetricsAsync(
string meterName,
IReadOnlyCollection<string> instrumentNames,
Func<Task> action)
{
var measurementMap = instrumentNames.ToDictionary(
name => name,
_ => new List<MetricMeasurement>(),
StringComparer.Ordinal);
var instrumentSet = new HashSet<string>(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<KeyValuePair<string, object?>> tags)
{
if (!measurementMap.TryGetValue(instrument.Name, out var list))
{
return;
}
var tagDictionary = new Dictionary<string, object?>(StringComparer.Ordinal);
foreach (var tag in tags)
{
tagDictionary[tag.Key] = tag.Value;
}
list.Add(new MetricMeasurement(instrument.Name, measurement, tagDictionary));
}
listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state)
=> RecordMeasurement(instrument, measurement, tags));
listener.SetMeasurementEventCallback<double>((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<MetricMeasurement>)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<string>();
var claims = new List<Claim>
{
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<string, object?> 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<JobDefinition> Definitions { get; set; } = Array.Empty<JobDefinition>();
public IReadOnlyList<JobRunSnapshot> RecentRuns { get; set; } = Array.Empty<JobRunSnapshot>();
public IReadOnlyList<JobRunSnapshot> ActiveRuns { get; set; } = Array.Empty<JobRunSnapshot>();
public Dictionary<Guid, JobRunSnapshot> Runs { get; } = new();
public Dictionary<string, JobRunSnapshot?> LastRuns { get; } = new(StringComparer.Ordinal);
public Task<JobTriggerResult> TriggerAsync(string kind, IReadOnlyDictionary<string, object?>? parameters, string trigger, CancellationToken cancellationToken)
=> Task.FromResult(NextResult);
public Task<IReadOnlyList<JobDefinition>> GetDefinitionsAsync(CancellationToken cancellationToken)
=> Task.FromResult(Definitions);
public Task<IReadOnlyList<JobRunSnapshot>> GetRecentRunsAsync(string? kind, int limit, CancellationToken cancellationToken)
{
IEnumerable<JobRunSnapshot> query = RecentRuns;
if (!string.IsNullOrWhiteSpace(kind))
{
query = query.Where(run => string.Equals(run.Kind, kind, StringComparison.Ordinal));
}
return Task.FromResult<IReadOnlyList<JobRunSnapshot>>(query.Take(limit).ToArray());
}
public Task<IReadOnlyList<JobRunSnapshot>> GetActiveRunsAsync(CancellationToken cancellationToken)
=> Task.FromResult(ActiveRuns);
public Task<JobRunSnapshot?> GetRunAsync(Guid runId, CancellationToken cancellationToken)
=> Task.FromResult(Runs.TryGetValue(runId, out var run) ? run : null);
public Task<JobRunSnapshot?> GetLastRunAsync(string kind, CancellationToken cancellationToken)
=> Task.FromResult(LastRuns.TryGetValue(kind, out var run) ? run : null);
public Task<IReadOnlyDictionary<string, JobRunSnapshot>> GetLastRunsAsync(IEnumerable<string> kinds, CancellationToken cancellationToken)
{
var map = new Dictionary<string, JobRunSnapshot>(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<IReadOnlyDictionary<string, JobRunSnapshot>>(map);
}
}
}