Add NKCKI severity smoothing, fixtures, and regression harness
This commit is contained in:
		
										
											Binary file not shown.
										
									
								
							| @@ -0,0 +1,7 @@ | ||||
| <html> | ||||
|   <body> | ||||
|     <ul> | ||||
|       <li><a href="/materialy/uyazvimosti/bulletin-sample.json.zip" title="Bulletin Sample">Bulletin Sample</a></li> | ||||
|     </ul> | ||||
|   </body> | ||||
| </html> | ||||
| @@ -0,0 +1,165 @@ | ||||
| [ | ||||
|   { | ||||
|     "advisoryKey": "BDU:2025-01001", | ||||
|     "affectedPackages": [ | ||||
|       { | ||||
|         "type": "vendor", | ||||
|         "identifier": "SampleSCADA <= 4.2", | ||||
|         "platform": null, | ||||
|         "versionRanges": [], | ||||
|         "normalizedVersions": [], | ||||
|         "statuses": [ | ||||
|           { | ||||
|             "provenance": { | ||||
|               "source": "ru-nkcki", | ||||
|               "kind": "package-status", | ||||
|               "value": "patch_available", | ||||
|               "decisionReason": null, | ||||
|               "recordedAt": "2025-09-22T00:00:00+00:00", | ||||
|               "fieldMask": [ | ||||
|                 "affectedpackages[].statuses[]" | ||||
|               ] | ||||
|             }, | ||||
|             "status": "fixed" | ||||
|           } | ||||
|         ], | ||||
|         "provenance": [ | ||||
|           { | ||||
|             "source": "ru-nkcki", | ||||
|             "kind": "package", | ||||
|             "value": "SampleSCADA <= 4.2", | ||||
|             "decisionReason": null, | ||||
|             "recordedAt": "2025-09-22T00:00:00+00:00", | ||||
|             "fieldMask": [ | ||||
|               "affectedpackages[]" | ||||
|             ] | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     ], | ||||
|     "aliases": [ | ||||
|       "BDU:2025-01001", | ||||
|       "CVE-2025-0101" | ||||
|     ], | ||||
|     "credits": [], | ||||
|     "cvssMetrics": [ | ||||
|       { | ||||
|         "baseScore": 8.5, | ||||
|         "baseSeverity": "high", | ||||
|         "provenance": { | ||||
|           "source": "ru-nkcki", | ||||
|           "kind": "cvss", | ||||
|           "value": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-09-22T00:00:00+00:00", | ||||
|           "fieldMask": [ | ||||
|             "cvssmetrics[]" | ||||
|           ] | ||||
|         }, | ||||
|         "vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H", | ||||
|         "version": "3.1" | ||||
|       } | ||||
|     ], | ||||
|     "exploitKnown": true, | ||||
|     "language": "ru", | ||||
|     "modified": "2025-09-22T00:00:00+00:00", | ||||
|     "provenance": [ | ||||
|       { | ||||
|         "source": "ru-nkcki", | ||||
|         "kind": "advisory", | ||||
|         "value": "BDU:2025-01001", | ||||
|         "decisionReason": null, | ||||
|         "recordedAt": "2025-09-22T00:00:00+00:00", | ||||
|         "fieldMask": [ | ||||
|           "advisory" | ||||
|         ] | ||||
|       } | ||||
|     ], | ||||
|     "published": "2025-09-20T00:00:00+00:00", | ||||
|     "references": [ | ||||
|       { | ||||
|         "kind": "details", | ||||
|         "provenance": { | ||||
|           "source": "ru-nkcki", | ||||
|           "kind": "reference", | ||||
|           "value": "https://bdu.fstec.ru/vul/2025-01001", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-09-22T00:00:00+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
|         }, | ||||
|         "sourceTag": "bdu", | ||||
|         "summary": null, | ||||
|         "url": "https://bdu.fstec.ru/vul/2025-01001" | ||||
|       }, | ||||
|       { | ||||
|         "kind": "details", | ||||
|         "provenance": { | ||||
|           "source": "ru-nkcki", | ||||
|           "kind": "reference", | ||||
|           "value": "https://cert.gov.ru/materialy/uyazvimosti/2025-01001", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-09-22T00:00:00+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
|         }, | ||||
|         "sourceTag": null, | ||||
|         "summary": null, | ||||
|         "url": "https://cert.gov.ru/materialy/uyazvimosti/2025-01001" | ||||
|       }, | ||||
|       { | ||||
|         "kind": "details", | ||||
|         "provenance": { | ||||
|           "source": "ru-nkcki", | ||||
|           "kind": "reference", | ||||
|           "value": "https://cert.gov.ru/materialy/uyazvimosti/2025-01001", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-09-22T00:00:00+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
|         }, | ||||
|         "sourceTag": "ru-nkcki", | ||||
|         "summary": null, | ||||
|         "url": "https://cert.gov.ru/materialy/uyazvimosti/2025-01001" | ||||
|       }, | ||||
|       { | ||||
|         "kind": "cwe", | ||||
|         "provenance": { | ||||
|           "source": "ru-nkcki", | ||||
|           "kind": "reference", | ||||
|           "value": "https://cwe.mitre.org/data/definitions/321.html", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-09-22T00:00:00+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
|         }, | ||||
|         "sourceTag": "cwe", | ||||
|         "summary": "Use of Hard-coded Cryptographic Key", | ||||
|         "url": "https://cwe.mitre.org/data/definitions/321.html" | ||||
|       }, | ||||
|       { | ||||
|         "kind": "external", | ||||
|         "provenance": { | ||||
|           "source": "ru-nkcki", | ||||
|           "kind": "reference", | ||||
|           "value": "https://vendor.example/advisories/sample-scada", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-09-22T00:00:00+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
|         }, | ||||
|         "sourceTag": null, | ||||
|         "summary": null, | ||||
|         "url": "https://vendor.example/advisories/sample-scada" | ||||
|       } | ||||
|     ], | ||||
|     "severity": "critical", | ||||
|     "summary": "Authenticated RCE in Sample SCADA", | ||||
|     "title": "Authenticated RCE in Sample SCADA" | ||||
|   } | ||||
| ] | ||||
| @@ -0,0 +1,289 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using System.Net; | ||||
| using System.Net.Http; | ||||
| using System.Net.Http.Headers; | ||||
| using System.Text; | ||||
| 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; | ||||
| using StellaOps.Feedser.Source.Common.Http; | ||||
| using StellaOps.Feedser.Source.Common.Testing; | ||||
| using StellaOps.Feedser.Source.Ru.Nkcki; | ||||
| using StellaOps.Feedser.Source.Ru.Nkcki.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.Models; | ||||
| using MongoDB.Driver; | ||||
| using Xunit; | ||||
|  | ||||
| namespace StellaOps.Feedser.Source.Ru.Nkcki.Tests; | ||||
|  | ||||
| [Collection("mongo-fixture")] | ||||
| public sealed class RuNkckiConnectorTests : IAsyncLifetime | ||||
| { | ||||
|     private static readonly Uri ListingUri = new("https://cert.gov.ru/materialy/uyazvimosti/"); | ||||
|     private static readonly Uri BulletinUri = new("https://cert.gov.ru/materialy/uyazvimosti/bulletin-sample.json.zip"); | ||||
|  | ||||
|     private readonly MongoIntegrationFixture _fixture; | ||||
|     private readonly FakeTimeProvider _timeProvider; | ||||
|     private readonly CannedHttpMessageHandler _handler; | ||||
|  | ||||
|     public RuNkckiConnectorTests(MongoIntegrationFixture fixture) | ||||
|     { | ||||
|         _fixture = fixture; | ||||
|         _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero)); | ||||
|         _handler = new CannedHttpMessageHandler(); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task FetchParseMap_ProducesExpectedSnapshot() | ||||
|     { | ||||
|         await using var provider = await BuildServiceProviderAsync(); | ||||
|         SeedListingAndBulletin(); | ||||
|  | ||||
|         var connector = provider.GetRequiredService<RuNkckiConnector>(); | ||||
|         await connector.FetchAsync(provider, CancellationToken.None); | ||||
|         _timeProvider.Advance(TimeSpan.FromMinutes(1)); | ||||
|         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.Single(advisories); | ||||
|  | ||||
|         var snapshot = SnapshotSerializer.ToSnapshot(advisories); | ||||
|         WriteOrAssertSnapshot(snapshot, "nkcki-advisories.snapshot.json"); | ||||
|  | ||||
|         var documentStore = provider.GetRequiredService<IDocumentStore>(); | ||||
|         var document = await documentStore.FindBySourceAndUriAsync(RuNkckiConnectorPlugin.SourceName, "https://cert.gov.ru/materialy/uyazvimosti/BDU:2025-01001", CancellationToken.None); | ||||
|         Assert.NotNull(document); | ||||
|         Assert.Equal(DocumentStatuses.Mapped, document!.Status); | ||||
|  | ||||
|         var stateRepository = provider.GetRequiredService<ISourceStateRepository>(); | ||||
|         var state = await stateRepository.TryGetAsync(RuNkckiConnectorPlugin.SourceName, CancellationToken.None); | ||||
|         Assert.NotNull(state); | ||||
|         Assert.True(IsEmptyArray(state!.Cursor, "pendingDocuments")); | ||||
|         Assert.True(IsEmptyArray(state.Cursor, "pendingMappings")); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task Fetch_ReusesCachedBulletinWhenListingFails() | ||||
|     { | ||||
|         await using var provider = await BuildServiceProviderAsync(); | ||||
|         SeedListingAndBulletin(); | ||||
|  | ||||
|         var connector = provider.GetRequiredService<RuNkckiConnector>(); | ||||
|         await connector.FetchAsync(provider, CancellationToken.None); | ||||
|  | ||||
|         _handler.Clear(); | ||||
|         _handler.AddResponse(ListingUri, () => new HttpResponseMessage(HttpStatusCode.InternalServerError) | ||||
|         { | ||||
|             Content = new StringContent("error", Encoding.UTF8, "text/plain"), | ||||
|         }); | ||||
|  | ||||
|         var advisoryStore = provider.GetRequiredService<IAdvisoryStore>(); | ||||
|         var before = await advisoryStore.GetRecentAsync(10, CancellationToken.None); | ||||
|         Assert.NotEmpty(before); | ||||
|  | ||||
|         await connector.FetchAsync(provider, CancellationToken.None); | ||||
|  | ||||
|         var after = await advisoryStore.GetRecentAsync(10, CancellationToken.None); | ||||
|         Assert.Equal(before.Select(advisory => advisory.AdvisoryKey).OrderBy(static key => key), after.Select(advisory => advisory.AdvisoryKey).OrderBy(static key => key)); | ||||
|  | ||||
|         _handler.AssertNoPendingResponses(); | ||||
|     } | ||||
|  | ||||
|     private async Task<ServiceProvider> BuildServiceProviderAsync() | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); | ||||
|         } | ||||
|         catch (MongoConnectionException ex) | ||||
|         { | ||||
|             Assert.Skip($"Mongo runner unavailable: {ex.Message}"); | ||||
|         } | ||||
|         catch (TimeoutException ex) | ||||
|         { | ||||
|             Assert.Skip($"Mongo runner unavailable: {ex.Message}"); | ||||
|         } | ||||
|  | ||||
|         _handler.Clear(); | ||||
|  | ||||
|         var services = new ServiceCollection(); | ||||
|         services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); | ||||
|         services.AddSingleton<TimeProvider>(_timeProvider); | ||||
|  | ||||
|         services.AddMongoStorage(options => | ||||
|         { | ||||
|             options.ConnectionString = _fixture.Runner.ConnectionString; | ||||
|             options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; | ||||
|             options.CommandTimeout = TimeSpan.FromSeconds(5); | ||||
|         }); | ||||
|  | ||||
|         services.AddSourceCommon(); | ||||
|         services.AddRuNkckiConnector(options => | ||||
|         { | ||||
|             options.BaseAddress = new Uri("https://cert.gov.ru/"); | ||||
|             options.ListingPath = "/materialy/uyazvimosti/"; | ||||
|             options.MaxBulletinsPerFetch = 2; | ||||
|             options.MaxVulnerabilitiesPerFetch = 50; | ||||
|             var cacheRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", _fixture.Database.DatabaseNamespace.DatabaseName); | ||||
|             Directory.CreateDirectory(cacheRoot); | ||||
|             options.CacheDirectory = Path.Combine(cacheRoot, "ru-nkcki"); | ||||
|             options.RequestDelay = TimeSpan.Zero; | ||||
|         }); | ||||
|  | ||||
|         services.Configure<HttpClientFactoryOptions>(RuNkckiOptions.HttpClientName, builderOptions => | ||||
|         { | ||||
|             builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler); | ||||
|         }); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var provider = services.BuildServiceProvider(); | ||||
|             var bootstrapper = provider.GetRequiredService<MongoBootstrapper>(); | ||||
|             await bootstrapper.InitializeAsync(CancellationToken.None); | ||||
|             return provider; | ||||
|         } | ||||
|         catch (MongoConnectionException ex) | ||||
|         { | ||||
|             Assert.Skip($"Mongo runner unavailable: {ex.Message}"); | ||||
|             throw; // Unreachable | ||||
|         } | ||||
|         catch (TimeoutException ex) | ||||
|         { | ||||
|             Assert.Skip($"Mongo runner unavailable: {ex.Message}"); | ||||
|             throw; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void SeedListingAndBulletin() | ||||
|     { | ||||
|         var listingHtml = ReadFixture("listing.html"); | ||||
|         _handler.AddTextResponse(ListingUri, listingHtml, "text/html"); | ||||
|  | ||||
|         var bulletinBytes = ReadBulletinFixture("bulletin-sample.json.zip"); | ||||
|         _handler.AddResponse(BulletinUri, () => | ||||
|         { | ||||
|             var response = new HttpResponseMessage(HttpStatusCode.OK) | ||||
|             { | ||||
|                 Content = new ByteArrayContent(bulletinBytes), | ||||
|             }; | ||||
|             response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip"); | ||||
|             response.Content.Headers.LastModified = new DateTimeOffset(2025, 9, 22, 0, 0, 0, TimeSpan.Zero); | ||||
|             return response; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private static bool IsEmptyArray(BsonDocument document, string field) | ||||
|     { | ||||
|         if (!document.TryGetValue(field, out var value) || value is not BsonArray array) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return array.Count == 0; | ||||
|     } | ||||
|  | ||||
|     private static string ReadFixture(string filename) | ||||
|     { | ||||
|         var path = Path.Combine("Fixtures", filename); | ||||
|         var resolved = ResolveFixturePath(path); | ||||
|         return File.ReadAllText(resolved); | ||||
|     } | ||||
|  | ||||
|     private static byte[] ReadBulletinFixture(string filename) | ||||
|     { | ||||
|         var path = Path.Combine("Fixtures", filename); | ||||
|         var resolved = ResolveFixturePath(path); | ||||
|         return File.ReadAllBytes(resolved); | ||||
|     } | ||||
|  | ||||
|     private static string ResolveFixturePath(string relativePath) | ||||
|     { | ||||
|         var projectRoot = GetProjectRoot(); | ||||
|         var projectPath = Path.Combine(projectRoot, relativePath); | ||||
|         if (File.Exists(projectPath)) | ||||
|         { | ||||
|             return projectPath; | ||||
|         } | ||||
|  | ||||
|         var binaryPath = Path.Combine(AppContext.BaseDirectory, relativePath); | ||||
|         if (File.Exists(binaryPath)) | ||||
|         { | ||||
|             return Path.GetFullPath(binaryPath); | ||||
|         } | ||||
|  | ||||
|         throw new FileNotFoundException($"Fixture not found: {relativePath}"); | ||||
|     } | ||||
|  | ||||
|     private static void WriteOrAssertSnapshot(string snapshot, string filename) | ||||
|     { | ||||
|         if (ShouldUpdateFixtures()) | ||||
|         { | ||||
|             var path = GetWritableFixturePath(filename); | ||||
|             Directory.CreateDirectory(Path.GetDirectoryName(path)!); | ||||
|             File.WriteAllText(path, snapshot); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         var expectedPath = ResolveFixturePath(Path.Combine("Fixtures", filename)); | ||||
|         if (!File.Exists(expectedPath)) | ||||
|         { | ||||
|             throw new FileNotFoundException($"Expected snapshot missing: {expectedPath}. Set UPDATE_NKCKI_FIXTURES=1 to generate."); | ||||
|         } | ||||
|  | ||||
|         var expected = File.ReadAllText(expectedPath); | ||||
|         Assert.Equal(Normalize(expected), Normalize(snapshot)); | ||||
|     } | ||||
|  | ||||
|     private static string GetWritableFixturePath(string filename) | ||||
|     { | ||||
|         var projectRoot = GetProjectRoot(); | ||||
|         return Path.Combine(projectRoot, "Fixtures", filename); | ||||
|     } | ||||
|  | ||||
|     private static bool ShouldUpdateFixtures() | ||||
|     { | ||||
|         var value = Environment.GetEnvironmentVariable("UPDATE_NKCKI_FIXTURES"); | ||||
|         return string.Equals(value, "1", StringComparison.OrdinalIgnoreCase) | ||||
|             || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); | ||||
|     } | ||||
|  | ||||
|     private static string Normalize(string text) | ||||
|         => text.Replace("\r\n", "\n", StringComparison.Ordinal); | ||||
|  | ||||
|     private static string GetProjectRoot() | ||||
|     { | ||||
|         var current = AppContext.BaseDirectory; | ||||
|         while (!string.IsNullOrEmpty(current)) | ||||
|         { | ||||
|             var candidate = Path.Combine(current, "StellaOps.Feedser.Source.Ru.Nkcki.Tests.csproj"); | ||||
|             if (File.Exists(candidate)) | ||||
|             { | ||||
|                 return current; | ||||
|             } | ||||
|  | ||||
|             current = Path.GetDirectoryName(current); | ||||
|         } | ||||
|  | ||||
|         throw new InvalidOperationException("Unable to locate project root for Ru.Nkcki tests."); | ||||
|     } | ||||
|  | ||||
|     public Task InitializeAsync() => Task.CompletedTask; | ||||
|  | ||||
|     public async Task DisposeAsync() | ||||
|         => await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); | ||||
| } | ||||
| @@ -0,0 +1,68 @@ | ||||
| using System.Collections.Immutable; | ||||
| using MongoDB.Bson; | ||||
| using StellaOps.Feedser.Source.Common; | ||||
| using StellaOps.Feedser.Models; | ||||
| using StellaOps.Feedser.Source.Ru.Nkcki.Internal; | ||||
| using StellaOps.Feedser.Storage.Mongo.Documents; | ||||
| using Xunit; | ||||
| using System.Reflection; | ||||
|  | ||||
| namespace StellaOps.Feedser.Source.Ru.Nkcki.Tests; | ||||
|  | ||||
| public sealed class RuNkckiMapperTests | ||||
| { | ||||
|     [Fact] | ||||
|     public void Map_ConstructsCanonicalAdvisory() | ||||
|     { | ||||
|         var dto = new RuNkckiVulnerabilityDto( | ||||
|             FstecId: "BDU:2025-00001", | ||||
|             MitreId: "CVE-2025-0001", | ||||
|             DatePublished: new DateTimeOffset(2025, 9, 1, 0, 0, 0, TimeSpan.Zero), | ||||
|             DateUpdated: new DateTimeOffset(2025, 9, 2, 0, 0, 0, TimeSpan.Zero), | ||||
|             CvssRating: "КРИТИЧЕСКИЙ", | ||||
|             PatchAvailable: true, | ||||
|             Description: "Test NKCKI vulnerability", | ||||
|             Cwe: new RuNkckiCweDto(79, "Cross-site scripting"), | ||||
|             ProductCategory: "Web", | ||||
|             Mitigation: "Apply update", | ||||
|             VulnerableSoftwareText: "ExampleApp <= 1.0", | ||||
|             VulnerableSoftwareHasCpe: false, | ||||
|             CvssScore: 8.8, | ||||
|             CvssVector: "AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", | ||||
|             CvssScoreV4: null, | ||||
|             CvssVectorV4: null, | ||||
|             Impact: "ACE", | ||||
|             MethodOfExploitation: "Special request", | ||||
|             UserInteraction: false, | ||||
|             Urls: ImmutableArray.Create("https://example.com/advisory")); | ||||
|  | ||||
|         var document = new DocumentRecord( | ||||
|             Guid.NewGuid(), | ||||
|             RuNkckiConnectorPlugin.SourceName, | ||||
|             "https://cert.gov.ru/materialy/uyazvimosti/2025-00001", | ||||
|             DateTimeOffset.UtcNow, | ||||
|             "abc", | ||||
|             DocumentStatuses.PendingMap, | ||||
|             "application/json", | ||||
|             null, | ||||
|             null, | ||||
|             null, | ||||
|             dto.DateUpdated, | ||||
|             ObjectId.GenerateNewId()); | ||||
|  | ||||
|         Assert.Equal("КРИТИЧЕСКИЙ", dto.CvssRating); | ||||
|         var normalizeSeverity = typeof(RuNkckiMapper).GetMethod("NormalizeSeverity", BindingFlags.NonPublic | BindingFlags.Static)!; | ||||
|         var ratingSeverity = (string?)normalizeSeverity.Invoke(null, new object?[] { dto.CvssRating }); | ||||
|         Assert.Equal("critical", ratingSeverity); | ||||
|  | ||||
|         var advisory = RuNkckiMapper.Map(dto, document, dto.DateUpdated!.Value); | ||||
|  | ||||
|         Assert.Contains("BDU:2025-00001", advisory.Aliases); | ||||
|         Assert.Contains("CVE-2025-0001", advisory.Aliases); | ||||
|         Assert.Equal("critical", advisory.Severity); | ||||
|         Assert.True(advisory.ExploitKnown); | ||||
|         Assert.Single(advisory.AffectedPackages); | ||||
|         Assert.Single(advisory.CvssMetrics); | ||||
|         Assert.Contains(advisory.References, reference => reference.Url.Contains("example.com", StringComparison.OrdinalIgnoreCase)); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user