Rename Concelier Source modules to Connector

This commit is contained in:
master
2025-10-18 20:11:18 +03:00
parent 89ede53cc3
commit 052da7a7d0
789 changed files with 1489 additions and 1489 deletions

View File

@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<vulnerabilities>
<vul>
<identifier>BDU:2025-00001</identifier>
<name>Множественные уязвимости криптопровайдера</name>
<description>Удалённый злоумышленник может вызвать отказ в обслуживании или получить доступ к данным.</description>
<solution>Установить обновление 8.2.19.116 защищённого комплекса.</solution>
<identify_date>01.12.2013</identify_date>
<severity>Высокий уровень опасности (базовая оценка CVSS 2.0 составляет 7,5)</severity>
<exploit_status>Существует в открытом доступе</exploit_status>
<fix_status>Уязвимость устранена</fix_status>
<vul_status>Подтверждена производителем</vul_status>
<vul_incident>0</vul_incident>
<cvss>
<vector score="7.5">AV:N/AC:L/Au:N/C:P/I:P/A:P</vector>
</cvss>
<cvss3>
<vector score="9.8">AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H</vector>
</cvss3>
<vulnerable_software>
<soft>
<vendor>ООО «1С-Софт»</vendor>
<name>1С:Предприятие</name>
<version>8.2.18.96</version>
<platform>Windows</platform>
<types>
<type>Прикладное ПО информационных систем</type>
</types>
</soft>
<soft>
<vendor>ООО «1С-Софт»</vendor>
<name>1С:Предприятие</name>
<version>8.2.19.116</version>
<platform>Не указана</platform>
<types>
<type>Прикладное ПО информационных систем</type>
</types>
</soft>
</vulnerable_software>
<environment>
<os>
<vendor>Microsoft Corp</vendor>
<name>Windows</name>
<version>-</version>
<platform>64-bit</platform>
</os>
<os>
<vendor>Microsoft Corp</vendor>
<name>Windows</name>
<version>-</version>
<platform>32-bit</platform>
</os>
</environment>
<cwes>
<cwe>
<identifier>CWE-310</identifier>
<name>Проблемы использования криптографии</name>
</cwe>
</cwes>
<sources>
https://advisories.example/BDU-2025-00001
http://mirror.example/ru-bdu/BDU-2025-00001
</sources>
<identifiers>
<identifier type="CVE" link="https://nvd.nist.gov/vuln/detail/CVE-2015-0206">CVE-2015-0206</identifier>
<identifier type="CVE" link="https://nvd.nist.gov/vuln/detail/CVE-2009-3555">CVE-2009-3555</identifier>
<identifier type="Positive Technologies Advisory" link="https://ptsecurity.com/PT-2015-0206">PT-2015-0206</identifier>
</identifiers>
<other>Язык разработки ПО С</other>
<vul_class>Уязвимость кода</vul_class>
<vul_state>Опубликована</vul_state>
</vul>
<vul>
<identifier>BDU:2025-00002</identifier>
<name>Уязвимость контроллера АСУ ТП</name>
<description>Локальный злоумышленник может повысить привилегии в контроллере.</description>
<solution>Производитель готовит обновление микропрограммы.</solution>
<identify_date>15.10.2024</identify_date>
<severity>Средний уровень опасности</severity>
<exploit_status>Данные уточняются</exploit_status>
<fix_status>Информация об устранении отсутствует</fix_status>
<vul_status>Потенциальная уязвимость</vul_status>
<vul_incident>2</vul_incident>
<cvss>
<vector score="6.5">AV:L/AC:H/Au:S/C:P/I:P/A:P</vector>
</cvss>
<vulnerable_software>
<soft>
<vendor>АО «Системы Управления»</vendor>
<name>SCADA Controller</name>
<version>1.0.0;1.0.1</version>
<platform>-</platform>
<types>
<type>ПО программно-аппаратного средства АСУ ТП</type>
</types>
</soft>
</vulnerable_software>
<cwes>
<cwe>
<identifier>CWE-269</identifier>
<name>Неправильное управление привилегиями</name>
</cwe>
<cwe>
<identifier>CWE-287</identifier>
<name>Недостаточная аутентификация</name>
</cwe>
</cwes>
<sources>
www.vendor.example/security/advisories/ctl-2025-01
</sources>
<identifiers>
<identifier type="ICSA" link="https://www.cisa.gov/news-events/ics-advisories/icsa-25-123-01">ICSA-25-123-01</identifier>
</identifiers>
<other>Поставщик сообщает об ограниченном наличии эксплойтов.</other>
<vul_class>Уязвимость архитектуры</vul_class>
<vul_state>Опубликована</vul_state>
</vul>
</vulnerabilities>

