Resolve Concelier/Excititor merge conflicts

This commit is contained in:
root
2025-10-20 14:19:25 +03:00
2687 changed files with 212646 additions and 85913 deletions

View File

@@ -0,0 +1,122 @@
using System.Collections.Generic;
using Microsoft.Extensions.Logging.Abstractions;
using Mongo2Go;
using MongoDB.Bson;
using MongoDB.Driver;
namespace StellaOps.Excititor.Storage.Mongo.Tests;
public sealed class MongoVexCacheMaintenanceTests : IAsyncLifetime
{
private readonly MongoDbRunner _runner;
private readonly IMongoDatabase _database;
public MongoVexCacheMaintenanceTests()
{
_runner = MongoDbRunner.Start();
var client = new MongoClient(_runner.ConnectionString);
_database = client.GetDatabase("vex-cache-maintenance-tests");
VexMongoMappingRegistry.Register();
}
[Fact]
public async Task RemoveExpiredAsync_DeletesEntriesBeforeCutoff()
{
var collection = _database.GetCollection<VexCacheEntryRecord>(VexMongoCollectionNames.Cache);
var now = DateTime.UtcNow;
await collection.InsertManyAsync(new[]
{
new VexCacheEntryRecord
{
Id = "sig-1|json",
QuerySignature = "sig-1",
Format = "json",
ArtifactAlgorithm = "sha256",
ArtifactDigest = "deadbeef",
CreatedAt = now.AddHours(-2),
ExpiresAt = now.AddHours(-1),
},
new VexCacheEntryRecord
{
Id = "sig-2|json",
QuerySignature = "sig-2",
Format = "json",
ArtifactAlgorithm = "sha256",
ArtifactDigest = "cafebabe",
CreatedAt = now,
ExpiresAt = now.AddHours(1),
},
});
var maintenance = new MongoVexCacheMaintenance(_database, NullLogger<MongoVexCacheMaintenance>.Instance);
var removed = await maintenance.RemoveExpiredAsync(DateTimeOffset.UtcNow, CancellationToken.None);
Assert.Equal(1, removed);
var remaining = await collection.CountDocumentsAsync(FilterDefinition<VexCacheEntryRecord>.Empty);
Assert.Equal(1, remaining);
}
[Fact]
public async Task RemoveMissingManifestReferencesAsync_DropsDanglingEntries()
{
var cache = _database.GetCollection<VexCacheEntryRecord>(VexMongoCollectionNames.Cache);
var exports = _database.GetCollection<VexExportManifestRecord>(VexMongoCollectionNames.Exports);
await exports.InsertOneAsync(new VexExportManifestRecord
{
Id = "manifest-existing",
QuerySignature = "sig-keep",
Format = "json",
CreatedAt = DateTime.UtcNow,
ArtifactAlgorithm = "sha256",
ArtifactDigest = "keep",
ClaimCount = 1,
SourceProviders = new List<string> { "vendor" },
});
await cache.InsertManyAsync(new[]
{
new VexCacheEntryRecord
{
Id = "sig-remove|json",
QuerySignature = "sig-remove",
Format = "json",
ArtifactAlgorithm = "sha256",
ArtifactDigest = "drop",
CreatedAt = DateTime.UtcNow,
ManifestId = "manifest-missing",
},
new VexCacheEntryRecord
{
Id = "sig-keep|json",
QuerySignature = "sig-keep",
Format = "json",
ArtifactAlgorithm = "sha256",
ArtifactDigest = "keep",
CreatedAt = DateTime.UtcNow,
ManifestId = "manifest-existing",
},
});
var maintenance = new MongoVexCacheMaintenance(_database, NullLogger<MongoVexCacheMaintenance>.Instance);
var removed = await maintenance.RemoveMissingManifestReferencesAsync(CancellationToken.None);
Assert.Equal(1, removed);
var remainingIds = await cache.Find(Builders<VexCacheEntryRecord>.Filter.Empty)
.Project(x => x.Id)
.ToListAsync();
Assert.Single(remainingIds);
Assert.Contains("sig-keep|json", remainingIds);
}
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync()
{
_runner.Dispose();
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,282 @@
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text;
using Microsoft.Extensions.Options;
using Mongo2Go;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Excititor.Core;
namespace StellaOps.Excititor.Storage.Mongo.Tests;
public sealed class MongoVexRepositoryTests : IAsyncLifetime
{
private readonly MongoDbRunner _runner;
private readonly MongoClient _client;
public MongoVexRepositoryTests()
{
_runner = MongoDbRunner.Start();
_client = new MongoClient(_runner.ConnectionString);
}
[Fact]
public async Task RawStore_UsesGridFsForLargePayloads()
{
var database = _client.GetDatabase($"vex-raw-gridfs-{Guid.NewGuid():N}");
var store = CreateRawStore(database, thresholdBytes: 32);
var payload = Encoding.UTF8.GetBytes(new string('A', 256));
var document = new VexRawDocument(
"red-hat",
VexDocumentFormat.Csaf,
new Uri("https://example.com/redhat/csaf.json"),
DateTimeOffset.UtcNow,
"sha256:large",
payload,
ImmutableDictionary<string, string>.Empty);
await store.StoreAsync(document, CancellationToken.None);
var rawCollection = database.GetCollection<BsonDocument>(VexMongoCollectionNames.Raw);
var stored = await rawCollection.Find(Builders<BsonDocument>.Filter.Eq("_id", document.Digest))
.FirstOrDefaultAsync();
Assert.NotNull(stored);
Assert.True(stored!.TryGetValue("GridFsObjectId", out var gridId));
Assert.False(gridId.IsBsonNull);
Assert.Empty(stored["Content"].AsBsonBinaryData.Bytes);
var filesCollection = database.GetCollection<BsonDocument>("vex.raw.files");
var fileCount = await filesCollection.CountDocumentsAsync(FilterDefinition<BsonDocument>.Empty);
Assert.Equal(1, fileCount);
var fetched = await store.FindByDigestAsync(document.Digest, CancellationToken.None);
Assert.NotNull(fetched);
Assert.Equal(payload, fetched!.Content.ToArray());
}
[Fact]
public async Task RawStore_ReplacesGridFsWithInlinePayload()
{
var database = _client.GetDatabase($"vex-raw-inline-{Guid.NewGuid():N}");
var store = CreateRawStore(database, thresholdBytes: 16);
var largePayload = Encoding.UTF8.GetBytes(new string('B', 128));
var digest = "sha256:inline";
var largeDocument = new VexRawDocument(
"cisco",
VexDocumentFormat.CycloneDx,
new Uri("https://example.com/cyclonedx.json"),
DateTimeOffset.UtcNow,
digest,
largePayload,
ImmutableDictionary<string, string>.Empty);
await store.StoreAsync(largeDocument, CancellationToken.None);
var smallDocument = largeDocument with
{
RetrievedAt = DateTimeOffset.UtcNow.AddMinutes(1),
Content = Encoding.UTF8.GetBytes("small"),
};
await store.StoreAsync(smallDocument, CancellationToken.None);
var rawCollection = database.GetCollection<BsonDocument>(VexMongoCollectionNames.Raw);
var stored = await rawCollection.Find(Builders<BsonDocument>.Filter.Eq("_id", digest))
.FirstOrDefaultAsync();
Assert.NotNull(stored);
Assert.True(stored!.TryGetValue("GridFsObjectId", out var gridId));
Assert.True(gridId.IsBsonNull);
Assert.Equal("small", Encoding.UTF8.GetString(stored["Content"].AsBsonBinaryData.Bytes));
var filesCollection = database.GetCollection<BsonDocument>("vex.raw.files");
var fileCount = await filesCollection.CountDocumentsAsync(FilterDefinition<BsonDocument>.Empty);
Assert.Equal(0, fileCount);
}
[Fact]
public async Task ExportStore_SavesManifestAndCacheTransactionally()
{
var database = _client.GetDatabase($"vex-export-save-{Guid.NewGuid():N}");
var options = Options.Create(new VexMongoStorageOptions
{
ExportCacheTtl = TimeSpan.FromHours(6),
GridFsInlineThresholdBytes = 64,
});
var sessionProvider = new VexMongoSessionProvider(_client, options);
var store = new MongoVexExportStore(_client, database, options, sessionProvider);
var signature = new VexQuerySignature("format=csaf|provider=redhat");
var manifest = new VexExportManifest(
"exports/20251016/redhat",
signature,
VexExportFormat.Csaf,
DateTimeOffset.UtcNow,
new VexContentAddress("sha256", "abcdef123456"),
claimCount: 5,
sourceProviders: new[] { "red-hat" },
fromCache: false,
consensusRevision: "rev-1",
attestation: null,
sizeBytes: 1024);
await store.SaveAsync(manifest, CancellationToken.None);
var exportsCollection = database.GetCollection<BsonDocument>(VexMongoCollectionNames.Exports);
var exportKey = BuildExportKey(signature, VexExportFormat.Csaf);
var exportDoc = await exportsCollection.Find(Builders<BsonDocument>.Filter.Eq("_id", exportKey))
.FirstOrDefaultAsync();
Assert.NotNull(exportDoc);
var cacheCollection = database.GetCollection<BsonDocument>(VexMongoCollectionNames.Cache);
var cacheKey = BuildExportKey(signature, VexExportFormat.Csaf);
var cacheDoc = await cacheCollection.Find(Builders<BsonDocument>.Filter.Eq("_id", cacheKey))
.FirstOrDefaultAsync();
Assert.NotNull(cacheDoc);
Assert.Equal(manifest.ExportId, cacheDoc!["ManifestId"].AsString);
Assert.True(cacheDoc.TryGetValue("ExpiresAt", out var expiresValue));
Assert.False(expiresValue.IsBsonNull);
}
[Fact]
public async Task ExportStore_FindAsync_ExpiresCacheEntries()
{
var database = _client.GetDatabase($"vex-export-expire-{Guid.NewGuid():N}");
var options = Options.Create(new VexMongoStorageOptions
{
ExportCacheTtl = TimeSpan.FromMinutes(5),
GridFsInlineThresholdBytes = 64,
});
var sessionProvider = new VexMongoSessionProvider(_client, options);
var store = new MongoVexExportStore(_client, database, options, sessionProvider);
var signature = new VexQuerySignature("format=json|provider=cisco");
var manifest = new VexExportManifest(
"exports/20251016/cisco",
signature,
VexExportFormat.Json,
DateTimeOffset.UtcNow,
new VexContentAddress("sha256", "deadbeef"),
claimCount: 3,
sourceProviders: new[] { "cisco" },
fromCache: false,
consensusRevision: "rev-2",
attestation: null,
sizeBytes: 2048);
await store.SaveAsync(manifest, CancellationToken.None);
var cacheCollection = database.GetCollection<BsonDocument>(VexMongoCollectionNames.Cache);
var cacheId = BuildExportKey(signature, VexExportFormat.Json);
var update = Builders<BsonDocument>.Update.Set("ExpiresAt", DateTime.UtcNow.AddMinutes(-10));
await cacheCollection.UpdateOneAsync(Builders<BsonDocument>.Filter.Eq("_id", cacheId), update);
var cached = await store.FindAsync(signature, VexExportFormat.Json, CancellationToken.None);
Assert.Null(cached);
var remaining = await cacheCollection.Find(Builders<BsonDocument>.Filter.Eq("_id", cacheId))
.FirstOrDefaultAsync();
Assert.Null(remaining);
}
[Fact]
public async Task ClaimStore_AppendsAndQueriesStatements()
{
var database = _client.GetDatabase($"vex-claims-{Guid.NewGuid():N}");
var store = new MongoVexClaimStore(database);
var product = new VexProduct("pkg:demo/app", "Demo App", version: "1.0.0", purl: "pkg:demo/app@1.0.0");
var document = new VexClaimDocument(
VexDocumentFormat.Csaf,
"sha256:claim-1",
new Uri("https://example.org/vex/claim-1.json"),
revision: "2025-10-19");
var initialClaim = new VexClaim(
vulnerabilityId: "CVE-2025-0101",
providerId: "redhat",
product: product,
status: VexClaimStatus.NotAffected,
document: document,
firstSeen: DateTimeOffset.UtcNow.AddMinutes(-30),
lastSeen: DateTimeOffset.UtcNow.AddMinutes(-10),
justification: VexJustification.ComponentNotPresent,
detail: "Package not shipped in this channel.",
confidence: new VexConfidence("high", 0.9, "policy/default"),
signals: new VexSignalSnapshot(
new VexSeveritySignal("CVSS:3.1", 5.8, "medium", "CVSS:3.1/..."),
kev: false,
epss: 0.21),
additionalMetadata: ImmutableDictionary<string, string>.Empty.Add("source", "csaf"));
await store.AppendAsync(new[] { initialClaim }, DateTimeOffset.UtcNow.AddMinutes(-5), CancellationToken.None);
var secondDocument = new VexClaimDocument(
VexDocumentFormat.Csaf,
"sha256:claim-2",
new Uri("https://example.org/vex/claim-2.json"),
revision: "2025-10-19.1");
var secondClaim = new VexClaim(
vulnerabilityId: initialClaim.VulnerabilityId,
providerId: initialClaim.ProviderId,
product: initialClaim.Product,
status: initialClaim.Status,
document: secondDocument,
firstSeen: initialClaim.FirstSeen,
lastSeen: DateTimeOffset.UtcNow,
justification: initialClaim.Justification,
detail: initialClaim.Detail,
confidence: initialClaim.Confidence,
signals: new VexSignalSnapshot(
new VexSeveritySignal("CVSS:3.1", 7.2, "high"),
kev: true,
epss: 0.43),
additionalMetadata: initialClaim.AdditionalMetadata.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value));
await store.AppendAsync(new[] { secondClaim }, DateTimeOffset.UtcNow, CancellationToken.None);
var all = await store.FindAsync("CVE-2025-0101", product.Key, since: null, CancellationToken.None);
var allList = all.ToList();
Assert.Equal(2, allList.Count);
Assert.Equal("sha256:claim-2", allList[0].Document.Digest);
Assert.True(allList[0].Signals?.Kev);
Assert.Equal(0.43, allList[0].Signals?.Epss);
Assert.Equal("sha256:claim-1", allList[1].Document.Digest);
Assert.Equal("csaf", allList[1].AdditionalMetadata["source"]);
var recentOnly = await store.FindAsync("CVE-2025-0101", product.Key, DateTimeOffset.UtcNow.AddMinutes(-2), CancellationToken.None);
var recentList = recentOnly.ToList();
Assert.Single(recentList);
Assert.Equal("sha256:claim-2", recentList[0].Document.Digest);
}
private MongoVexRawStore CreateRawStore(IMongoDatabase database, int thresholdBytes)
{
var options = Options.Create(new VexMongoStorageOptions
{
RawBucketName = "vex.raw",
GridFsInlineThresholdBytes = thresholdBytes,
ExportCacheTtl = TimeSpan.FromHours(1),
});
var sessionProvider = new VexMongoSessionProvider(_client, options);
return new MongoVexRawStore(_client, database, options, sessionProvider);
}
private static string BuildExportKey(VexQuerySignature signature, VexExportFormat format)
=> string.Format(CultureInfo.InvariantCulture, "{0}|{1}", signature.Value, format.ToString().ToLowerInvariant());
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync()
{
_runner.Dispose();
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,184 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Mongo2Go;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Excititor.Core;
namespace StellaOps.Excititor.Storage.Mongo.Tests;
public sealed class MongoVexSessionConsistencyTests : IAsyncLifetime
{
private readonly MongoDbRunner _runner;
public MongoVexSessionConsistencyTests()
{
_runner = MongoDbRunner.Start(singleNodeReplSet: true);
}
[Fact]
public async Task SessionProvidesReadYourWrites()
{
await using var provider = BuildServiceProvider();
await using var scope = provider.CreateAsyncScope();
var sessionProvider = scope.ServiceProvider.GetRequiredService<IVexMongoSessionProvider>();
var providerStore = scope.ServiceProvider.GetRequiredService<IVexProviderStore>();
var session = await sessionProvider.StartSessionAsync();
var descriptor = new VexProvider("red-hat", "Red Hat", VexProviderKind.Vendor);
await providerStore.SaveAsync(descriptor, CancellationToken.None, session);
var fetched = await providerStore.FindAsync(descriptor.Id, CancellationToken.None, session);
Assert.NotNull(fetched);
Assert.Equal(descriptor.DisplayName, fetched!.DisplayName);
}
[Fact]
public async Task SessionMaintainsMonotonicReadsAcrossStepDown()
{
await using var provider = BuildServiceProvider();
await using var scope = provider.CreateAsyncScope();
var client = scope.ServiceProvider.GetRequiredService<IMongoClient>();
var sessionProvider = scope.ServiceProvider.GetRequiredService<IVexMongoSessionProvider>();
var providerStore = scope.ServiceProvider.GetRequiredService<IVexProviderStore>();
var session = await sessionProvider.StartSessionAsync();
var initial = new VexProvider("cisco", "Cisco", VexProviderKind.Vendor);
await providerStore.SaveAsync(initial, CancellationToken.None, session);
var baseline = await providerStore.FindAsync(initial.Id, CancellationToken.None, session);
Assert.Equal("Cisco", baseline!.DisplayName);
await ForcePrimaryStepDownAsync(client, CancellationToken.None);
await WaitForPrimaryAsync(client, CancellationToken.None);
await ExecuteWithRetryAsync(async () =>
{
var updated = new VexProvider(initial.Id, "Cisco Systems", initial.Kind);
await providerStore.SaveAsync(updated, CancellationToken.None, session);
}, CancellationToken.None);
var afterFailover = await providerStore.FindAsync(initial.Id, CancellationToken.None, session);
Assert.Equal("Cisco Systems", afterFailover!.DisplayName);
var subsequent = await providerStore.FindAsync(initial.Id, CancellationToken.None, session);
Assert.Equal("Cisco Systems", subsequent!.DisplayName);
}
private ServiceProvider BuildServiceProvider()
{
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddDebug());
services.Configure<VexMongoStorageOptions>(options =>
{
options.ConnectionString = _runner.ConnectionString;
options.DatabaseName = $"excititor-session-tests-{Guid.NewGuid():N}";
options.CommandTimeout = TimeSpan.FromSeconds(5);
options.RawBucketName = "vex.raw";
});
services.AddExcititorMongoStorage();
return services.BuildServiceProvider();
}
private static async Task ExecuteWithRetryAsync(Func<Task> action, CancellationToken cancellationToken)
{
const int maxAttempts = 10;
var attempt = 0;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
await action();
return;
}
catch (MongoException ex) when (IsStepDownTransient(ex) && attempt++ < maxAttempts)
{
await Task.Delay(TimeSpan.FromMilliseconds(200), cancellationToken);
}
}
}
private static bool IsStepDownTransient(MongoException ex)
{
if (ex is MongoConnectionException)
{
return true;
}
if (ex is MongoCommandException command)
{
return command.Code is 7 or 89 or 91 or 10107 or 11600
|| string.Equals(command.CodeName, "NotPrimaryNoSecondaryOk", StringComparison.OrdinalIgnoreCase)
|| string.Equals(command.CodeName, "NotWritablePrimary", StringComparison.OrdinalIgnoreCase)
|| string.Equals(command.CodeName, "PrimarySteppedDown", StringComparison.OrdinalIgnoreCase)
|| string.Equals(command.CodeName, "NotPrimary", StringComparison.OrdinalIgnoreCase);
}
return false;
}
private static async Task ForcePrimaryStepDownAsync(IMongoClient client, CancellationToken cancellationToken)
{
var admin = client.GetDatabase("admin");
var command = new BsonDocument
{
{ "replSetStepDown", 1 },
{ "force", true },
};
try
{
await admin.RunCommandAsync<BsonDocument>(command, cancellationToken: cancellationToken);
}
catch (MongoException ex) when (IsStepDownTransient(ex))
{
// Expected when the primary closes connections during the step-down sequence.
}
}
private static async Task WaitForPrimaryAsync(IMongoClient client, CancellationToken cancellationToken)
{
var admin = client.GetDatabase("admin");
var helloCommand = new BsonDocument("hello", 1);
for (var attempt = 0; attempt < 40; attempt++)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var result = await admin.RunCommandAsync<BsonDocument>(helloCommand, cancellationToken: cancellationToken);
if (result.TryGetValue("isWritablePrimary", out var value) && value.IsBoolean && value.AsBoolean)
{
return;
}
}
catch (MongoException ex) when (IsStepDownTransient(ex))
{
// Primary still recovering, retry.
}
await Task.Delay(TimeSpan.FromMilliseconds(200), cancellationToken);
}
throw new TimeoutException("Replica set primary did not recover in time.");
}
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync()
{
_runner.Dispose();
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,170 @@
using System;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Mongo2Go;
using StellaOps.Excititor.Core;
namespace StellaOps.Excititor.Storage.Mongo.Tests;
public sealed class MongoVexStatementBackfillServiceTests : IAsyncLifetime
{
private readonly MongoDbRunner _runner;
public MongoVexStatementBackfillServiceTests()
{
_runner = MongoDbRunner.Start(singleNodeReplSet: true);
}
[Fact]
public async Task RunAsync_BackfillsStatementsFromRawDocuments()
{
await using var provider = BuildServiceProvider();
await using var scope = provider.CreateAsyncScope();
var rawStore = scope.ServiceProvider.GetRequiredService<IVexRawStore>();
var claimStore = scope.ServiceProvider.GetRequiredService<IVexClaimStore>();
var backfill = scope.ServiceProvider.GetRequiredService<VexStatementBackfillService>();
var retrievedAt = DateTimeOffset.UtcNow.AddMinutes(-15);
var metadata = ImmutableDictionary<string, string>.Empty
.Add("vulnId", "CVE-2025-0001")
.Add("productKey", "pkg:test/app");
var document = new VexRawDocument(
"test-provider",
VexDocumentFormat.Csaf,
new Uri("https://example.test/vex.json"),
retrievedAt,
"sha256:test-doc",
ReadOnlyMemory<byte>.Empty,
metadata);
await rawStore.StoreAsync(document, CancellationToken.None);
var result = await backfill.RunAsync(new VexStatementBackfillRequest(), CancellationToken.None);
Assert.Equal(1, result.DocumentsEvaluated);
Assert.Equal(1, result.DocumentsBackfilled);
Assert.Equal(1, result.ClaimsWritten);
Assert.Equal(0, result.NormalizationFailures);
var claims = await claimStore.FindAsync("CVE-2025-0001", "pkg:test/app", since: null, CancellationToken.None);
var claim = Assert.Single(claims);
Assert.Equal(VexClaimStatus.NotAffected, claim.Status);
Assert.Equal("test-provider", claim.ProviderId);
Assert.Equal(retrievedAt.ToUnixTimeSeconds(), claim.FirstSeen.ToUnixTimeSeconds());
Assert.NotNull(claim.Signals);
Assert.Equal(0.2, claim.Signals!.Epss);
Assert.Equal("cvss", claim.Signals!.Severity?.Scheme);
}
[Fact]
public async Task RunAsync_SkipsExistingDocumentsUnlessForced()
{
await using var provider = BuildServiceProvider();
await using var scope = provider.CreateAsyncScope();
var rawStore = scope.ServiceProvider.GetRequiredService<IVexRawStore>();
var claimStore = scope.ServiceProvider.GetRequiredService<IVexClaimStore>();
var backfill = scope.ServiceProvider.GetRequiredService<VexStatementBackfillService>();
var metadata = ImmutableDictionary<string, string>.Empty
.Add("vulnId", "CVE-2025-0002")
.Add("productKey", "pkg:test/api");
var document = new VexRawDocument(
"test-provider",
VexDocumentFormat.Csaf,
new Uri("https://example.test/vex-2.json"),
DateTimeOffset.UtcNow.AddMinutes(-10),
"sha256:test-doc-2",
ReadOnlyMemory<byte>.Empty,
metadata);
await rawStore.StoreAsync(document, CancellationToken.None);
var first = await backfill.RunAsync(new VexStatementBackfillRequest(), CancellationToken.None);
Assert.Equal(1, first.DocumentsBackfilled);
var second = await backfill.RunAsync(new VexStatementBackfillRequest(), CancellationToken.None);
Assert.Equal(1, second.DocumentsEvaluated);
Assert.Equal(0, second.DocumentsBackfilled);
Assert.Equal(1, second.SkippedExisting);
var forced = await backfill.RunAsync(new VexStatementBackfillRequest(Force: true), CancellationToken.None);
Assert.Equal(1, forced.DocumentsBackfilled);
var claims = await claimStore.FindAsync("CVE-2025-0002", "pkg:test/api", since: null, CancellationToken.None);
Assert.Equal(2, claims.Count);
}
private ServiceProvider BuildServiceProvider()
{
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddDebug());
services.AddSingleton(TimeProvider.System);
services.Configure<VexMongoStorageOptions>(options =>
{
options.ConnectionString = _runner.ConnectionString;
options.DatabaseName = $"excititor-backfill-tests-{Guid.NewGuid():N}";
options.CommandTimeout = TimeSpan.FromSeconds(5);
options.RawBucketName = "vex.raw";
options.GridFsInlineThresholdBytes = 1024;
options.ExportCacheTtl = TimeSpan.FromHours(1);
});
services.AddExcititorMongoStorage();
services.AddSingleton<IVexNormalizer, TestNormalizer>();
return services.BuildServiceProvider();
}
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync()
{
_runner.Dispose();
return Task.CompletedTask;
}
private sealed class TestNormalizer : IVexNormalizer
{
public string Format => "csaf";
public bool CanHandle(VexRawDocument document) => true;
public ValueTask<VexClaimBatch> NormalizeAsync(VexRawDocument document, VexProvider provider, CancellationToken cancellationToken)
{
var productKey = document.Metadata.TryGetValue("productKey", out var value) ? value : "pkg:test/default";
var vulnId = document.Metadata.TryGetValue("vulnId", out var vuln) ? vuln : "CVE-TEST-0000";
var product = new VexProduct(productKey, "Test Product");
var claimDocument = new VexClaimDocument(
document.Format,
document.Digest,
document.SourceUri);
var timestamp = document.RetrievedAt == default ? DateTimeOffset.UtcNow : document.RetrievedAt;
var claim = new VexClaim(
vulnId,
provider.Id,
product,
VexClaimStatus.NotAffected,
claimDocument,
timestamp,
timestamp,
VexJustification.ComponentNotPresent,
detail: "backfill-test",
confidence: new VexConfidence("high", 0.95, "unit-test"),
signals: new VexSignalSnapshot(
new VexSeveritySignal("cvss", 5.4, "medium"),
kev: false,
epss: 0.2));
var claims = ImmutableArray.Create(claim);
return ValueTask.FromResult(new VexClaimBatch(document, claims, ImmutableDictionary<string, string>.Empty));
}
}
}

