Initial commit (history squashed)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"resultsPerPage": 1,
|
||||
"startIndex": 0,
|
||||
"totalResults": 1,
|
||||
"vulnerabilities": "this-should-be-an-array"
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"resultsPerPage": 2,
|
||||
"startIndex": 0,
|
||||
"totalResults": 5,
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": {
|
||||
"id": "CVE-2024-1000",
|
||||
"sourceIdentifier": "nvd@nist.gov",
|
||||
"published": "2024-02-01T10:00:00Z",
|
||||
"lastModified": "2024-02-02T10:00:00Z",
|
||||
"descriptions": [
|
||||
{ "lang": "en", "value": "Multipage vulnerability one." }
|
||||
],
|
||||
"metrics": {
|
||||
"cvssMetricV31": [
|
||||
{
|
||||
"cvssData": {
|
||||
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
||||
"baseScore": 9.8,
|
||||
"baseSeverity": "CRITICAL"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"nodes": [
|
||||
{
|
||||
"cpeMatch": [
|
||||
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_a:1.0:*:*:*:*:*:*:*" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"cve": {
|
||||
"id": "CVE-2024-1001",
|
||||
"sourceIdentifier": "nvd@nist.gov",
|
||||
"published": "2024-02-01T11:00:00Z",
|
||||
"lastModified": "2024-02-02T11:00:00Z",
|
||||
"descriptions": [
|
||||
{ "lang": "en", "value": "Multipage vulnerability two." }
|
||||
],
|
||||
"metrics": {
|
||||
"cvssMetricV31": [
|
||||
{
|
||||
"cvssData": {
|
||||
"vectorString": "CVSS:3.1/AV:P/AC:L/PR:L/UI:R/S:U/C:L/I:L/A:L",
|
||||
"baseScore": 5.1,
|
||||
"baseSeverity": "MEDIUM"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"nodes": [
|
||||
{
|
||||
"cpeMatch": [
|
||||
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_b:2.0:*:*:*:*:*:*:*" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"resultsPerPage": 2,
|
||||
"startIndex": 2,
|
||||
"totalResults": 5,
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": {
|
||||
"id": "CVE-2024-1002",
|
||||
"sourceIdentifier": "nvd@nist.gov",
|
||||
"published": "2024-02-01T12:00:00Z",
|
||||
"lastModified": "2024-02-02T12:00:00Z",
|
||||
"descriptions": [
|
||||
{ "lang": "en", "value": "Multipage vulnerability three." }
|
||||
],
|
||||
"metrics": {
|
||||
"cvssMetricV31": [
|
||||
{
|
||||
"cvssData": {
|
||||
"vectorString": "CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N",
|
||||
"baseScore": 3.1,
|
||||
"baseSeverity": "LOW"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"nodes": [
|
||||
{
|
||||
"cpeMatch": [
|
||||
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_c:3.0:*:*:*:*:*:*:*" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"cve": {
|
||||
"id": "CVE-2024-1003",
|
||||
"sourceIdentifier": "nvd@nist.gov",
|
||||
"published": "2024-02-01T13:00:00Z",
|
||||
"lastModified": "2024-02-02T13:00:00Z",
|
||||
"descriptions": [
|
||||
{ "lang": "en", "value": "Multipage vulnerability four." }
|
||||
],
|
||||
"metrics": {
|
||||
"cvssMetricV31": [
|
||||
{
|
||||
"cvssData": {
|
||||
"vectorString": "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:M/I:L/A:L",
|
||||
"baseScore": 7.4,
|
||||
"baseSeverity": "HIGH"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"nodes": [
|
||||
{
|
||||
"cpeMatch": [
|
||||
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_d:4.0:*:*:*:*:*:*:*" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"resultsPerPage": 2,
|
||||
"startIndex": 4,
|
||||
"totalResults": 5,
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": {
|
||||
"id": "CVE-2024-1004",
|
||||
"sourceIdentifier": "nvd@nist.gov",
|
||||
"published": "2024-02-01T14:00:00Z",
|
||||
"lastModified": "2024-02-02T14:00:00Z",
|
||||
"descriptions": [
|
||||
{ "lang": "en", "value": "Multipage vulnerability five." }
|
||||
],
|
||||
"metrics": {
|
||||
"cvssMetricV31": [
|
||||
{
|
||||
"cvssData": {
|
||||
"vectorString": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:L/I:H/A:L",
|
||||
"baseScore": 7.9,
|
||||
"baseSeverity": "HIGH"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"nodes": [
|
||||
{
|
||||
"cpeMatch": [
|
||||
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_e:5.0:*:*:*:*:*:*:*" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"resultsPerPage": 2000,
|
||||
"startIndex": 0,
|
||||
"totalResults": 2,
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": {
|
||||
"id": "CVE-2024-0001",
|
||||
"sourceIdentifier": "nvd@nist.gov",
|
||||
"published": "2024-01-01T10:00:00Z",
|
||||
"lastModified": "2024-01-02T10:00:00Z",
|
||||
"descriptions": [
|
||||
{ "lang": "en", "value": "Example vulnerability one." }
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"url": "https://vendor.example.com/advisories/0001",
|
||||
"source": "Vendor",
|
||||
"tags": ["Vendor Advisory"]
|
||||
}
|
||||
],
|
||||
"metrics": {
|
||||
"cvssMetricV31": [
|
||||
{
|
||||
"cvssData": {
|
||||
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
||||
"baseScore": 9.8,
|
||||
"baseSeverity": "CRITICAL"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"nodes": [
|
||||
{
|
||||
"cpeMatch": [
|
||||
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"cve": {
|
||||
"id": "CVE-2024-0002",
|
||||
"sourceIdentifier": "nvd@nist.gov",
|
||||
"published": "2024-01-01T11:00:00Z",
|
||||
"lastModified": "2024-01-02T11:00:00Z",
|
||||
"descriptions": [
|
||||
{ "lang": "fr", "value": "Description française" },
|
||||
{ "lang": "en", "value": "Example vulnerability two." }
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"url": "https://cisa.example.gov/alerts/0002",
|
||||
"source": "CISA",
|
||||
"tags": ["US Government Resource"]
|
||||
}
|
||||
],
|
||||
"metrics": {
|
||||
"cvssMetricV30": [
|
||||
{
|
||||
"cvssData": {
|
||||
"vectorString": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L",
|
||||
"baseScore": 4.6,
|
||||
"baseSeverity": "MEDIUM"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"nodes": [
|
||||
{
|
||||
"cpeMatch": [
|
||||
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_two:2.0:*:*:*:*:*:*:*" },
|
||||
{ "vulnerable": false, "criteria": "cpe:2.3:a:example:product_two:2.1:*:*:*:*:*:*:*" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"resultsPerPage": 2000,
|
||||
"startIndex": 0,
|
||||
"totalResults": 1,
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": {
|
||||
"id": "CVE-2024-0003",
|
||||
"sourceIdentifier": "nvd@nist.gov",
|
||||
"published": "2024-01-01T12:00:00Z",
|
||||
"lastModified": "2024-01-02T12:00:00Z",
|
||||
"descriptions": [
|
||||
{ "lang": "en", "value": "Example vulnerability three." }
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"url": "https://example.org/patches/0003",
|
||||
"source": "Vendor",
|
||||
"tags": ["Patch"]
|
||||
}
|
||||
],
|
||||
"metrics": {
|
||||
"cvssMetricV2": [
|
||||
{
|
||||
"cvssData": {
|
||||
"vectorString": "AV:N/AC:M/Au:N/C:P/I:P/A:P",
|
||||
"baseScore": 6.8,
|
||||
"baseSeverity": "MEDIUM"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"nodes": [
|
||||
{
|
||||
"cpeMatch": [
|
||||
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_three:3.5:*:*:*:*:*:*:*" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"resultsPerPage": 2000,
|
||||
"startIndex": 0,
|
||||
"totalResults": 1,
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": {
|
||||
"id": "CVE-2024-0001",
|
||||
"sourceIdentifier": "nvd@nist.gov",
|
||||
"published": "2024-01-01T10:00:00Z",
|
||||
"lastModified": "2024-01-03T12:00:00Z",
|
||||
"descriptions": [
|
||||
{ "lang": "en", "value": "Example vulnerability one updated." }
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"url": "https://vendor.example.com/advisories/0001",
|
||||
"source": "Vendor",
|
||||
"tags": ["Vendor Advisory"]
|
||||
},
|
||||
{
|
||||
"url": "https://kb.example.com/articles/0001",
|
||||
"source": "KnowledgeBase",
|
||||
"tags": ["Third Party Advisory"]
|
||||
}
|
||||
],
|
||||
"metrics": {
|
||||
"cvssMetricV31": [
|
||||
{
|
||||
"cvssData": {
|
||||
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
|
||||
"baseScore": 8.8,
|
||||
"baseSeverity": "HIGH"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"nodes": [
|
||||
{
|
||||
"cpeMatch": [
|
||||
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*" },
|
||||
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_one:1.1:*:*:*:*:*:*:*" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Feedser.Source.Common.Testing;
|
||||
using StellaOps.Feedser.Source.Nvd;
|
||||
using StellaOps.Feedser.Source.Nvd.Configuration;
|
||||
using StellaOps.Feedser.Storage.Mongo;
|
||||
using StellaOps.Feedser.Storage.Mongo.Advisories;
|
||||
using StellaOps.Feedser.Storage.Mongo.Documents;
|
||||
using StellaOps.Feedser.Testing;
|
||||
using StellaOps.Feedser.Testing;
|
||||
using System.Net;
|
||||
|
||||
namespace StellaOps.Feedser.Source.Nvd.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class NvdConnectorHarnessTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ConnectorTestHarness _harness;
|
||||
|
||||
public NvdConnectorHarnessTests(MongoIntegrationFixture fixture)
|
||||
{
|
||||
_harness = new ConnectorTestHarness(fixture, new DateTimeOffset(2024, 1, 2, 12, 0, 0, TimeSpan.Zero), NvdOptions.HttpClientName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchAsync_MultiPagePersistsStartIndexMetadata()
|
||||
{
|
||||
await _harness.ResetAsync();
|
||||
|
||||
var options = new NvdOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://nvd.example.test/api"),
|
||||
WindowSize = TimeSpan.FromHours(1),
|
||||
WindowOverlap = TimeSpan.FromMinutes(5),
|
||||
InitialBackfill = TimeSpan.FromHours(2),
|
||||
};
|
||||
|
||||
var timeProvider = _harness.TimeProvider;
|
||||
var handler = _harness.Handler;
|
||||
|
||||
var windowStart = timeProvider.GetUtcNow() - options.InitialBackfill;
|
||||
var windowEnd = windowStart + options.WindowSize;
|
||||
|
||||
var firstUri = BuildRequestUri(options, windowStart, windowEnd);
|
||||
var secondUri = BuildRequestUri(options, windowStart, windowEnd, startIndex: 2);
|
||||
var thirdUri = BuildRequestUri(options, windowStart, windowEnd, startIndex: 4);
|
||||
|
||||
handler.AddJsonResponse(firstUri, ReadFixture("nvd-multipage-1.json"));
|
||||
handler.AddJsonResponse(secondUri, ReadFixture("nvd-multipage-2.json"));
|
||||
handler.AddJsonResponse(thirdUri, ReadFixture("nvd-multipage-3.json"));
|
||||
|
||||
await _harness.EnsureServiceProviderAsync(services =>
|
||||
{
|
||||
services.AddNvdConnector(opts =>
|
||||
{
|
||||
opts.BaseEndpoint = options.BaseEndpoint;
|
||||
opts.WindowSize = options.WindowSize;
|
||||
opts.WindowOverlap = options.WindowOverlap;
|
||||
opts.InitialBackfill = options.InitialBackfill;
|
||||
});
|
||||
});
|
||||
|
||||
var provider = _harness.ServiceProvider;
|
||||
var connector = new NvdConnectorPlugin().Create(provider);
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
|
||||
var documentStore = provider.GetRequiredService<IDocumentStore>();
|
||||
|
||||
var firstDocument = await documentStore.FindBySourceAndUriAsync(NvdConnectorPlugin.SourceName, firstUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(firstDocument);
|
||||
Assert.Equal("0", firstDocument!.Metadata["startIndex"]);
|
||||
|
||||
var secondDocument = await documentStore.FindBySourceAndUriAsync(NvdConnectorPlugin.SourceName, secondUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(secondDocument);
|
||||
Assert.Equal("2", secondDocument!.Metadata["startIndex"]);
|
||||
|
||||
var thirdDocument = await documentStore.FindBySourceAndUriAsync(NvdConnectorPlugin.SourceName, thirdUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(thirdDocument);
|
||||
Assert.Equal("4", thirdDocument!.Metadata["startIndex"]);
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
var pendingDocuments = state!.Cursor.TryGetValue("pendingDocuments", out var pending)
|
||||
? pending.AsBsonArray
|
||||
: new BsonArray();
|
||||
Assert.Equal(3, pendingDocuments.Count);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public Task DisposeAsync() => _harness.ResetAsync();
|
||||
|
||||
private static Uri BuildRequestUri(NvdOptions options, DateTimeOffset start, DateTimeOffset end, int startIndex = 0)
|
||||
{
|
||||
var builder = new UriBuilder(options.BaseEndpoint);
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
["lastModifiedStartDate"] = start.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK"),
|
||||
["lastModifiedEndDate"] = end.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK"),
|
||||
["resultsPerPage"] = "2000",
|
||||
};
|
||||
|
||||
if (startIndex > 0)
|
||||
{
|
||||
parameters["startIndex"] = startIndex.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
builder.Query = string.Join("&", parameters.Select(kvp => $"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}"));
|
||||
return builder.Uri;
|
||||
}
|
||||
|
||||
private static string ReadFixture(string filename)
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "Source", "Nvd", "Fixtures", filename);
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
}
|
||||
607
src/StellaOps.Feedser.Source.Nvd.Tests/Nvd/NvdConnectorTests.cs
Normal file
607
src/StellaOps.Feedser.Source.Nvd.Tests/Nvd/NvdConnectorTests.cs
Normal file
@@ -0,0 +1,607 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Diagnostics.Metrics;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Feedser.Source.Common.Fetch;
|
||||
using StellaOps.Feedser.Source.Common.Http;
|
||||
using StellaOps.Feedser.Source.Common.Testing;
|
||||
using StellaOps.Feedser.Source.Common;
|
||||
using StellaOps.Feedser.Source.Nvd;
|
||||
using StellaOps.Feedser.Source.Nvd.Configuration;
|
||||
using StellaOps.Feedser.Source.Nvd.Internal;
|
||||
using StellaOps.Feedser.Storage.Mongo;
|
||||
using StellaOps.Feedser.Storage.Mongo.Advisories;
|
||||
using StellaOps.Feedser.Storage.Mongo.Documents;
|
||||
using StellaOps.Feedser.Storage.Mongo.Dtos;
|
||||
using StellaOps.Feedser.Storage.Mongo.ChangeHistory;
|
||||
using StellaOps.Feedser.Testing;
|
||||
|
||||
namespace StellaOps.Feedser.Source.Nvd.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class NvdConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private FakeTimeProvider _timeProvider;
|
||||
private readonly DateTimeOffset _initialNow;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
private ServiceProvider? _serviceProvider;
|
||||
|
||||
public NvdConnectorTests(MongoIntegrationFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_initialNow = new DateTimeOffset(2024, 1, 2, 12, 0, 0, TimeSpan.Zero);
|
||||
_timeProvider = new FakeTimeProvider(_initialNow);
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_FlowProducesCanonicalAdvisories()
|
||||
{
|
||||
await ResetDatabaseAsync();
|
||||
|
||||
var options = new NvdOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://nvd.example.test/api"),
|
||||
WindowSize = TimeSpan.FromHours(1),
|
||||
WindowOverlap = TimeSpan.FromMinutes(5),
|
||||
InitialBackfill = TimeSpan.FromHours(2),
|
||||
};
|
||||
|
||||
var window1Start = _timeProvider.GetUtcNow() - options.InitialBackfill;
|
||||
var window1End = window1Start + options.WindowSize;
|
||||
_handler.AddJsonResponse(BuildRequestUri(options, window1Start, window1End), ReadFixture("nvd-window-1.json"));
|
||||
|
||||
await EnsureServiceProviderAsync(options);
|
||||
var provider = _serviceProvider!;
|
||||
|
||||
var connector = new NvdConnectorPlugin().Create(provider);
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Contains(advisories, advisory => advisory.AdvisoryKey == "CVE-2024-0001");
|
||||
Assert.Contains(advisories, advisory => advisory.AdvisoryKey == "CVE-2024-0002");
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
var cursorDocument = state!.Cursor;
|
||||
Assert.NotNull(cursorDocument);
|
||||
var lastWindowEnd = cursorDocument.TryGetValue("windowEnd", out var endValue) ? ReadDateTime(endValue) : (DateTimeOffset?)null;
|
||||
Assert.Equal(window1End.UtcDateTime, lastWindowEnd?.UtcDateTime);
|
||||
|
||||
_timeProvider.Advance(TimeSpan.FromHours(1));
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var startCandidate = (lastWindowEnd ?? window1End) - options.WindowOverlap;
|
||||
var backfillLimit = now - options.InitialBackfill;
|
||||
var window2Start = startCandidate < backfillLimit ? backfillLimit : startCandidate;
|
||||
var window2End = window2Start + options.WindowSize;
|
||||
if (window2End > now)
|
||||
{
|
||||
window2End = now;
|
||||
}
|
||||
|
||||
_handler.AddJsonResponse(BuildRequestUri(options, window2Start, window2End), ReadFixture("nvd-window-2.json"));
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(3, advisories.Count);
|
||||
Assert.Contains(advisories, advisory => advisory.AdvisoryKey == "CVE-2024-0003");
|
||||
|
||||
var documentStore = provider.GetRequiredService<IDocumentStore>();
|
||||
var finalState = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(finalState);
|
||||
var pendingDocuments = finalState!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs)
|
||||
? pendingDocs.AsBsonArray
|
||||
: new BsonArray();
|
||||
Assert.Empty(pendingDocuments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchAsync_MultiPageWindowFetchesAllPages()
|
||||
{
|
||||
await ResetDatabaseAsync();
|
||||
|
||||
var options = new NvdOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://nvd.example.test/api"),
|
||||
WindowSize = TimeSpan.FromHours(1),
|
||||
WindowOverlap = TimeSpan.FromMinutes(5),
|
||||
InitialBackfill = TimeSpan.FromHours(2),
|
||||
};
|
||||
|
||||
var windowStart = _timeProvider.GetUtcNow() - options.InitialBackfill;
|
||||
var windowEnd = windowStart + options.WindowSize;
|
||||
|
||||
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd), ReadFixture("nvd-multipage-1.json"));
|
||||
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 2), ReadFixture("nvd-multipage-2.json"));
|
||||
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
|
||||
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
|
||||
|
||||
await EnsureServiceProviderAsync(options);
|
||||
var provider = _serviceProvider!;
|
||||
var connector = new NvdConnectorPlugin().Create(provider);
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
var pendingDocuments = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs)
|
||||
? pendingDocs.AsBsonArray.Select(v => Guid.Parse(v.AsString)).ToArray()
|
||||
: Array.Empty<Guid>();
|
||||
Assert.Equal(3, pendingDocuments.Length);
|
||||
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
var advisoryKeys = advisories.Select(advisory => advisory.AdvisoryKey).OrderBy(k => k).ToArray();
|
||||
|
||||
Assert.Equal(new[] { "CVE-2024-1000", "CVE-2024-1001", "CVE-2024-1002", "CVE-2024-1003", "CVE-2024-1004" }, advisoryKeys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Observability_RecordsCountersForSuccessfulFlow()
|
||||
{
|
||||
await ResetDatabaseAsync();
|
||||
|
||||
var options = new NvdOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://nvd.example.test/api"),
|
||||
WindowSize = TimeSpan.FromHours(1),
|
||||
WindowOverlap = TimeSpan.FromMinutes(5),
|
||||
InitialBackfill = TimeSpan.FromHours(2),
|
||||
};
|
||||
|
||||
using var collector = new MetricCollector(NvdDiagnostics.MeterName);
|
||||
|
||||
var windowStart = _timeProvider.GetUtcNow() - options.InitialBackfill;
|
||||
var windowEnd = windowStart + options.WindowSize;
|
||||
|
||||
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd), ReadFixture("nvd-multipage-1.json"));
|
||||
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 2), ReadFixture("nvd-multipage-2.json"));
|
||||
|
||||
await EnsureServiceProviderAsync(options);
|
||||
var provider = _serviceProvider!;
|
||||
var connector = new NvdConnectorPlugin().Create(provider);
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
Assert.Equal(3, collector.GetValue("nvd.fetch.attempts"));
|
||||
Assert.Equal(3, collector.GetValue("nvd.fetch.documents"));
|
||||
Assert.Equal(0, collector.GetValue("nvd.fetch.failures"));
|
||||
Assert.Equal(0, collector.GetValue("nvd.fetch.unchanged"));
|
||||
Assert.Equal(3, collector.GetValue("nvd.parse.success"));
|
||||
Assert.Equal(0, collector.GetValue("nvd.parse.failures"));
|
||||
Assert.Equal(0, collector.GetValue("nvd.parse.quarantine"));
|
||||
Assert.Equal(5, collector.GetValue("nvd.map.success"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChangeHistory_RecordsDifferencesForModifiedCve()
|
||||
{
|
||||
await ResetDatabaseAsync();
|
||||
|
||||
var options = new NvdOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://nvd.example.test/api"),
|
||||
WindowSize = TimeSpan.FromHours(1),
|
||||
WindowOverlap = TimeSpan.FromMinutes(5),
|
||||
InitialBackfill = TimeSpan.FromHours(2),
|
||||
};
|
||||
|
||||
var windowStart = _timeProvider.GetUtcNow() - options.InitialBackfill;
|
||||
var windowEnd = windowStart + options.WindowSize;
|
||||
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd), ReadFixture("nvd-window-1.json"));
|
||||
|
||||
await EnsureServiceProviderAsync(options);
|
||||
var provider = _serviceProvider!;
|
||||
var connector = new NvdConnectorPlugin().Create(provider);
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var historyStore = provider.GetRequiredService<IChangeHistoryStore>();
|
||||
var historyEntries = await historyStore.GetRecentAsync("nvd", "CVE-2024-0001", 5, CancellationToken.None);
|
||||
Assert.Empty(historyEntries);
|
||||
|
||||
_timeProvider.Advance(TimeSpan.FromHours(2));
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
|
||||
var cursorDocument = state!.Cursor;
|
||||
var lastWindowEnd = cursorDocument.TryGetValue("windowEnd", out var endValue) ? ReadDateTime(endValue) : (DateTimeOffset?)null;
|
||||
var startCandidate = (lastWindowEnd ?? windowEnd) - options.WindowOverlap;
|
||||
var backfillLimit = now - options.InitialBackfill;
|
||||
var window2Start = startCandidate < backfillLimit ? backfillLimit : startCandidate;
|
||||
var window2End = window2Start + options.WindowSize;
|
||||
if (window2End > now)
|
||||
{
|
||||
window2End = now;
|
||||
}
|
||||
|
||||
_handler.AddJsonResponse(BuildRequestUri(options, window2Start, window2End), ReadFixture("nvd-window-update.json"));
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var updatedAdvisory = await advisoryStore.FindAsync("CVE-2024-0001", CancellationToken.None);
|
||||
Assert.NotNull(updatedAdvisory);
|
||||
Assert.Equal("high", updatedAdvisory!.Severity);
|
||||
|
||||
historyEntries = await historyStore.GetRecentAsync("nvd", "CVE-2024-0001", 5, CancellationToken.None);
|
||||
Assert.NotEmpty(historyEntries);
|
||||
var latest = historyEntries[0];
|
||||
Assert.Equal("nvd", latest.SourceName);
|
||||
Assert.Equal("CVE-2024-0001", latest.AdvisoryKey);
|
||||
Assert.NotNull(latest.PreviousHash);
|
||||
Assert.NotEqual(latest.PreviousHash, latest.CurrentHash);
|
||||
Assert.Contains(latest.Changes, change => change.Field == "severity" && change.ChangeType == "Modified");
|
||||
Assert.Contains(latest.Changes, change => change.Field == "references" && change.ChangeType == "Modified");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_InvalidSchema_QuarantinesDocumentAndEmitsMetric()
|
||||
{
|
||||
await ResetDatabaseAsync();
|
||||
|
||||
var options = new NvdOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://nvd.example.test/api"),
|
||||
WindowSize = TimeSpan.FromHours(1),
|
||||
WindowOverlap = TimeSpan.FromMinutes(5),
|
||||
InitialBackfill = TimeSpan.FromHours(2),
|
||||
};
|
||||
|
||||
using var collector = new MetricCollector(NvdDiagnostics.MeterName);
|
||||
|
||||
var windowStart = _timeProvider.GetUtcNow() - options.InitialBackfill;
|
||||
var windowEnd = windowStart + options.WindowSize;
|
||||
var requestUri = BuildRequestUri(options, windowStart, windowEnd);
|
||||
|
||||
_handler.AddJsonResponse(requestUri, ReadFixture("nvd-invalid-schema.json"));
|
||||
|
||||
await EnsureServiceProviderAsync(options);
|
||||
var provider = _serviceProvider!;
|
||||
var connector = new NvdConnectorPlugin().Create(provider);
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
|
||||
var documentStore = provider.GetRequiredService<IDocumentStore>();
|
||||
var document = await documentStore.FindBySourceAndUriAsync(NvdConnectorPlugin.SourceName, requestUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(document);
|
||||
Assert.Equal(DocumentStatuses.Failed, document!.Status);
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
var pendingDocs = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue) ? pendingDocsValue.AsBsonArray : new BsonArray();
|
||||
Assert.Empty(pendingDocs);
|
||||
var pendingMappings = state.Cursor.TryGetValue("pendingMappings", out var pendingMappingsValue) ? pendingMappingsValue.AsBsonArray : new BsonArray();
|
||||
Assert.Empty(pendingMappings);
|
||||
|
||||
Assert.Equal(1, collector.GetValue("nvd.fetch.documents"));
|
||||
Assert.Equal(0, collector.GetValue("nvd.parse.success"));
|
||||
Assert.Equal(1, collector.GetValue("nvd.parse.quarantine"));
|
||||
Assert.Equal(0, collector.GetValue("nvd.map.success"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResetDatabase_IsolatesRuns()
|
||||
{
|
||||
await ResetDatabaseAsync();
|
||||
|
||||
var options = new NvdOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://nvd.example.test/api"),
|
||||
WindowSize = TimeSpan.FromHours(1),
|
||||
WindowOverlap = TimeSpan.FromMinutes(5),
|
||||
InitialBackfill = TimeSpan.FromHours(2),
|
||||
};
|
||||
|
||||
var start = _timeProvider.GetUtcNow() - options.InitialBackfill;
|
||||
var end = start + options.WindowSize;
|
||||
_handler.AddJsonResponse(BuildRequestUri(options, start, end), ReadFixture("nvd-window-1.json"));
|
||||
|
||||
await EnsureServiceProviderAsync(options);
|
||||
var provider = _serviceProvider!;
|
||||
var connector = new NvdConnectorPlugin().Create(provider);
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var firstRunKeys = (await advisoryStore.GetRecentAsync(10, CancellationToken.None))
|
||||
.Select(advisory => advisory.AdvisoryKey)
|
||||
.OrderBy(k => k)
|
||||
.ToArray();
|
||||
Assert.Equal(new[] { "CVE-2024-0001", "CVE-2024-0002" }, firstRunKeys);
|
||||
|
||||
await ResetDatabaseAsync();
|
||||
|
||||
options = new NvdOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://nvd.example.test/api"),
|
||||
WindowSize = TimeSpan.FromHours(1),
|
||||
WindowOverlap = TimeSpan.FromMinutes(5),
|
||||
InitialBackfill = TimeSpan.FromHours(2),
|
||||
};
|
||||
|
||||
start = _timeProvider.GetUtcNow() - options.InitialBackfill;
|
||||
end = start + options.WindowSize;
|
||||
_handler.AddJsonResponse(BuildRequestUri(options, start, end), ReadFixture("nvd-window-2.json"));
|
||||
|
||||
await EnsureServiceProviderAsync(options);
|
||||
provider = _serviceProvider!;
|
||||
connector = new NvdConnectorPlugin().Create(provider);
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var secondRunKeys = (await advisoryStore.GetRecentAsync(10, CancellationToken.None))
|
||||
.Select(advisory => advisory.AdvisoryKey)
|
||||
.OrderBy(k => k)
|
||||
.ToArray();
|
||||
Assert.Equal(new[] { "CVE-2024-0003" }, secondRunKeys);
|
||||
}
|
||||
|
||||
private async Task EnsureServiceProviderAsync(NvdOptions options)
|
||||
{
|
||||
if (_serviceProvider is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_serviceProvider = await CreateServiceProviderAsync(options, _handler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Resume_CompletesPendingDocumentsAfterRestart()
|
||||
{
|
||||
await ResetDatabaseAsync();
|
||||
|
||||
var options = new NvdOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://nvd.example.test/api"),
|
||||
WindowSize = TimeSpan.FromHours(1),
|
||||
WindowOverlap = TimeSpan.FromMinutes(5),
|
||||
InitialBackfill = TimeSpan.FromHours(2),
|
||||
};
|
||||
|
||||
var windowStart = _timeProvider.GetUtcNow() - options.InitialBackfill;
|
||||
var windowEnd = windowStart + options.WindowSize;
|
||||
var requestUri = BuildRequestUri(options, windowStart, windowEnd);
|
||||
|
||||
var fetchHandler = new CannedHttpMessageHandler();
|
||||
fetchHandler.AddJsonResponse(requestUri, ReadFixture("nvd-window-1.json"));
|
||||
|
||||
Guid[] pendingDocumentIds;
|
||||
await using (var fetchProvider = await CreateServiceProviderAsync(options, fetchHandler))
|
||||
{
|
||||
var connector = new NvdConnectorPlugin().Create(fetchProvider);
|
||||
await connector.FetchAsync(fetchProvider, CancellationToken.None);
|
||||
|
||||
var stateRepository = fetchProvider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
var pending = state!.Cursor.TryGetValue("pendingDocuments", out var value)
|
||||
? value.AsBsonArray
|
||||
: new BsonArray();
|
||||
Assert.NotEmpty(pending);
|
||||
pendingDocumentIds = pending.Select(v => Guid.Parse(v.AsString)).ToArray();
|
||||
}
|
||||
|
||||
var resumeHandler = new CannedHttpMessageHandler();
|
||||
await using (var resumeProvider = await CreateServiceProviderAsync(options, resumeHandler))
|
||||
{
|
||||
var resumeConnector = new NvdConnectorPlugin().Create(resumeProvider);
|
||||
|
||||
await resumeConnector.ParseAsync(resumeProvider, CancellationToken.None);
|
||||
await resumeConnector.MapAsync(resumeProvider, CancellationToken.None);
|
||||
|
||||
var documentStore = resumeProvider.GetRequiredService<IDocumentStore>();
|
||||
foreach (var documentId in pendingDocumentIds)
|
||||
{
|
||||
var document = await documentStore.FindAsync(documentId, CancellationToken.None);
|
||||
Assert.NotNull(document);
|
||||
Assert.Equal(DocumentStatuses.Mapped, document!.Status);
|
||||
}
|
||||
|
||||
var advisoryStore = resumeProvider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.NotEmpty(advisories);
|
||||
|
||||
var stateRepository = resumeProvider.GetRequiredService<ISourceStateRepository>();
|
||||
var finalState = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(finalState);
|
||||
var cursor = finalState!.Cursor;
|
||||
var finalPendingDocs = cursor.TryGetValue("pendingDocuments", out var pendingDocs) ? pendingDocs.AsBsonArray : new BsonArray();
|
||||
Assert.Empty(finalPendingDocs);
|
||||
var finalPendingMappings = cursor.TryGetValue("pendingMappings", out var pendingMappings) ? pendingMappings.AsBsonArray : new BsonArray();
|
||||
Assert.Empty(finalPendingMappings);
|
||||
}
|
||||
}
|
||||
|
||||
private Task ResetDatabaseAsync()
|
||||
{
|
||||
return ResetDatabaseInternalAsync();
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> CreateServiceProviderAsync(NvdOptions options, CannedHttpMessageHandler handler)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(handler);
|
||||
|
||||
services.AddMongoStorage(storageOptions =>
|
||||
{
|
||||
storageOptions.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
storageOptions.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
storageOptions.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddNvdConnector(configure: opts =>
|
||||
{
|
||||
opts.BaseEndpoint = options.BaseEndpoint;
|
||||
opts.WindowSize = options.WindowSize;
|
||||
opts.WindowOverlap = options.WindowOverlap;
|
||||
opts.InitialBackfill = options.InitialBackfill;
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(NvdOptions.HttpClientName, builderOptions =>
|
||||
{
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
|
||||
{
|
||||
builder.PrimaryHandler = handler;
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private async Task ResetDatabaseInternalAsync()
|
||||
{
|
||||
if (_serviceProvider is not null)
|
||||
{
|
||||
if (_serviceProvider is IAsyncDisposable asyncDisposable)
|
||||
{
|
||||
await asyncDisposable.DisposeAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_serviceProvider.Dispose();
|
||||
}
|
||||
|
||||
_serviceProvider = null;
|
||||
}
|
||||
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
_timeProvider = new FakeTimeProvider(_initialNow);
|
||||
}
|
||||
|
||||
private sealed class MetricCollector : IDisposable
|
||||
{
|
||||
private readonly MeterListener _listener;
|
||||
private readonly ConcurrentDictionary<string, long> _measurements = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public MetricCollector(string meterName)
|
||||
{
|
||||
_listener = new MeterListener
|
||||
{
|
||||
InstrumentPublished = (instrument, listener) =>
|
||||
{
|
||||
if (instrument.Meter.Name == meterName)
|
||||
{
|
||||
listener.EnableMeasurementEvents(instrument);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
_measurements.AddOrUpdate(instrument.Name, measurement, (_, existing) => existing + measurement);
|
||||
});
|
||||
|
||||
_listener.Start();
|
||||
}
|
||||
|
||||
public long GetValue(string instrumentName)
|
||||
=> _measurements.TryGetValue(instrumentName, out var value) ? value : 0;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_listener.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static Uri BuildRequestUri(NvdOptions options, DateTimeOffset start, DateTimeOffset end, int startIndex = 0)
|
||||
{
|
||||
var builder = new UriBuilder(options.BaseEndpoint);
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
["lastModifiedStartDate"] = start.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK"),
|
||||
["lastModifiedEndDate"] = end.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK"),
|
||||
["resultsPerPage"] = "2000",
|
||||
};
|
||||
|
||||
if (startIndex > 0)
|
||||
{
|
||||
parameters["startIndex"] = startIndex.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
builder.Query = string.Join("&", parameters.Select(static kvp => $"{System.Net.WebUtility.UrlEncode(kvp.Key)}={System.Net.WebUtility.UrlEncode(kvp.Value)}"));
|
||||
return builder.Uri;
|
||||
}
|
||||
|
||||
private static DateTimeOffset? ReadDateTime(BsonValue value)
|
||||
{
|
||||
return value.BsonType switch
|
||||
{
|
||||
BsonType.DateTime => DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc),
|
||||
BsonType.String when DateTimeOffset.TryParse(value.AsString, out var parsed) => parsed.ToUniversalTime(),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static string ReadFixture(string filename)
|
||||
{
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
var primary = Path.Combine(baseDirectory, "Source", "Nvd", "Fixtures", filename);
|
||||
if (File.Exists(primary))
|
||||
{
|
||||
return File.ReadAllText(primary);
|
||||
}
|
||||
|
||||
var secondary = Path.Combine(baseDirectory, "Nvd", "Fixtures", filename);
|
||||
if (File.Exists(secondary))
|
||||
{
|
||||
return File.ReadAllText(secondary);
|
||||
}
|
||||
|
||||
throw new FileNotFoundException($"Fixture '{filename}' was not found in the test output directory.");
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await ResetDatabaseInternalAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Feedser.Models/StellaOps.Feedser.Models.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Feedser.Source.Common/StellaOps.Feedser.Source.Common.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Feedser.Source.Nvd/StellaOps.Feedser.Source.Nvd.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Feedser.Storage.Mongo/StellaOps.Feedser.Storage.Mongo.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Nvd/Fixtures/*.json" CopyToOutputDirectory="Always" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user