This commit is contained in:
Vladimir Moushkov
2025-10-15 10:03:56 +03:00
parent ea8226120c
commit ea1106ce7c
276 changed files with 21674 additions and 934 deletions

View File

@@ -0,0 +1,7 @@
<html>
<body>
<ul>
<li><a href="/materialy/uyazvimosti/bulletin-legacy.json.zip" title="Legacy Bulletin">Legacy Bulletin</a></li>
</ul>
</body>
</html>

View File

@@ -3,5 +3,8 @@
<ul>
<li><a href="/materialy/uyazvimosti/bulletin-sample.json.zip" title="Bulletin Sample">Bulletin Sample</a></li>
</ul>
<div class="pagination">
<a class="pagination__link" href="/materialy/uyazvimosti/?PAGEN_1=2">2</a>
</div>
</body>
</html>

View File

@@ -3,11 +3,57 @@
"advisoryKey": "BDU:2025-01001",
"affectedPackages": [
{
"type": "vendor",
"identifier": "SampleSCADA <= 4.2",
"platform": null,
"versionRanges": [],
"normalizedVersions": [],
"type": "ics-vendor",
"identifier": "SampleVendor SampleGateway",
"platform": "Energy, ICS",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": "2.0",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": false,
"nevra": null,
"semVer": {
"constraintExpression": ">= 2.0",
"exactValue": null,
"fixed": null,
"fixedInclusive": false,
"introduced": "2.0",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": false,
"style": "greaterThanOrEqual"
},
"vendorExtensions": null
},
"provenance": {
"source": "ru-nkcki",
"kind": "package-range",
"value": "SampleVendor SampleGateway >= 2.0 All platforms",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": ">= 2.0",
"rangeKind": "semver"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "gte",
"min": "2.0",
"minInclusive": true,
"max": null,
"maxInclusive": null,
"value": null,
"notes": "SampleVendor SampleGateway >= 2.0 All platforms"
}
],
"statuses": [
{
"provenance": {
@@ -15,7 +61,7 @@
"kind": "package-status",
"value": "patch_available",
"decisionReason": null,
"recordedAt": "2025-09-22T00:00:00+00:00",
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
@@ -27,9 +73,89 @@
{
"source": "ru-nkcki",
"kind": "package",
"value": "SampleSCADA <= 4.2",
"value": "SampleVendor SampleGateway >= 2.0 All platforms",
"decisionReason": null,
"recordedAt": "2025-09-22T00:00:00+00:00",
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
},
{
"type": "ics-vendor",
"identifier": "SampleVendor SampleSCADA",
"platform": "Energy, ICS",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": "4.2",
"primitives": {
"evr": null,
"hasVendorExtensions": false,
"nevra": null,
"semVer": {
"constraintExpression": "<= 4.2",
"exactValue": null,
"fixed": null,
"fixedInclusive": false,
"introduced": null,
"introducedInclusive": true,
"lastAffected": "4.2",
"lastAffectedInclusive": true,
"style": "lessThanOrEqual"
},
"vendorExtensions": null
},
"provenance": {
"source": "ru-nkcki",
"kind": "package-range",
"value": "SampleVendor SampleSCADA <= 4.2",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "<= 4.2",
"rangeKind": "semver"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "lte",
"min": null,
"minInclusive": null,
"max": "4.2",
"maxInclusive": true,
"value": null,
"notes": "SampleVendor SampleSCADA <= 4.2"
}
],
"statuses": [
{
"provenance": {
"source": "ru-nkcki",
"kind": "package-status",
"value": "patch_available",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "fixed"
}
],
"provenance": [
{
"source": "ru-nkcki",
"kind": "package",
"value": "SampleVendor SampleSCADA <= 4.2",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
@@ -51,13 +177,29 @@
"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",
"recordedAt": "2025-10-12T00:01: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"
},
{
"baseScore": 6.4,
"baseSeverity": "medium",
"provenance": {
"source": "ru-nkcki",
"kind": "cvss",
"value": "CVSS:4.0/AV:N/AC:H/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:4.0/AV:N/AC:H/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H",
"version": "4.0"
}
],
"exploitKnown": true,
@@ -69,7 +211,7 @@
"kind": "advisory",
"value": "BDU:2025-01001",
"decisionReason": null,
"recordedAt": "2025-09-22T00:00:00+00:00",
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"advisory"
]
@@ -84,7 +226,7 @@
"kind": "reference",
"value": "https://bdu.fstec.ru/vul/2025-01001",
"decisionReason": null,
"recordedAt": "2025-09-22T00:00:00+00:00",
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
@@ -100,23 +242,7 @@
"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",
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
@@ -132,7 +258,7 @@
"kind": "reference",
"value": "https://cwe.mitre.org/data/definitions/321.html",
"decisionReason": null,
"recordedAt": "2025-09-22T00:00:00+00:00",
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
@@ -148,7 +274,7 @@
"kind": "reference",
"value": "https://vendor.example/advisories/sample-scada",
"decisionReason": null,
"recordedAt": "2025-09-22T00:00:00+00:00",
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
@@ -161,5 +287,209 @@
"severity": "critical",
"summary": "Authenticated RCE in Sample SCADA",
"title": "Authenticated RCE in Sample SCADA"
},
{
"advisoryKey": "BDU:2024-00011",
"affectedPackages": [
{
"type": "cpe",
"identifier": "LegacyPanel",
"platform": "Software",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": "2.5",
"primitives": {
"evr": null,
"hasVendorExtensions": false,
"nevra": null,
"semVer": {
"constraintExpression": "<= 2.5",
"exactValue": null,
"fixed": null,
"fixedInclusive": false,
"introduced": null,
"introducedInclusive": true,
"lastAffected": "2.5",
"lastAffectedInclusive": true,
"style": "lessThanOrEqual"
},
"vendorExtensions": null
},
"provenance": {
"source": "ru-nkcki",
"kind": "package-range",
"value": "LegacyPanel 1.0 - 2.5",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "<= 2.5",
"rangeKind": "semver"
},
{
"fixedVersion": null,
"introducedVersion": "1.0",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": false,
"nevra": null,
"semVer": {
"constraintExpression": ">= 1.0",
"exactValue": null,
"fixed": null,
"fixedInclusive": false,
"introduced": "1.0",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": false,
"style": "greaterThanOrEqual"
},
"vendorExtensions": null
},
"provenance": {
"source": "ru-nkcki",
"kind": "package-range",
"value": "LegacyPanel 1.0 - 2.5",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": ">= 1.0",
"rangeKind": "semver"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "gte",
"min": "1.0",
"minInclusive": true,
"max": null,
"maxInclusive": null,
"value": null,
"notes": "LegacyPanel 1.0 - 2.5"
},
{
"scheme": "semver",
"type": "lte",
"min": null,
"minInclusive": null,
"max": "2.5",
"maxInclusive": true,
"value": null,
"notes": "LegacyPanel 1.0 - 2.5"
}
],
"statuses": [
{
"provenance": {
"source": "ru-nkcki",
"kind": "package-status",
"value": "affected",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "affected"
}
],
"provenance": [
{
"source": "ru-nkcki",
"kind": "package",
"value": "LegacyPanel 1.0 - 2.5",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
],
"aliases": [
"BDU:2024-00011"
],
"credits": [],
"cvssMetrics": [
{
"baseScore": 8.8,
"baseSeverity": "high",
"provenance": {
"source": "ru-nkcki",
"kind": "cvss",
"value": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"exploitKnown": true,
"language": "ru",
"modified": "2024-08-02T00:00:00+00:00",
"provenance": [
{
"source": "ru-nkcki",
"kind": "advisory",
"value": "BDU:2024-00011",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2024-08-01T00:00:00+00:00",
"references": [
{
"kind": "details",
"provenance": {
"source": "ru-nkcki",
"kind": "reference",
"value": "https://bdu.fstec.ru/vul/2024-00011",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "bdu",
"summary": null,
"url": "https://bdu.fstec.ru/vul/2024-00011"
},
{
"kind": "details",
"provenance": {
"source": "ru-nkcki",
"kind": "reference",
"value": "https://cert.gov.ru/materialy/uyazvimosti/2024-00011",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "ru-nkcki",
"summary": null,
"url": "https://cert.gov.ru/materialy/uyazvimosti/2024-00011"
}
],
"severity": "high",
"summary": "Legacy panel overflow",
"title": "Legacy panel overflow"
}
]

View File

@@ -33,7 +33,9 @@ namespace StellaOps.Feedser.Source.Ru.Nkcki.Tests;
public sealed class RuNkckiConnectorTests : IAsyncLifetime
{
private static readonly Uri ListingUri = new("https://cert.gov.ru/materialy/uyazvimosti/");
private static readonly Uri ListingPage2Uri = new("https://cert.gov.ru/materialy/uyazvimosti/?PAGEN_1=2");
private static readonly Uri BulletinUri = new("https://cert.gov.ru/materialy/uyazvimosti/bulletin-sample.json.zip");
private static readonly Uri LegacyBulletinUri = new("https://cert.gov.ru/materialy/uyazvimosti/bulletin-legacy.json.zip");
private readonly MongoIntegrationFixture _fixture;
private readonly FakeTimeProvider _timeProvider;
@@ -60,13 +62,13 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
Assert.Single(advisories);
Assert.Equal(2, advisories.Count);
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);
var document = await documentStore.FindBySourceAndUriAsync(RuNkckiConnectorPlugin.SourceName, "https://cert.gov.ru/materialy/uyazvimosti/2025-01001", CancellationToken.None);
Assert.NotNull(document);
Assert.Equal(DocumentStatuses.Mapped, document!.Status);
@@ -85,18 +87,25 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime
var connector = provider.GetRequiredService<RuNkckiConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
_handler.Clear();
_handler.AddResponse(ListingUri, () => new HttpResponseMessage(HttpStatusCode.InternalServerError)
for (var i = 0; i < 3; i++)
{
Content = new StringContent("error", Encoding.UTF8, "text/plain"),
});
_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);
Assert.Equal(2, before.Count);
await connector.FetchAsync(provider, CancellationToken.None);
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(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));
@@ -106,18 +115,7 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime
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}");
}
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
_handler.Clear();
@@ -138,7 +136,9 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime
options.BaseAddress = new Uri("https://cert.gov.ru/");
options.ListingPath = "/materialy/uyazvimosti/";
options.MaxBulletinsPerFetch = 2;
options.MaxListingPagesPerFetch = 2;
options.MaxVulnerabilitiesPerFetch = 50;
options.ListingCacheDuration = TimeSpan.Zero;
var cacheRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", _fixture.Database.DatabaseNamespace.DatabaseName);
Directory.CreateDirectory(cacheRoot);
options.CacheDirectory = Path.Combine(cacheRoot, "ru-nkcki");
@@ -150,23 +150,10 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime
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;
}
var provider = services.BuildServiceProvider();
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
await bootstrapper.InitializeAsync(CancellationToken.None);
return provider;
}
private void SeedListingAndBulletin()
@@ -174,6 +161,9 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime
var listingHtml = ReadFixture("listing.html");
_handler.AddTextResponse(ListingUri, listingHtml, "text/html");
var listingPage2Html = ReadFixture("listing-page2.html");
_handler.AddTextResponse(ListingPage2Uri, listingPage2Html, "text/html");
var bulletinBytes = ReadBulletinFixture("bulletin-sample.json.zip");
_handler.AddResponse(BulletinUri, () =>
{
@@ -185,6 +175,18 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime
response.Content.Headers.LastModified = new DateTimeOffset(2025, 9, 22, 0, 0, 0, TimeSpan.Zero);
return response;
});
var legacyBytes = ReadBulletinFixture("bulletin-legacy.json.zip");
_handler.AddResponse(LegacyBulletinUri, () =>
{
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ByteArrayContent(legacyBytes),
};
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
response.Content.Headers.LastModified = new DateTimeOffset(2024, 8, 2, 0, 0, 0, TimeSpan.Zero);
return response;
});
}
private static bool IsEmptyArray(BsonDocument document, string field)

View File

@@ -18,14 +18,24 @@ public sealed class RuNkckiJsonParserTests
"patch_available": true,
"description": "Test description",
"cwe": {"cwe_number": 79, "cwe_description": "Cross-site scripting"},
"product_category": "Web",
"mitigation": "Apply update",
"vulnerable_software": {"software_text": "ExampleApp 1.0", "cpe": false},
"cvss": {"cvss_score": 8.8, "cvss_vector": "AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", "cvss_score_v4": 5.5, "cvss_vector_v4": "AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H"},
"product_category": ["Web", "CMS"],
"mitigation": ["Apply update", "Review configuration"],
"vulnerable_software": {
"software_text": "ExampleCMS <= 1.0",
"software": [{"vendor": "Example", "name": "ExampleCMS", "version": "<= 1.0"}],
"cpe": false
},
"cvss": {
"cvss_score": 8.8,
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
"cvss_score_v4": 5.5,
"cvss_vector_v4": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H"
},
"impact": "ACE",
"method_of_exploitation": "Special request",
"user_interaction": false,
"urls": ["https://example.com/advisory", "https://cert.gov.ru/materialy/uyazvimosti/2025-00001"]
"urls": ["https://example.com/advisory", {"url": "https://cert.gov.ru/materialy/uyazvimosti/2025-00001"}],
"tags": ["cms"]
}
""";
@@ -35,9 +45,16 @@ public sealed class RuNkckiJsonParserTests
Assert.Equal("BDU:2025-00001", dto.FstecId);
Assert.Equal("CVE-2025-0001", dto.MitreId);
Assert.Equal(8.8, dto.CvssScore);
Assert.Equal("AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", dto.CvssVector);
Assert.Equal("CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", dto.CvssVector);
Assert.True(dto.PatchAvailable);
Assert.Equal(79, dto.Cwe?.Number);
Assert.Contains("Web", dto.ProductCategories);
Assert.Contains("CMS", dto.ProductCategories);
Assert.Single(dto.VulnerableSoftwareEntries);
var entry = dto.VulnerableSoftwareEntries[0];
Assert.Equal("Example ExampleCMS", entry.Identifier);
Assert.Contains("<= 1.0", entry.RangeExpressions);
Assert.Equal(2, dto.Urls.Length);
Assert.Contains("cms", dto.Tags);
}
}

View File

@@ -14,6 +14,12 @@ public sealed class RuNkckiMapperTests
[Fact]
public void Map_ConstructsCanonicalAdvisory()
{
var softwareEntries = ImmutableArray.Create(
new RuNkckiSoftwareEntry(
"SampleVendor SampleSCADA",
"SampleVendor SampleSCADA <= 4.2",
ImmutableArray.Create("<= 4.2")));
var dto = new RuNkckiVulnerabilityDto(
FstecId: "BDU:2025-00001",
MitreId: "CVE-2025-0001",
@@ -23,18 +29,20 @@ public sealed class RuNkckiMapperTests
PatchAvailable: true,
Description: "Test NKCKI vulnerability",
Cwe: new RuNkckiCweDto(79, "Cross-site scripting"),
ProductCategory: "Web",
ProductCategories: ImmutableArray.Create("ICS", "Automation"),
Mitigation: "Apply update",
VulnerableSoftwareText: "ExampleApp <= 1.0",
VulnerableSoftwareText: null,
VulnerableSoftwareHasCpe: false,
VulnerableSoftwareEntries: softwareEntries,
CvssScore: 8.8,
CvssVector: "AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
CvssScoreV4: null,
CvssVectorV4: null,
CvssVector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
CvssScoreV4: 6.4,
CvssVectorV4: "CVSS:4.0/AV:N/AC:H/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H",
Impact: "ACE",
MethodOfExploitation: "Special request",
UserInteraction: false,
Urls: ImmutableArray.Create("https://example.com/advisory"));
Urls: ImmutableArray.Create("https://example.com/advisory", "https://cert.gov.ru/materialy/uyazvimosti/2025-00001"),
Tags: ImmutableArray.Create("ics"));
var document = new DocumentRecord(
Guid.NewGuid(),
@@ -62,7 +70,12 @@ public sealed class RuNkckiMapperTests
Assert.Equal("critical", advisory.Severity);
Assert.True(advisory.ExploitKnown);
Assert.Single(advisory.AffectedPackages);
Assert.Single(advisory.CvssMetrics);
var package = advisory.AffectedPackages[0];
Assert.Equal(AffectedPackageTypes.IcsVendor, package.Type);
Assert.Single(package.NormalizedVersions);
Assert.Equal(2, advisory.CvssMetrics.Length);
Assert.Contains(advisory.CvssMetrics, metric => metric.Version == "4.0");
Assert.Equal("critical", advisory.Severity);
Assert.Contains(advisory.References, reference => reference.Url.Contains("example.com", StringComparison.OrdinalIgnoreCase));
}
}