View File

@@ -0,0 +1,267 @@
using System.Globalization;
using Mongo2Go;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Excititor.Core;
namespace StellaOps.Excititor.Storage.Mongo.Tests;
public sealed class MongoVexStoreMappingTests : IAsyncLifetime
{
private readonly MongoDbRunner _runner;
private readonly IMongoDatabase _database;
public MongoVexStoreMappingTests()
{
_runner = MongoDbRunner.Start();
var client = new MongoClient(_runner.ConnectionString);
_database = client.GetDatabase("excititor-storage-mapping-tests");
VexMongoMappingRegistry.Register();
}
[Fact]
public async Task ProviderStore_RoundTrips_WithExtraFields()
{
var providers = _database.GetCollection<BsonDocument>(VexMongoCollectionNames.Providers);
var providerId = "red-hat";
var document = new BsonDocument
{
{ "_id", providerId },
{ "DisplayName", "Red Hat CSAF" },
{ "Kind", "vendor" },
{ "BaseUris", new BsonArray { "https://example.com/csaf" } },
{
"Discovery",
new BsonDocument
{
{ "WellKnownMetadata", "https://example.com/.well-known/csaf" },
{ "RolIeService", "https://example.com/service/rolie" },
{ "UnsupportedField", "ignored" },
}
},
{
"Trust",
new BsonDocument
{
{ "Weight", 0.75 },
{
"Cosign",
new BsonDocument
{
{ "Issuer", "issuer@example.com" },
{ "IdentityPattern", "spiffe://example/*" },
{ "Unexpected", true },
}
},
{ "PgpFingerprints", new BsonArray { "ABCDEF1234567890" } },
{ "AnotherIgnoredField", 123 },
}
},
{ "Enabled", true },
{ "UnexpectedRoot", new BsonDocument { { "flag", true } } },
};
await providers.InsertOneAsync(document);
var store = new MongoVexProviderStore(_database);
var result = await store.FindAsync(providerId, CancellationToken.None);
Assert.NotNull(result);
Assert.Equal(providerId, result!.Id);
Assert.Equal("Red Hat CSAF", result.DisplayName);
Assert.Equal(VexProviderKind.Vendor, result.Kind);
Assert.Single(result.BaseUris);
Assert.Equal("https://example.com/csaf", result.BaseUris[0].ToString());
Assert.Equal("https://example.com/.well-known/csaf", result.Discovery.WellKnownMetadata?.ToString());
Assert.Equal("https://example.com/service/rolie", result.Discovery.RolIeService?.ToString());
Assert.Equal(0.75, result.Trust.Weight);
Assert.NotNull(result.Trust.Cosign);
Assert.Equal("issuer@example.com", result.Trust.Cosign!.Issuer);
Assert.Equal("spiffe://example/*", result.Trust.Cosign!.IdentityPattern);
Assert.Contains("ABCDEF1234567890", result.Trust.PgpFingerprints);
Assert.True(result.Enabled);
}
[Fact]
public async Task ConsensusStore_IgnoresUnknownFields()
{
var consensus = _database.GetCollection<BsonDocument>(VexMongoCollectionNames.Consensus);
var vulnerabilityId = "CVE-2025-12345";
var productKey = "pkg:maven/org.example/app@1.2.3";
var consensusId = string.Format(CultureInfo.InvariantCulture, "{0}|{1}", vulnerabilityId.Trim(), productKey.Trim());
var document = new BsonDocument
{
{ "_id", consensusId },
{ "VulnerabilityId", vulnerabilityId },
{
"Product",
new BsonDocument
{
{ "Key", productKey },
{ "Name", "Example App" },
{ "Version", "1.2.3" },
{ "Purl", productKey },
{ "Extra", "ignored" },
}
},
{ "Status", "notaffected" },
{ "CalculatedAt", DateTime.UtcNow },
{
"Sources",
new BsonArray
{
new BsonDocument
{
{ "ProviderId", "red-hat" },
{ "Status", "notaffected" },
{ "DocumentDigest", "sha256:123" },
{ "Weight", 0.9 },
{ "Justification", "componentnotpresent" },
{ "Detail", "Vendor statement" },
{
"Confidence",
new BsonDocument
{
{ "Level", "high" },
{ "Score", 0.7 },
{ "Method", "review" },
{ "Unexpected", "ignored" },
}
},
{ "UnknownField", true },
},
}
},
{
"Conflicts",
new BsonArray
{
new BsonDocument
{
{ "ProviderId", "cisco" },
{ "Status", "affected" },
{ "DocumentDigest", "sha256:999" },
{ "Justification", "requiresconfiguration" },
{ "Detail", "Different guidance" },
{ "Reason", "policy_override" },
{ "Other", 1 },
},
}
},
{
"Signals",
new BsonDocument
{
{
"Severity",
new BsonDocument
{
{ "Scheme", "CVSS:3.1" },
{ "Score", 7.5 },
{ "Label", "high" },
{ "Vector", "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" },
}
},
{ "Kev", true },
{ "Epss", 0.42 },
}
},
{ "PolicyVersion", "2025.10" },
{ "PolicyRevisionId", "rev-1" },
{ "PolicyDigest", "sha256:abc" },
{ "Summary", "Vendor confirms not affected." },
{ "GeneratedAt", DateTime.UtcNow },
{ "Unexpected", new BsonDocument { { "foo", "bar" } } },
};
await consensus.InsertOneAsync(document);
var store = new MongoVexConsensusStore(_database);
var result = await store.FindAsync(vulnerabilityId, productKey, CancellationToken.None);
Assert.NotNull(result);
Assert.Equal(vulnerabilityId, result!.VulnerabilityId);
Assert.Equal(productKey, result.Product.Key);
Assert.Equal("Example App", result.Product.Name);
Assert.Equal(VexConsensusStatus.NotAffected, result.Status);
Assert.Single(result.Sources);
var source = result.Sources[0];
Assert.Equal("red-hat", source.ProviderId);
Assert.Equal(VexClaimStatus.NotAffected, source.Status);
Assert.Equal("sha256:123", source.DocumentDigest);
Assert.Equal(0.9, source.Weight);
Assert.Equal(VexJustification.ComponentNotPresent, source.Justification);
Assert.NotNull(source.Confidence);
Assert.Equal("high", source.Confidence!.Level);
Assert.Equal(0.7, source.Confidence!.Score);
Assert.Equal("review", source.Confidence!.Method);
Assert.Single(result.Conflicts);
var conflict = result.Conflicts[0];
Assert.Equal("cisco", conflict.ProviderId);
Assert.Equal(VexClaimStatus.Affected, conflict.Status);
Assert.Equal(VexJustification.RequiresConfiguration, conflict.Justification);
Assert.Equal("policy_override", conflict.Reason);
Assert.Equal("Vendor confirms not affected.", result.Summary);
Assert.Equal("2025.10", result.PolicyVersion);
Assert.NotNull(result.Signals);
Assert.True(result.Signals!.Kev);
Assert.Equal(0.42, result.Signals.Epss);
Assert.NotNull(result.Signals.Severity);
Assert.Equal("CVSS:3.1", result.Signals.Severity!.Scheme);
Assert.Equal(7.5, result.Signals.Severity.Score);
}
[Fact]
public async Task CacheIndex_RoundTripsGridFsMetadata()
{
var gridObjectId = ObjectId.GenerateNewId().ToString();
var index = new MongoVexCacheIndex(_database);
var signature = new VexQuerySignature("format=csaf|vendor=redhat");
var now = DateTimeOffset.UtcNow;
var expires = now.AddHours(12);
var entry = new VexCacheEntry(
signature,
VexExportFormat.Csaf,
new VexContentAddress("sha256", "abcdef123456"),
now,
sizeBytes: 1024,
manifestId: "manifest-001",
gridFsObjectId: gridObjectId,
expiresAt: expires);
await index.SaveAsync(entry, CancellationToken.None);
var cacheId = string.Format(
CultureInfo.InvariantCulture,
"{0}|{1}",
signature.Value,
entry.Format.ToString().ToLowerInvariant());
var cache = _database.GetCollection<BsonDocument>(VexMongoCollectionNames.Cache);
var filter = Builders<BsonDocument>.Filter.Eq("_id", cacheId);
var update = Builders<BsonDocument>.Update.Set("UnexpectedField", true);
await cache.UpdateOneAsync(filter, update);
var roundTrip = await index.FindAsync(signature, VexExportFormat.Csaf, CancellationToken.None);
Assert.NotNull(roundTrip);
Assert.Equal(entry.QuerySignature.Value, roundTrip!.QuerySignature.Value);
Assert.Equal(entry.Format, roundTrip.Format);
Assert.Equal(entry.Artifact.Digest, roundTrip.Artifact.Digest);
Assert.Equal(entry.ManifestId, roundTrip.ManifestId);
Assert.Equal(entry.GridFsObjectId, roundTrip.GridFsObjectId);
Assert.Equal(entry.SizeBytes, roundTrip.SizeBytes);
Assert.NotNull(roundTrip.ExpiresAt);
Assert.Equal(expires.ToUnixTimeMilliseconds(), roundTrip.ExpiresAt!.Value.ToUnixTimeMilliseconds());
}
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync()
{
_runner.Dispose();
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Excititor.Storage.Mongo\StellaOps.Excititor.Storage.Mongo.csproj" />
<ProjectReference Include="..\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
<ProjectReference Include="..\StellaOps.Excititor.Policy\StellaOps.Excititor.Policy.csproj" />
<ProjectReference Include="..\StellaOps.Concelier.Storage.Mongo\StellaOps.Concelier.Storage.Mongo.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,68 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Mongo2Go;
using MongoDB.Driver;
using StellaOps.Excititor.Storage.Mongo.Migrations;
using StellaOps.Excititor.Storage.Mongo;
namespace StellaOps.Excititor.Storage.Mongo.Tests;
public sealed class VexMongoMigrationRunnerTests : IAsyncLifetime
{
private readonly MongoDbRunner _runner;
private readonly IMongoDatabase _database;
public VexMongoMigrationRunnerTests()
{
_runner = MongoDbRunner.Start();
var client = new MongoClient(_runner.ConnectionString);
_database = client.GetDatabase("excititor-migrations-tests");
}
[Fact]
public async Task RunAsync_AppliesInitialIndexesOnce()
{
var migrations = new IVexMongoMigration[]
{
new VexInitialIndexMigration(),
new VexConsensusSignalsMigration(),
};
var runner = new VexMongoMigrationRunner(_database, migrations, NullLogger<VexMongoMigrationRunner>.Instance);
await runner.RunAsync(CancellationToken.None);
await runner.RunAsync(CancellationToken.None);
var appliedCollection = _database.GetCollection<VexMigrationRecord>(VexMongoCollectionNames.Migrations);
var applied = await appliedCollection.Find(FilterDefinition<VexMigrationRecord>.Empty).ToListAsync();
Assert.Equal(2, applied.Count);
Assert.Equal(migrations.Select(m => m.Id).OrderBy(id => id, StringComparer.Ordinal), applied.Select(record => record.Id).OrderBy(id => id, StringComparer.Ordinal));
Assert.True(HasIndex(_database.GetCollection<VexRawDocumentRecord>(VexMongoCollectionNames.Raw), "ProviderId_1_Format_1_RetrievedAt_1"));
Assert.True(HasIndex(_database.GetCollection<VexProviderRecord>(VexMongoCollectionNames.Providers), "Kind_1"));
Assert.True(HasIndex(_database.GetCollection<VexConsensusRecord>(VexMongoCollectionNames.Consensus), "VulnerabilityId_1_Product.Key_1"));
Assert.True(HasIndex(_database.GetCollection<VexConsensusRecord>(VexMongoCollectionNames.Consensus), "PolicyRevisionId_1_PolicyDigest_1"));
Assert.True(HasIndex(_database.GetCollection<VexConsensusRecord>(VexMongoCollectionNames.Consensus), "PolicyRevisionId_1_CalculatedAt_-1"));
Assert.True(HasIndex(_database.GetCollection<VexExportManifestRecord>(VexMongoCollectionNames.Exports), "QuerySignature_1_Format_1"));
Assert.True(HasIndex(_database.GetCollection<VexCacheEntryRecord>(VexMongoCollectionNames.Cache), "QuerySignature_1_Format_1"));
Assert.True(HasIndex(_database.GetCollection<VexCacheEntryRecord>(VexMongoCollectionNames.Cache), "ExpiresAt_1"));
Assert.True(HasIndex(_database.GetCollection<VexStatementRecord>(VexMongoCollectionNames.Statements), "VulnerabilityId_1_Product.Key_1_InsertedAt_-1"));
Assert.True(HasIndex(_database.GetCollection<VexStatementRecord>(VexMongoCollectionNames.Statements), "ProviderId_1_InsertedAt_-1"));
Assert.True(HasIndex(_database.GetCollection<VexStatementRecord>(VexMongoCollectionNames.Statements), "Document.Digest_1"));
}
private static bool HasIndex<TDocument>(IMongoCollection<TDocument> collection, string name)
{
var indexes = collection.Indexes.List().ToList();
return indexes.Any(index => index["name"].AsString == name);
}
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync()
{
_runner.Dispose();
return Task.CompletedTask;
}
}