Files
git.stella-ops.org/src/StellaOps.Feedser.Merge.Tests/MergePrecedenceIntegrationTests.cs

212 lines
8.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using MongoDB.Driver;
using StellaOps.Feedser.Merge.Services;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.MergeEvents;
using StellaOps.Feedser.Testing;
namespace StellaOps.Feedser.Merge.Tests;
[Collection("mongo-fixture")]
public sealed class MergePrecedenceIntegrationTests : IAsyncLifetime
{
private readonly MongoIntegrationFixture _fixture;
private MergeEventStore? _mergeEventStore;
private MergeEventWriter? _mergeEventWriter;
private AdvisoryPrecedenceMerger? _merger;
private FakeTimeProvider? _timeProvider;
public MergePrecedenceIntegrationTests(MongoIntegrationFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task MergePipeline_PsirtOverridesNvd_AndKevOnlyTogglesExploitKnown()
{
await EnsureInitializedAsync();
var merger = _merger!;
var writer = _mergeEventWriter!;
var store = _mergeEventStore!;
var timeProvider = _timeProvider!;
var expectedTimestamp = timeProvider.GetUtcNow();
var nvd = CreateNvdBaseline();
var vendor = CreateVendorOverride();
var kev = CreateKevSignal();
var merged = merger.Merge(new[] { nvd, vendor, kev });
Assert.Equal("CVE-2025-1000", merged.AdvisoryKey);
Assert.Equal("Vendor Security Advisory", merged.Title);
Assert.Equal("Critical impact on supported platforms.", merged.Summary);
Assert.Equal("critical", merged.Severity);
Assert.True(merged.ExploitKnown);
var affected = Assert.Single(merged.AffectedPackages);
Assert.Empty(affected.VersionRanges);
Assert.Contains(affected.Statuses, status => status.Status == "known_affected" && status.Provenance.Source == "vendor");
var mergeProvenance = Assert.Single(merged.Provenance, p => p.Source == "merge");
Assert.Equal("precedence", mergeProvenance.Kind);
Assert.Equal(expectedTimestamp, mergeProvenance.RecordedAt);
Assert.Contains("vendor", mergeProvenance.Value, StringComparison.OrdinalIgnoreCase);
Assert.Contains("kev", mergeProvenance.Value, StringComparison.OrdinalIgnoreCase);
var inputDocumentIds = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
var record = await writer.AppendAsync(merged.AdvisoryKey, nvd, merged, inputDocumentIds, CancellationToken.None);
Assert.Equal(expectedTimestamp, record.MergedAt);
Assert.Equal(inputDocumentIds, record.InputDocumentIds);
Assert.NotEqual(record.BeforeHash, record.AfterHash);
var records = await store.GetRecentAsync(merged.AdvisoryKey, 5, CancellationToken.None);
var persisted = Assert.Single(records);
Assert.Equal(record.Id, persisted.Id);
Assert.Equal(merged.AdvisoryKey, persisted.AdvisoryKey);
Assert.True(persisted.AfterHash.Length > 0);
Assert.True(persisted.BeforeHash.Length > 0);
}
public async Task InitializeAsync()
{
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 3, 1, 0, 0, 0, TimeSpan.Zero))
{
AutoAdvanceAmount = TimeSpan.Zero,
};
_merger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), _timeProvider);
_mergeEventStore = new MergeEventStore(_fixture.Database, NullLogger<MergeEventStore>.Instance);
_mergeEventWriter = new MergeEventWriter(_mergeEventStore, new CanonicalHashCalculator(), _timeProvider, NullLogger<MergeEventWriter>.Instance);
await DropMergeCollectionAsync();
}
public Task DisposeAsync() => Task.CompletedTask;
private async Task EnsureInitializedAsync()
{
if (_mergeEventWriter is null)
{
await InitializeAsync();
}
}
private async Task DropMergeCollectionAsync()
{
try
{
await _fixture.Database.DropCollectionAsync(MongoStorageDefaults.Collections.MergeEvent);
}
catch (MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase))
{
// Collection has not been created yet safe to ignore.
}
}
private static Advisory CreateNvdBaseline()
{
var provenance = new AdvisoryProvenance("nvd", "document", "https://nvd.nist.gov/vuln/detail/CVE-2025-1000", DateTimeOffset.Parse("2025-02-10T00:00:00Z"));
return new Advisory(
"CVE-2025-1000",
"CVE-2025-1000",
"Baseline description from NVD.",
"en",
DateTimeOffset.Parse("2025-02-05T00:00:00Z"),
DateTimeOffset.Parse("2025-02-10T12:00:00Z"),
"medium",
exploitKnown: false,
aliases: new[] { "CVE-2025-1000" },
references: new[]
{
new AdvisoryReference("https://nvd.nist.gov/vuln/detail/CVE-2025-1000", "advisory", "nvd", "NVD reference", provenance),
},
affectedPackages: new[]
{
new AffectedPackage(
AffectedPackageTypes.Cpe,
"cpe:2.3:o:vendor:product:1.0:*:*:*:*:*:*:*",
"vendor-os",
new[]
{
new AffectedVersionRange(
rangeKind: "cpe",
introducedVersion: null,
fixedVersion: null,
lastAffectedVersion: null,
rangeExpression: "<=1.0",
provenance: provenance)
},
Array.Empty<AffectedPackageStatus>(),
new[] { provenance })
},
cvssMetrics: new[]
{
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", provenance)
},
provenance: new[] { provenance });
}
private static Advisory CreateVendorOverride()
{
var provenance = new AdvisoryProvenance("vendor", "psirt", "VSA-2025-1000", DateTimeOffset.Parse("2025-02-11T00:00:00Z"));
return new Advisory(
"CVE-2025-1000",
"Vendor Security Advisory",
"Critical impact on supported platforms.",
"en",
DateTimeOffset.Parse("2025-02-06T00:00:00Z"),
DateTimeOffset.Parse("2025-02-11T06:00:00Z"),
"critical",
exploitKnown: false,
aliases: new[] { "CVE-2025-1000", "VSA-2025-1000" },
references: new[]
{
new AdvisoryReference("https://vendor.example/advisories/VSA-2025-1000", "advisory", "vendor", "Vendor advisory", provenance),
},
affectedPackages: new[]
{
new AffectedPackage(
AffectedPackageTypes.Cpe,
"cpe:2.3:o:vendor:product:1.0:*:*:*:*:*:*:*",
"vendor-os",
Array.Empty<AffectedVersionRange>(),
new[]
{
new AffectedPackageStatus("known_affected", provenance)
},
new[] { provenance })
},
cvssMetrics: new[]
{
new CvssMetric("3.1", "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", 10.0, "critical", provenance)
},
provenance: new[] { provenance });
}
private static Advisory CreateKevSignal()
{
var provenance = new AdvisoryProvenance("kev", "catalog", "CVE-2025-1000", DateTimeOffset.Parse("2025-02-12T00:00:00Z"));
return new Advisory(
"CVE-2025-1000",
"Known Exploited Vulnerability",
null,
null,
published: null,
modified: null,
severity: null,
exploitKnown: true,
aliases: new[] { "KEV-CVE-2025-1000" },
references: Array.Empty<AdvisoryReference>(),
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { provenance });
}
}