View File

@@ -0,0 +1,335 @@
[
{
"advisoryKey": "BDU:2025-00001",
"affectedPackages": [
{
"type": "vendor",
"identifier": "ООО «1С-Софт» 1С:Предприятие",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": null,
"provenance": {
"source": "ru-bdu",
"kind": "package-range",
"value": "8.2.19.116",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "8.2.19.116",
"rangeKind": "string"
}
],
"normalizedVersions": [
{
"scheme": "ru-bdu.raw",
"type": "exact",
"min": null,
"minInclusive": null,
"max": null,
"maxInclusive": null,
"value": "8.2.19.116",
"notes": null
}
],
"statuses": [
{
"provenance": {
"source": "ru-bdu",
"kind": "package-status",
"value": "Подтверждена производителем",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "affected"
},
{
"provenance": {
"source": "ru-bdu",
"kind": "package-fix-status",
"value": "Уязвимость устранена",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "fixed"
}
],
"provenance": [
{
"source": "ru-bdu",
"kind": "package",
"value": "ООО «1С-Софт» 1С:Предприятие",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
},
{
"type": "vendor",
"identifier": "ООО «1С-Софт» 1С:Предприятие",
"platform": "Windows",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": null,
"provenance": {
"source": "ru-bdu",
"kind": "package-range",
"value": "8.2.18.96",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "8.2.18.96",
"rangeKind": "string"
}
],
"normalizedVersions": [
{
"scheme": "ru-bdu.raw",
"type": "exact",
"min": null,
"minInclusive": null,
"max": null,
"maxInclusive": null,
"value": "8.2.18.96",
"notes": null
}
],
"statuses": [
{
"provenance": {
"source": "ru-bdu",
"kind": "package-status",
"value": "Подтверждена производителем",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "affected"
},
{
"provenance": {
"source": "ru-bdu",
"kind": "package-fix-status",
"value": "Уязвимость устранена",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "fixed"
}
],
"provenance": [
{
"source": "ru-bdu",
"kind": "package",
"value": "ООО «1С-Софт» 1С:Предприятие",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
],
"aliases": [
"BDU:2025-00001",
"CVE-2009-3555",
"CVE-2015-0206",
"PT-2015-0206"
],
"credits": [],
"cvssMetrics": [
{
"baseScore": 7.5,
"baseSeverity": "high",
"provenance": {
"source": "ru-bdu",
"kind": "cvss",
"value": "CVSS:2.0/AV:N/AC:L/AU:N/C:P/I:P/A:P",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:2.0/AV:N/AC:L/AU:N/C:P/I:P/A:P",
"version": "2.0"
},
{
"baseScore": 9.8,
"baseSeverity": "critical",
"provenance": {
"source": "ru-bdu",
"kind": "cvss",
"value": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"exploitKnown": true,
"language": "ru",
"modified": "2013-01-12T00:00:00+00:00",
"provenance": [
{
"source": "ru-bdu",
"kind": "advisory",
"value": "BDU:2025-00001",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2013-01-12T00:00:00+00:00",
"references": [
{
"kind": "source",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "http://mirror.example/ru-bdu/BDU-2025-00001",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "ru-bdu",
"summary": null,
"url": "http://mirror.example/ru-bdu/BDU-2025-00001"
},
{
"kind": "source",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://advisories.example/BDU-2025-00001",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "ru-bdu",
"summary": null,
"url": "https://advisories.example/BDU-2025-00001"
},
{
"kind": "details",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://bdu.fstec.ru/vul/2025-00001",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "ru-bdu",
"summary": null,
"url": "https://bdu.fstec.ru/vul/2025-00001"
},
{
"kind": "cwe",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://cwe.mitre.org/data/definitions/310.html",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "cwe",
"summary": "Проблемы использования криптографии",
"url": "https://cwe.mitre.org/data/definitions/310.html"
},
{
"kind": "cve",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://nvd.nist.gov/vuln/detail/CVE-2009-3555",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "cve",
"summary": "CVE-2009-3555",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2009-3555"
},
{
"kind": "cve",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://nvd.nist.gov/vuln/detail/CVE-2015-0206",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "cve",
"summary": "CVE-2015-0206",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2015-0206"
},
{
"kind": "external",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://ptsecurity.com/PT-2015-0206",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "positivetechnologiesadvisory",
"summary": "PT-2015-0206",
"url": "https://ptsecurity.com/PT-2015-0206"
}
],
"severity": "critical",
"summary": "Удалённый злоумышленник может вызвать отказ в обслуживании или получить доступ к данным.",
"title": "Множественные уязвимости криптопровайдера"
}
]

View File

@@ -0,0 +1,11 @@
[
{
"metadata": {
"ru-bdu.identifier": "BDU:2025-00001",
"ru-bdu.name": "Множественные уязвимости криптопровайдера"
},
"sha256": "c43df9c4a75a74b281ff09122bb8f63096a0a73b30df74d73c3bc997019bd4d4",
"status": "mapped",
"uri": "https://bdu.fstec.ru/vul/2025-00001"
}
]

View File

@@ -0,0 +1,86 @@
[
{
"documentUri": "https://bdu.fstec.ru/vul/2025-00001",
"payload": {
"identifier": "BDU:2025-00001",
"name": "Множественные уязвимости криптопровайдера",
"description": "Удалённый злоумышленник может вызвать отказ в обслуживании или получить доступ к данным.",
"solution": "Установить обновление 8.2.19.116 защищённого комплекса.",
"identifyDate": "2013-01-12T00:00:00+00:00",
"severityText": "Высокий уровень опасности (базовая оценка CVSS 2.0 составляет 7,5)",
"cvssVector": "AV:N/AC:L/Au:N/C:P/I:P/A:P",
"cvssScore": 7.5,
"cvss3Vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"cvss3Score": 9.8,
"exploitStatus": "Существует в открытом доступе",
"incidentCount": 0,
"fixStatus": "Уязвимость устранена",
"vulStatus": "Подтверждена производителем",
"vulClass": "Уязвимость кода",
"vulState": "Опубликована",
"other": "Язык разработки ПО С",
"software": [
{
"vendor": "ООО «1С-Софт»",
"name": "1С:Предприятие",
"version": "8.2.18.96",
"platform": "Windows",
"types": [
"Прикладное ПО информационных систем"
]
},
{
"vendor": "ООО «1С-Софт»",
"name": "1С:Предприятие",
"version": "8.2.19.116",
"platform": "Не указана",
"types": [
"Прикладное ПО информационных систем"
]
}
],
"environment": [
{
"vendor": "Microsoft Corp",
"name": "Windows",
"version": "-",
"platform": "64-bit"
},
{
"vendor": "Microsoft Corp",
"name": "Windows",
"version": "-",
"platform": "32-bit"
}
],
"cwes": [
{
"identifier": "CWE-310",
"name": "Проблемы использования криптографии"
}
],
"sources": [
"https://advisories.example/BDU-2025-00001",
"http://mirror.example/ru-bdu/BDU-2025-00001"
],
"identifiers": [
{
"type": "CVE",
"value": "CVE-2015-0206",
"link": "https://nvd.nist.gov/vuln/detail/CVE-2015-0206"
},
{
"type": "CVE",
"value": "CVE-2009-3555",
"link": "https://nvd.nist.gov/vuln/detail/CVE-2009-3555"
},
{
"type": "Positive Technologies Advisory",
"value": "PT-2015-0206",
"link": "https://ptsecurity.com/PT-2015-0206"
}
]
},
"schemaVersion": "ru-bdu.v1"
}
]

View File

@@ -0,0 +1,11 @@
[
{
"headers": {
"accept": "application/zip,application/octet-stream,application/x-zip-compressed",
"accept-Language": "ru-RU,ru; q=0.9,en-US; q=0.6,en; q=0.4",
"user-Agent": "StellaOps/Concelier,(+https://stella-ops.org)"
},
"method": "GET",
"uri": "https://bdu.fstec.ru/files/documents/vulxml.zip"
}
]

View File

@@ -0,0 +1,5 @@
{
"lastSuccessfulFetch": "2025-10-14T08:00:00.0000000+00:00",
"pendingDocuments": [],
"pendingMappings": []
}

View File

@@ -0,0 +1,303 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Connector.Ru.Bdu;
using StellaOps.Concelier.Connector.Ru.Bdu.Configuration;
using StellaOps.Concelier.Connector.Ru.Bdu.Internal;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Concelier.Testing;
using Xunit;
using Xunit.Sdk;
namespace StellaOps.Concelier.Connector.Ru.Bdu.Tests;
[Collection("mongo-fixture")]
public sealed class RuBduConnectorSnapshotTests : IAsyncLifetime
{
private const string UpdateFixturesVariable = "UPDATE_BDU_FIXTURES";
private static readonly Uri ArchiveUri = new("https://bdu.fstec.ru/files/documents/vulxml.zip");
private readonly MongoIntegrationFixture _fixture;
private ConnectorTestHarness? _harness;
public RuBduConnectorSnapshotTests(MongoIntegrationFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task FetchParseMap_ProducesDeterministicSnapshots()
{
var harness = await EnsureHarnessAsync();
harness.Handler.AddResponse(ArchiveUri, BuildArchiveResponse);
var connector = harness.ServiceProvider.GetRequiredService<RuBduConnector>();
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
var initialState = await stateRepository.TryGetAsync(RuBduConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(initialState);
var cursorBeforeParse = initialState!.Cursor is null ? RuBduCursor.Empty : RuBduCursor.FromBson(initialState.Cursor);
Assert.NotEmpty(cursorBeforeParse.PendingDocuments);
var expectedDocumentIds = cursorBeforeParse.PendingDocuments.ToArray();
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
var documentsCollection = _fixture.Database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.Document);
var documentCount = await documentsCollection.CountDocumentsAsync(Builders<BsonDocument>.Filter.Empty);
Assert.True(documentCount > 0, "Expected persisted documents after map stage");
var documentsSnapshot = await BuildDocumentsSnapshotAsync(harness.ServiceProvider, expectedDocumentIds);
WriteOrAssertSnapshot(documentsSnapshot, "ru-bdu-documents.snapshot.json");
var dtoSnapshot = await BuildDtoSnapshotAsync(harness.ServiceProvider);
WriteOrAssertSnapshot(dtoSnapshot, "ru-bdu-dtos.snapshot.json");
var advisoriesSnapshot = await BuildAdvisoriesSnapshotAsync(harness.ServiceProvider);
WriteOrAssertSnapshot(advisoriesSnapshot, "ru-bdu-advisories.snapshot.json");
var stateSnapshot = await BuildStateSnapshotAsync(harness.ServiceProvider);
WriteOrAssertSnapshot(stateSnapshot, "ru-bdu-state.snapshot.json");
var requestsSnapshot = BuildRequestsSnapshot(harness.Handler.Requests);
WriteOrAssertSnapshot(requestsSnapshot, "ru-bdu-requests.snapshot.json");
harness.Handler.AssertNoPendingResponses();
}
public Task InitializeAsync() => Task.CompletedTask;
public async Task DisposeAsync()
{
if (_harness is not null)
{
await _harness.DisposeAsync();
_harness = null;
}
}
private async Task<ConnectorTestHarness> EnsureHarnessAsync()
{
if (_harness is not null)
{
return _harness;
}
var initialTime = new DateTimeOffset(2025, 10, 14, 8, 0, 0, TimeSpan.Zero);
var harness = new ConnectorTestHarness(_fixture, initialTime, RuBduOptions.HttpClientName);
await harness.EnsureServiceProviderAsync(services =>
{
services.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddProvider(NullLoggerProvider.Instance);
});
services.AddRuBduConnector(options =>
{
options.BaseAddress = new Uri("https://bdu.fstec.ru/");
options.DataArchivePath = "files/documents/vulxml.zip";
options.MaxVulnerabilitiesPerFetch = 25;
options.RequestTimeout = TimeSpan.FromSeconds(30);
var cacheRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", _fixture.Database.DatabaseNamespace.DatabaseName, "ru-bdu");
Directory.CreateDirectory(cacheRoot);
options.CacheDirectory = cacheRoot;
});
services.Configure<HttpClientFactoryOptions>(RuBduOptions.HttpClientName, options =>
{
options.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = harness.Handler);
});
});
_harness = harness;
return harness;
}
private static HttpResponseMessage BuildArchiveResponse()
{
var archiveBytes = CreateArchiveBytes();
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ByteArrayContent(archiveBytes),
};
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
response.Content.Headers.LastModified = new DateTimeOffset(2025, 10, 14, 9, 30, 0, TimeSpan.Zero);
response.Content.Headers.ContentLength = archiveBytes.Length;
return response;
}
private async Task<string> BuildDocumentsSnapshotAsync(IServiceProvider provider, IReadOnlyCollection<Guid> documentIds)
{
var documentStore = provider.GetRequiredService<IDocumentStore>();
var records = new List<object>(documentIds.Count);
foreach (var documentId in documentIds)
{
var record = await documentStore.FindAsync(documentId, CancellationToken.None);
if (record is null)
{
var existing = await _fixture.Database
.GetCollection<BsonDocument>("documents")
.Find(Builders<BsonDocument>.Filter.Empty)
.Project(Builders<BsonDocument>.Projection.Include("Uri"))
.ToListAsync(CancellationToken.None);
var uris = existing
.Select(document => document.GetValue("Uri", BsonValue.Create(string.Empty)).AsString)
.ToArray();
throw new XunitException($"Document id not found: {documentId}. Known URIs: {string.Join(", ", uris)}");
}
records.Add(new
{
record.Uri,
record.Status,
record.Sha256,
Metadata = record.Metadata is null
? null
: record.Metadata
.OrderBy(static pair => pair.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.OrdinalIgnoreCase)
});
}
var ordered = records
.OrderBy(static entry => entry?.GetType().GetProperty("Uri")?.GetValue(entry)?.ToString(), StringComparer.Ordinal)
.ToArray();
return SnapshotSerializer.ToSnapshot(ordered);
}
private async Task<string> BuildDtoSnapshotAsync(IServiceProvider provider)
{
var dtoStore = provider.GetRequiredService<IDtoStore>();
var documentStore = provider.GetRequiredService<IDocumentStore>();
var records = await dtoStore.GetBySourceAsync(RuBduConnectorPlugin.SourceName, 25, CancellationToken.None);
var entries = new List<object>(records.Count);
foreach (var record in records.OrderBy(static r => r.DocumentId))
{
var document = await documentStore.FindAsync(record.DocumentId, CancellationToken.None);
Assert.NotNull(document);
var payload = BsonTypeMapper.MapToDotNetValue(record.Payload);
entries.Add(new
{
DocumentUri = document!.Uri,
record.SchemaVersion,
Payload = payload,
});
}
return SnapshotSerializer.ToSnapshot(entries.OrderBy(static entry => entry.GetType().GetProperty("DocumentUri")!.GetValue(entry)?.ToString(), StringComparer.Ordinal).ToArray());
}
private async Task<string> BuildAdvisoriesSnapshotAsync(IServiceProvider provider)
{
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
var advisories = await advisoryStore.GetRecentAsync(25, CancellationToken.None);
var ordered = advisories
.OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.Ordinal)
.ToArray();
return SnapshotSerializer.ToSnapshot(ordered);
}
private async Task<string> BuildStateSnapshotAsync(IServiceProvider provider)
{
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(RuBduConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var cursor = state!.Cursor is null ? RuBduCursor.Empty : RuBduCursor.FromBson(state.Cursor);
var snapshot = new
{
PendingDocuments = cursor.PendingDocuments.Select(static guid => guid.ToString()).OrderBy(static id => id, StringComparer.Ordinal).ToArray(),
PendingMappings = cursor.PendingMappings.Select(static guid => guid.ToString()).OrderBy(static id => id, StringComparer.Ordinal).ToArray(),
LastSuccessfulFetch = cursor.LastSuccessfulFetch?.ToUniversalTime().ToString("O"),
};
return SnapshotSerializer.ToSnapshot(snapshot);
}
private static string BuildRequestsSnapshot(IReadOnlyCollection<CannedHttpMessageHandler.CannedRequestRecord> requests)
{
var ordered = requests
.Select(record => new
{
Method = record.Method.Method,
Uri = record.Uri.ToString(),
Headers = record.Headers
.OrderBy(static kvp => kvp.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(static kvp => kvp.Key, static kvp => kvp.Value, StringComparer.OrdinalIgnoreCase),
})
.OrderBy(static entry => entry.Uri, StringComparer.Ordinal)
.ToArray();
return SnapshotSerializer.ToSnapshot(ordered);
}
private static string ReadFixtureText(string filename)
{
var path = GetSourceFixturePath(filename);
return File.ReadAllText(path, Encoding.UTF8);
}
private static byte[] CreateArchiveBytes()
{
var xml = ReadFixtureText("export-sample.xml");
using var buffer = new MemoryStream();
using (var archive = new ZipArchive(buffer, ZipArchiveMode.Create, leaveOpen: true))
{
var entry = archive.CreateEntry("export/export.xml", CompressionLevel.NoCompression);
entry.LastWriteTime = new DateTimeOffset(2025, 10, 14, 9, 0, 0, TimeSpan.Zero);
using var entryStream = entry.Open();
using var writer = new StreamWriter(entryStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
writer.Write(xml);
}
return buffer.ToArray();
}
private static bool ShouldUpdateFixtures()
=> !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(UpdateFixturesVariable));
private static void WriteOrAssertSnapshot(string content, string filename)
{
var path = GetSourceFixturePath(filename);
if (ShouldUpdateFixtures())
{
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, content, Encoding.UTF8);
}
else
{
Assert.True(File.Exists(path), $"Snapshot '{filename}' is missing. Run {UpdateFixturesVariable}=1 dotnet test to regenerate fixtures.");
var expected = File.ReadAllText(path, Encoding.UTF8);
Assert.Equal(expected, content);
}
}
private static string GetSourceFixturePath(string relativeName)
=> Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Fixtures", relativeName));
}

View File

@@ -0,0 +1,95 @@
using System.Collections.Immutable;
using MongoDB.Bson;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Ru.Bdu.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
using Xunit;
namespace StellaOps.Concelier.Connector.Ru.Bdu.Tests;
public sealed class RuBduMapperTests
{
[Fact]
public void Map_ConstructsCanonicalAdvisory()
{
var dto = new RuBduVulnerabilityDto(
Identifier: "BDU:2025-12345",
Name: "Уязвимость тестового продукта",
Description: "Описание",
Solution: "Обновить",
IdentifyDate: new DateTimeOffset(2025, 10, 10, 0, 0, 0, TimeSpan.Zero),
SeverityText: "Высокий уровень опасности",
CvssVector: "AV:N/AC:L/Au:N/C:P/I:P/A:P",
CvssScore: 7.5,
Cvss3Vector: "AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
Cvss3Score: 9.8,
ExploitStatus: "Существует",
IncidentCount: 2,
FixStatus: "Уязвимость устранена",
VulStatus: "Подтверждена производителем",
VulClass: null,
VulState: null,
Other: null,
Software: new[]
{
new RuBduSoftwareDto(
"ООО Вендор",
"Продукт",
"1.2.3;1.2.4",
"Windows",
new[] { "ПО программно-аппаратного средства АСУ ТП" }.ToImmutableArray())
}.ToImmutableArray(),
Environment: ImmutableArray<RuBduEnvironmentDto>.Empty,
Cwes: new[] { new RuBduCweDto("CWE-79", "XSS"), new RuBduCweDto("CWE-89", "SQL Injection") }.ToImmutableArray(),
Sources: new[]
{
"https://advisories.example/BDU-2025-12345",
"www.example.com/ru-bdu/BDU-2025-12345"
}.ToImmutableArray(),
Identifiers: new[]
{
new RuBduExternalIdentifierDto("CVE", "CVE-2025-12345", "https://nvd.nist.gov/vuln/detail/CVE-2025-12345"),
new RuBduExternalIdentifierDto("Positive Technologies Advisory", "PT-2025-001", "https://ptsecurity.com/PT-2025-001")
}.ToImmutableArray());
var document = new DocumentRecord(
Guid.NewGuid(),
RuBduConnectorPlugin.SourceName,
"https://bdu.fstec.ru/vul/2025-12345",
DateTimeOffset.UtcNow,
"abc",
DocumentStatuses.PendingMap,
"application/json",
null,
null,
null,
dto.IdentifyDate,
ObjectId.GenerateNewId());
var advisory = RuBduMapper.Map(dto, document, dto.IdentifyDate!.Value);
Assert.Equal("BDU:2025-12345", advisory.AdvisoryKey);
Assert.Contains("BDU:2025-12345", advisory.Aliases);
Assert.Contains("CVE-2025-12345", advisory.Aliases);
Assert.Equal("critical", advisory.Severity);
Assert.True(advisory.ExploitKnown);
var package = Assert.Single(advisory.AffectedPackages);
Assert.Equal(AffectedPackageTypes.IcsVendor, package.Type);
Assert.Equal(2, package.VersionRanges.Length);
Assert.Equal(2, package.NormalizedVersions.Length);
Assert.All(package.NormalizedVersions, rule => Assert.Equal("ru-bdu.raw", rule.Scheme));
Assert.Contains(package.NormalizedVersions, rule => rule.Value == "1.2.3");
Assert.Contains(package.NormalizedVersions, rule => rule.Value == "1.2.4");
Assert.Contains(package.Statuses, status => status.Status == AffectedPackageStatusCatalog.Affected);
Assert.Contains(package.Statuses, status => status.Status == AffectedPackageStatusCatalog.Fixed);
Assert.Equal(2, advisory.CvssMetrics.Length);
Assert.Contains(advisory.References, reference => reference.Url == "https://bdu.fstec.ru/vul/2025-12345" && reference.Kind == "details");
Assert.Contains(advisory.References, reference => reference.Url == "https://nvd.nist.gov/vuln/detail/CVE-2025-12345" && reference.Kind == "cve");
Assert.Contains(advisory.References, reference => reference.Url == "https://advisories.example/BDU-2025-12345" && reference.Kind == "source");
Assert.Contains(advisory.References, reference => reference.Url == "https://www.example.com/ru-bdu/BDU-2025-12345" && reference.Kind == "source");
Assert.Contains(advisory.References, reference => reference.SourceTag == "positivetechnologiesadvisory");
}
}

View File

@@ -0,0 +1,93 @@
using System.IO;
using System.Xml.Linq;
using StellaOps.Concelier.Connector.Ru.Bdu.Internal;
using Xunit;
namespace StellaOps.Concelier.Connector.Ru.Bdu.Tests;
public sealed class RuBduXmlParserTests
{
[Fact]
public void TryParse_ValidElement_ReturnsDto()
{
const string xml = """
<vul>
<identifier>BDU:2025-12345</identifier>
<name>Уязвимость тестового продукта</name>
<description>Описание уязвимости</description>
<solution>Обновить продукт</solution>
<identify_date>2025-10-10</identify_date>
<severity>Высокий уровень опасности</severity>
<exploit_status>Существует эксплойт</exploit_status>
<fix_status>Устранена</fix_status>
<vul_status>Подтверждена производителем</vul_status>
<vul_incident>1</vul_incident>
<cvss>
<vector score="7.5">AV:N/AC:L/Au:N/C:P/I:P/A:P</vector>
</cvss>
<cvss3>
<vector score="9.8">AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H</vector>
</cvss3>
<vulnerable_software>
<soft>
<vendor>ООО «Вендор»</vendor>
<name>Продукт</name>
<version>1.2.3</version>
<platform>Windows</platform>
<types>
<type>ics</type>
</types>
</soft>
</vulnerable_software>
<sources>
https://advisories.example/BDU-2025-12345
https://mirror.example/ru-bdu/BDU-2025-12345
</sources>
<identifiers>
<identifier type="CVE" link="https://nvd.nist.gov/vuln/detail/CVE-2025-12345">CVE-2025-12345</identifier>
<identifier type="GHSA" link="https://github.com/advisories/GHSA-xxxx-yyyy-zzzz">GHSA-xxxx-yyyy-zzzz</identifier>
</identifiers>
<cwes>
<cwe>
<identifier>CWE-79</identifier>
<name>XSS</name>
</cwe>
</cwes>
</vul>
""";
var element = XElement.Parse(xml);
var dto = RuBduXmlParser.TryParse(element);
Assert.NotNull(dto);
Assert.Equal("BDU:2025-12345", dto!.Identifier);
Assert.Equal("Уязвимость тестового продукта", dto.Name);
Assert.Equal("AV:N/AC:L/Au:N/C:P/I:P/A:P", dto.CvssVector);
Assert.Equal(7.5, dto.CvssScore);
Assert.Equal("AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", dto.Cvss3Vector);
Assert.Equal(9.8, dto.Cvss3Score);
Assert.Single(dto.Software);
Assert.Single(dto.Cwes);
Assert.Equal(2, dto.Sources.Length);
Assert.Contains("https://advisories.example/BDU-2025-12345", dto.Sources);
Assert.Equal(2, dto.Identifiers.Length);
Assert.Contains(dto.Identifiers, identifier => identifier.Type == "CVE" && identifier.Value == "CVE-2025-12345");
Assert.Contains(dto.Identifiers, identifier => identifier.Type == "GHSA" && identifier.Link == "https://github.com/advisories/GHSA-xxxx-yyyy-zzzz");
}
[Fact]
public void TryParse_SampleArchiveEntries_ReturnDtos()
{
var path = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Fixtures", "export-sample.xml"));
var document = XDocument.Load(path);
var vulnerabilities = document.Root?.Elements("vul");
Assert.NotNull(vulnerabilities);
foreach (var element in vulnerabilities!)
{
var dto = RuBduXmlParser.TryParse(element);
Assert.NotNull(dto);
Assert.False(string.IsNullOrWhiteSpace(dto!.Identifier));
}
}
}

View File

@@ -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.Bdu/StellaOps.Concelier.Connector.Ru.Bdu.csproj" />
</ItemGroup>
</Project>