Resolve Concelier/Excititor merge conflicts
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
@@ -0,0 +1,10 @@
|
||||
<html>
|
||||
<body>
|
||||
<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>
|
||||
@@ -0,0 +1,495 @@
|
||||
[
|
||||
{
|
||||
"advisoryKey": "BDU:2025-01001",
|
||||
"affectedPackages": [
|
||||
{
|
||||
"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": {
|
||||
"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 SampleGateway >= 2.0 All platforms",
|
||||
"decisionReason": null,
|
||||
"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[]"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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-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,
|
||||
"language": "ru",
|
||||
"modified": "2025-09-22T00:00:00+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"source": "ru-nkcki",
|
||||
"kind": "advisory",
|
||||
"value": "BDU:2025-01001",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:01: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-10-12T00:01: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-10-12T00:01: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-10-12T00:01: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-10-12T00:01: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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,291 @@
|
||||
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.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Ru.Nkcki;
|
||||
using StellaOps.Concelier.Connector.Ru.Nkcki.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
using MongoDB.Driver;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.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 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;
|
||||
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.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/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);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
_handler.Clear();
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
_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.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));
|
||||
|
||||
_handler.AssertNoPendingResponses();
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
|
||||
_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.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");
|
||||
options.RequestDelay = TimeSpan.Zero;
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(RuNkckiOptions.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 void SeedListingAndBulletin()
|
||||
{
|
||||
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, () =>
|
||||
{
|
||||
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;
|
||||
});
|
||||
|
||||
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)
|
||||
{
|
||||
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.Concelier.Connector.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,60 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Concelier.Connector.Ru.Nkcki.Internal;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ru.Nkcki.Tests;
|
||||
|
||||
public sealed class RuNkckiJsonParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_WellFormedEntry_ReturnsDto()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"vuln_id": {"MITRE": "CVE-2025-0001", "FSTEC": "BDU:2025-00001"},
|
||||
"date_published": "2025-09-01",
|
||||
"date_updated": "2025-09-02",
|
||||
"cvss_rating": "КРИТИЧЕСКИЙ",
|
||||
"patch_available": true,
|
||||
"description": "Test description",
|
||||
"cwe": {"cwe_number": 79, "cwe_description": "Cross-site scripting"},
|
||||
"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", {"url": "https://cert.gov.ru/materialy/uyazvimosti/2025-00001"}],
|
||||
"tags": ["cms"]
|
||||
}
|
||||
""";
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var dto = RuNkckiJsonParser.Parse(document.RootElement);
|
||||
|
||||
Assert.Equal("BDU:2025-00001", dto.FstecId);
|
||||
Assert.Equal("CVE-2025-0001", dto.MitreId);
|
||||
Assert.Equal(8.8, dto.CvssScore);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.Collections.Immutable;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Ru.Nkcki.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using Xunit;
|
||||
using System.Reflection;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ru.Nkcki.Tests;
|
||||
|
||||
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",
|
||||
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"),
|
||||
ProductCategories: ImmutableArray.Create("ICS", "Automation"),
|
||||
Mitigation: "Apply update",
|
||||
VulnerableSoftwareText: null,
|
||||
VulnerableSoftwareHasCpe: false,
|
||||
VulnerableSoftwareEntries: softwareEntries,
|
||||
CvssScore: 8.8,
|
||||
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", "https://cert.gov.ru/materialy/uyazvimosti/2025-00001"),
|
||||
Tags: ImmutableArray.Create("ics"));
|
||||
|
||||
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);
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Concelier.Normalization/StellaOps.Concelier.Normalization.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Concelier.Connector.Ru.Nkcki/StellaOps.Concelier.Connector.Ru.Nkcki.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user