243 lines
9.5 KiB
C#
243 lines
9.5 KiB
C#
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 },
|
|
},
|
|
}
|
|
},
|
|
{ "PolicyVersion", "2025.10" },
|
|
{ "PolicyRevisionId", "rev-1" },
|
|
{ "PolicyDigest", "sha256:abc" },
|
|
{ "Summary", "Vendor confirms not affected." },
|
|
{ "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);
|
|
}
|
|
|
|
[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;
|
|
}
|
|
}
|