Initial commit (history squashed)
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user