Implement ledger metrics for observability and add tests for Ruby packages endpoints
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added `LedgerMetrics` class to record write latency and total events for ledger operations. - Created comprehensive tests for Ruby packages endpoints, covering scenarios for missing inventory, successful retrieval, and identifier handling. - Introduced `TestSurfaceSecretsScope` for managing environment variables during tests. - Developed `ProvenanceMongoExtensions` for attaching DSSE provenance and trust information to event documents. - Implemented `EventProvenanceWriter` and `EventWriter` classes for managing event provenance in MongoDB. - Established MongoDB indexes for efficient querying of events based on provenance and trust. - Added models and JSON parsing logic for DSSE provenance and trust information.
This commit is contained in:
@@ -31,6 +31,7 @@ using StellaOps.Concelier.Core.Jobs;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Observations;
|
||||
using StellaOps.Concelier.Core.Raw;
|
||||
using StellaOps.Concelier.WebService.Jobs;
|
||||
@@ -265,42 +266,46 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
CreateAdvisoryRawDocument("tenant-a", "nvd", "tenant-a:chunk:newest", newerHash, newestRaw.DeepClone().AsBsonDocument),
|
||||
CreateAdvisoryRawDocument("tenant-a", "nvd", "tenant-a:chunk:older", olderHash, olderRaw.DeepClone().AsBsonDocument));
|
||||
|
||||
await SeedCanonicalAdvisoriesAsync(
|
||||
CreateStructuredAdvisory("CVE-2025-0001", "GHSA-2025-0001", "tenant-a:chunk:newest", newerCreatedAt));
|
||||
|
||||
using var client = _factory.CreateClient();
|
||||
var response = await client.GetAsync("/advisories/cve-2025-0001/chunks?tenant=tenant-a§ion=summary&format=csaf");
|
||||
var response = await client.GetAsync("/advisories/cve-2025-0001/chunks?tenant=tenant-a§ion=workaround");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var payload = await response.Content.ReadAsStringAsync();
|
||||
using var document = JsonDocument.Parse(payload);
|
||||
var root = document.RootElement;
|
||||
|
||||
Assert.Equal("cve-2025-0001", root.GetProperty("advisoryKey").GetString());
|
||||
Assert.Equal("CVE-2025-0001", root.GetProperty("advisoryKey").GetString());
|
||||
Assert.Equal(1, root.GetProperty("total").GetInt32());
|
||||
Assert.False(root.GetProperty("truncated").GetBoolean());
|
||||
|
||||
var chunk = Assert.Single(root.GetProperty("chunks").EnumerateArray());
|
||||
Assert.Equal("summary", chunk.GetProperty("section").GetString());
|
||||
Assert.Equal("summary.intro", chunk.GetProperty("paragraphId").GetString());
|
||||
var text = chunk.GetProperty("text").GetString();
|
||||
Assert.False(string.IsNullOrWhiteSpace(text));
|
||||
Assert.Contains("deterministic summary paragraph", text, StringComparison.OrdinalIgnoreCase);
|
||||
var entry = Assert.Single(root.GetProperty("entries").EnumerateArray());
|
||||
Assert.Equal("workaround", entry.GetProperty("type").GetString());
|
||||
Assert.Equal("tenant-a:chunk:newest", entry.GetProperty("documentId").GetString());
|
||||
Assert.Equal("/references/0", entry.GetProperty("fieldPath").GetString());
|
||||
Assert.False(string.IsNullOrWhiteSpace(entry.GetProperty("chunkId").GetString()));
|
||||
|
||||
var metadata = chunk.GetProperty("metadata");
|
||||
Assert.Equal("summary.intro", metadata.GetProperty("path").GetString());
|
||||
Assert.Equal("csaf", metadata.GetProperty("format").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 sources = root.GetProperty("sources").EnumerateArray().ToArray();
|
||||
Assert.Equal(2, sources.Length);
|
||||
Assert.Equal("tenant-a:chunk:newest", sources[0].GetProperty("observationId").GetString());
|
||||
Assert.Equal("tenant-a:chunk:older", sources[1].GetProperty("observationId").GetString());
|
||||
Assert.All(
|
||||
sources,
|
||||
source => Assert.True(string.Equals("csaf", source.GetProperty("format").GetString(), StringComparison.OrdinalIgnoreCase)));
|
||||
var provenance = entry.GetProperty("provenance");
|
||||
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");
|
||||
@@ -526,6 +531,12 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
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();
|
||||
|
||||
@@ -588,6 +599,12 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
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();
|
||||
|
||||
@@ -1936,6 +1953,111 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
[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<IMongoDatabase>();
|
||||
var statements = database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.AdvisoryStatements);
|
||||
|
||||
var stored = await statements
|
||||
.Find(Builders<BsonDocument>.Filter.Eq("_id", statementId.ToString()))
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
Assert.NotNull(stored);
|
||||
var dsse = stored!["provenance"].AsBsonDocument["dsse"].AsBsonDocument;
|
||||
Assert.Equal("sha256:feedface", dsse["envelopeDigest"].AsString);
|
||||
var trustDoc = stored["trust"].AsBsonDocument;
|
||||
Assert.True(trustDoc["verified"].AsBoolean);
|
||||
Assert.Equal("Authority@stella", trustDoc["verifier"].AsString);
|
||||
}
|
||||
finally
|
||||
{
|
||||
using var cleanupScope = _factory.Services.CreateScope();
|
||||
var database = cleanupScope.ServiceProvider.GetRequiredService<IMongoDatabase>();
|
||||
var statements = database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.AdvisoryStatements);
|
||||
await statements.DeleteOneAsync(Builders<BsonDocument>.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; }
|
||||
@@ -1978,6 +2100,121 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
|
||||
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<IMongoDatabase>();
|
||||
|
||||
await DropCollectionIfExistsAsync(database, MongoStorageDefaults.Collections.Advisory);
|
||||
await DropCollectionIfExistsAsync(database, MongoStorageDefaults.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(IMongoDatabase database, string collectionName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await database.DropCollectionAsync(collectionName);
|
||||
}
|
||||
catch (MongoCommandException 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 BsonDocument[] documents)
|
||||
{
|
||||
var client = new MongoClient(_runner.ConnectionString);
|
||||
|
||||
Reference in New Issue
Block a user