Resolve Concelier/Excititor merge conflicts
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user