audit notes work completed, test fixes work (95% done), new sprints, new data sources setup and configuration
This commit is contained in:
@@ -4,17 +4,15 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<UseXunitV3></UseXunitV3>
|
||||
<NoWarn>CS0104;CS0168;CS0219;CS0414;CS0649;CS8600;CS8602;CS8603;CS8604</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="nunit" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="5.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageReference Include="xunit.v3" Version="3.2.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
@@ -6,23 +6,22 @@ using System.Text;
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Gost_28147_89;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_28147_89
|
||||
{
|
||||
/// <summary>
|
||||
/// Шифрование и дешифрование данных с использованием случайного сессионного ключа.
|
||||
/// ?????????? ? ???????????? ?????? ? ?????????????? ?????????? ??????????? ?????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест имитирует обмен данными между условным отправителем, который шифрует заданный поток байт, и условным получателем, который дешифрует
|
||||
/// зашифрованный поток байт. Шифрация осуществляется с использованием случайного симметричного ключа, который в свою очередь шифруется
|
||||
/// с использованием открытого ключа получателя. Соответственно для дешифрации данных сначала расшифровывается случайный симметричный ключ
|
||||
/// с использованием закрытого ключа получателя.
|
||||
/// ???? ????????? ????? ??????? ????? ???????? ????????????, ??????? ??????? ???????? ????? ????, ? ???????? ???????????, ??????? ?????????
|
||||
/// ????????????? ????? ????. ???????? ?????????????? ? ?????????????? ?????????? ????????????? ?????, ??????? ? ???? ??????? ?????????
|
||||
/// ? ?????????????? ????????? ????? ??????????. ?????????????? ??? ?????????? ?????? ??????? ???????????????? ????????? ???????????? ????
|
||||
/// ? ?????????????? ????????? ????? ??????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Шифрование и дешифрование данных с использованием случайного сессионного ключа")]
|
||||
public class EncryptDecryptSessionKeyTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldEncryptAndDecrypt(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -42,7 +41,7 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт
|
||||
// ????????? ????? ????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data to encrypt..."));
|
||||
}
|
||||
@@ -51,17 +50,17 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
{
|
||||
var encryptedDataStream = new MemoryStream();
|
||||
|
||||
// Отправитель создает случайный сессионный ключ для шифрации данных
|
||||
// ??????????? ??????? ????????? ?????????? ???? ??? ???????? ??????
|
||||
using (var senderSessionKey = new Gost_28147_89_SymmetricAlgorithm(publicKey.ProviderType))
|
||||
{
|
||||
// Отправитель передает получателю вектор инициализации
|
||||
// ??????????? ???????? ?????????? ?????? ?????????????
|
||||
iv = senderSessionKey.IV;
|
||||
|
||||
// Отправитель шифрует сессионный ключ и передает его получателю
|
||||
// ??????????? ??????? ?????????? ???? ? ???????? ??? ??????????
|
||||
var formatter = publicKey.CreateKeyExchangeFormatter();
|
||||
sessionKey = formatter.CreateKeyExchangeData(senderSessionKey);
|
||||
|
||||
// Отправитель шифрует данные с использованием сессионного ключа
|
||||
// ??????????? ??????? ?????? ? ?????????????? ??????????? ?????
|
||||
using (var encryptor = senderSessionKey.CreateEncryptor())
|
||||
{
|
||||
var cryptoStream = new CryptoStream(encryptedDataStream, encryptor, CryptoStreamMode.Write);
|
||||
@@ -81,13 +80,13 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
|
||||
var deformatter = privateKey.CreateKeyExchangeDeformatter();
|
||||
|
||||
// Получатель принимает от отправителя зашифрованный сессионный ключ и дешифрует его
|
||||
// ?????????? ????????? ?? ??????????? ????????????? ?????????? ???? ? ????????? ???
|
||||
using (var receiverSessionKey = deformatter.DecryptKeyExchangeAlgorithm(sessionKey))
|
||||
{
|
||||
// Получатель принимает от отправителя вектор инициализации
|
||||
// ?????????? ????????? ?? ??????????? ?????? ?????????????
|
||||
receiverSessionKey.IV = iv;
|
||||
|
||||
// Получатель дешифрует данные с использованием сессионного ключа
|
||||
// ?????????? ????????? ?????? ? ?????????????? ??????????? ?????
|
||||
using (var decryptor = receiverSessionKey.CreateDecryptor())
|
||||
{
|
||||
var cryptoStream = new CryptoStream(encryptedDataStream, decryptor, CryptoStreamMode.Read);
|
||||
@@ -100,4 +99,4 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
return decryptedDataStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Gost_28147_89;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_28147_89
|
||||
{
|
||||
/// <summary>
|
||||
/// Вычисление имитовставки на базе общего симметричного ключа ГОСТ 28147-89.
|
||||
/// ?????????? ???????????? ?? ???? ?????? ????????????? ????? ???? 28147-89.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест выполняет подпись и проверку подписи потока байт с использованием имитовставки.
|
||||
/// ???? ????????? ??????? ? ???????? ??????? ?????? ???? ? ?????????????? ????????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Вычисление имитовставки на базе общего симметричного ключа ГОСТ 28147-89")]
|
||||
public class Gost_28147_89_ImitHashAlgorithmTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldComputeImitHash(ProviderType providerType)
|
||||
{
|
||||
@@ -31,29 +30,29 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
var isValidImitDataStream = VerifyImitDataStream(sharedKey, imitDataStream);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(isValidImitDataStream);
|
||||
Assert.True(isValidImitDataStream);
|
||||
}
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт
|
||||
// ????????? ????? ????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data for imit..."));
|
||||
}
|
||||
|
||||
private static Stream CreateImitDataStream(Gost_28147_89_SymmetricAlgorithmBase sharedKey, Stream dataStream)
|
||||
{
|
||||
// Создание объекта для вычисления имитовставки
|
||||
// ???????? ??????? ??? ?????????? ????????????
|
||||
using (var imitHash = new Gost_28147_89_ImitHashAlgorithm(sharedKey))
|
||||
{
|
||||
// Вычисление имитовставки для потока данных
|
||||
// ?????????? ???????????? ??? ?????? ??????
|
||||
var imitHashValue = imitHash.ComputeHash(dataStream);
|
||||
|
||||
// Запись имитовставки в начало выходного потока данных
|
||||
// ?????? ???????????? ? ?????? ????????? ?????? ??????
|
||||
var imitDataStream = new MemoryStream();
|
||||
imitDataStream.Write(imitHashValue, 0, imitHashValue.Length);
|
||||
|
||||
// Копирование исходного потока данных в выходной поток
|
||||
// ??????????? ????????? ?????? ?????? ? ???????? ?????
|
||||
dataStream.Position = 0;
|
||||
dataStream.CopyTo(imitDataStream);
|
||||
|
||||
@@ -65,19 +64,19 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
|
||||
private static bool VerifyImitDataStream(Gost_28147_89_SymmetricAlgorithmBase sharedKey, Stream imitDataStream)
|
||||
{
|
||||
// Создание объекта для вычисления имитовставки
|
||||
// ???????? ??????? ??? ?????????? ????????????
|
||||
using (var imitHash = new Gost_28147_89_ImitHashAlgorithm(sharedKey))
|
||||
{
|
||||
// Считывание имитовставки из потока данных
|
||||
// ?????????? ???????????? ?? ?????? ??????
|
||||
var imitHashValue = new byte[imitHash.HashSize / 8];
|
||||
imitDataStream.Read(imitHashValue, 0, imitHashValue.Length);
|
||||
|
||||
// Вычисление реального значения имитовставки для потока данных
|
||||
// ?????????? ????????? ???????? ???????????? ??? ?????? ??????
|
||||
var expectedImitHashValue = imitHash.ComputeHash(imitDataStream);
|
||||
|
||||
// Сравнение исходной имитовставки с ожидаемой
|
||||
// ????????? ???????? ???????????? ? ?????????
|
||||
return imitHashValue.SequenceEqual(expectedImitHashValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Gost_28147_89;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_28147_89
|
||||
{
|
||||
/// <summary>
|
||||
/// Шифрование и дешифрование данных с использованием общего симметричного ключа ГОСТ 28147-89.
|
||||
/// ?????????? ? ???????????? ?????? ? ?????????????? ?????? ????????????? ????? ???? 28147-89.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает поток байт, шифрует его с использованием общего симметричного ключа,
|
||||
/// а затем дешифрует зашифрованные данные и проверяет корректность дешифрации.
|
||||
/// ???? ??????? ????? ????, ??????? ??? ? ?????????????? ?????? ????????????? ?????,
|
||||
/// ? ????? ????????? ????????????? ?????? ? ????????? ???????????? ??????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Шифрование и дешифрование данных с использованием общего симметричного ключа ГОСТ 28147-89")]
|
||||
public class Gost_28147_89_SymmetricAlgorithmTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldEncryptAndDecrypt(ProviderType providerType)
|
||||
{
|
||||
@@ -37,7 +36,7 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт
|
||||
// ????????? ????? ????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data to encrypt..."));
|
||||
}
|
||||
@@ -74,4 +73,4 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
return decryptedDataStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
@@ -6,23 +6,22 @@ using System.Text;
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Gost_28147_89;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_28147_89
|
||||
{
|
||||
/// <summary>
|
||||
/// Шифрование и дешифрование данных с использованием случайного сессионного ключа ГОСТ Р 34.12-2015 Кузнечик.
|
||||
/// ?????????? ? ???????????? ?????? ? ?????????????? ?????????? ??????????? ????? ???? ? 34.12-2015 ????????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест имитирует обмен данными между условным отправителем, который шифрует заданный поток байт, и условным получателем, который дешифрует
|
||||
/// зашифрованный поток байт. Шифрация осуществляется с использованием случайного симметричного ключа, который в свою очередь шифруется
|
||||
/// с использованием открытого ключа получателя. Соответственно для дешифрации данных сначала расшифровывается случайный симметричный ключ
|
||||
/// с использованием закрытого ключа получателя.
|
||||
/// ???? ????????? ????? ??????? ????? ???????? ????????????, ??????? ??????? ???????? ????? ????, ? ???????? ???????????, ??????? ?????????
|
||||
/// ????????????? ????? ????. ???????? ?????????????? ? ?????????????? ?????????? ????????????? ?????, ??????? ? ???? ??????? ?????????
|
||||
/// ? ?????????????? ????????? ????? ??????????. ?????????????? ??? ?????????? ?????? ??????? ???????????????? ????????? ???????????? ????
|
||||
/// ? ?????????????? ????????? ????? ??????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Шифрование и дешифрование данных с использованием случайного сессионного ключа ГОСТ Р 34.12-2015 Кузнечик")]
|
||||
public class KuznyechikEncryptDecryptSessionKeyTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldEncryptAndDecrypt(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -42,7 +41,7 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт
|
||||
// ????????? ????? ????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data to encrypt..."));
|
||||
}
|
||||
@@ -51,17 +50,17 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
{
|
||||
var encryptedDataStream = new MemoryStream();
|
||||
|
||||
// Отправитель создает случайный сессионный ключ для шифрации данных
|
||||
// ??????????? ??????? ????????? ?????????? ???? ??? ???????? ??????
|
||||
using (var senderSessionKey = new Gost_3412_K_SymmetricAlgorithm(publicKey.ProviderType))
|
||||
{
|
||||
// Отправитель передает получателю вектор инициализации
|
||||
// ??????????? ???????? ?????????? ?????? ?????????????
|
||||
iv = senderSessionKey.IV;
|
||||
|
||||
// Отправитель шифрует сессионный ключ и передает его получателю
|
||||
// ??????????? ??????? ?????????? ???? ? ???????? ??? ??????????
|
||||
var formatter = publicKey.CreateKeyExchangeFormatter();
|
||||
sessionKey = formatter.CreateKeyExchangeData(senderSessionKey);
|
||||
|
||||
// Отправитель шифрует данные с использованием сессионного ключа
|
||||
// ??????????? ??????? ?????? ? ?????????????? ??????????? ?????
|
||||
using (var encryptor = senderSessionKey.CreateEncryptor())
|
||||
{
|
||||
var cryptoStream = new CryptoStream(encryptedDataStream, encryptor, CryptoStreamMode.Write);
|
||||
@@ -81,13 +80,13 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
|
||||
var deformatter = privateKey.CreateKeyExchangeDeformatter();
|
||||
|
||||
// Получатель принимает от отправителя зашифрованный сессионный ключ и дешифрует его
|
||||
// ?????????? ????????? ?? ??????????? ????????????? ?????????? ???? ? ????????? ???
|
||||
using (var receiverSessionKey = deformatter.DecryptKeyExchangeAlgorithm(sessionKey))
|
||||
{
|
||||
// Получатель принимает от отправителя вектор инициализации
|
||||
// ?????????? ????????? ?? ??????????? ?????? ?????????????
|
||||
receiverSessionKey.IV = iv;
|
||||
|
||||
// Получатель дешифрует данные с использованием сессионного ключа
|
||||
// ?????????? ????????? ?????? ? ?????????????? ??????????? ?????
|
||||
using (var decryptor = receiverSessionKey.CreateDecryptor())
|
||||
{
|
||||
var cryptoStream = new CryptoStream(encryptedDataStream, decryptor, CryptoStreamMode.Read);
|
||||
@@ -100,4 +99,4 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
return decryptedDataStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Gost_28147_89;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_28147_89
|
||||
{
|
||||
/// <summary>
|
||||
/// Вычисление имитовставки на базе общего симметричного ключа ГОСТ Р 34.12-2015 Кузнечик.
|
||||
/// ?????????? ???????????? ?? ???? ?????? ????????????? ????? ???? ? 34.12-2015 ????????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест выполняет подпись и проверку подписи потока байт с использованием имитовставки.
|
||||
/// ???? ????????? ??????? ? ???????? ??????? ?????? ???? ? ?????????????? ????????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Вычисление имитовставки на базе общего симметричного ключа ГОСТ Р 34.12-2015 Кузнечик")]
|
||||
public class KuznyechikImitHashAlgorithmTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldComputeImitHash(ProviderType providerType)
|
||||
{
|
||||
@@ -31,29 +30,29 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
var isValidImitDataStream = VerifyImitDataStream(sharedKey, imitDataStream);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(isValidImitDataStream);
|
||||
Assert.True(isValidImitDataStream);
|
||||
}
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт
|
||||
// ????????? ????? ????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data for imit..."));
|
||||
}
|
||||
|
||||
private static Stream CreateImitDataStream(Gost_3412_K_SymmetricAlgorithm sharedKey, Stream dataStream)
|
||||
{
|
||||
// Создание объекта для вычисления имитовставки
|
||||
// ???????? ??????? ??? ?????????? ????????????
|
||||
using (var imitHash = new Gost_3412_K_ImitHashAlgorithm(sharedKey))
|
||||
{
|
||||
// Вычисление имитовставки для потока данных
|
||||
// ?????????? ???????????? ??? ?????? ??????
|
||||
var imitHashValue = imitHash.ComputeHash(dataStream);
|
||||
|
||||
// Запись имитовставки в начало выходного потока данных
|
||||
// ?????? ???????????? ? ?????? ????????? ?????? ??????
|
||||
var imitDataStream = new MemoryStream();
|
||||
imitDataStream.Write(imitHashValue, 0, imitHashValue.Length);
|
||||
|
||||
// Копирование исходного потока данных в выходной поток
|
||||
// ??????????? ????????? ?????? ?????? ? ???????? ?????
|
||||
dataStream.Position = 0;
|
||||
dataStream.CopyTo(imitDataStream);
|
||||
|
||||
@@ -65,19 +64,19 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
|
||||
private static bool VerifyImitDataStream(Gost_3412_K_SymmetricAlgorithm sharedKey, Stream imitDataStream)
|
||||
{
|
||||
// Создание объекта для вычисления имитовставки
|
||||
// ???????? ??????? ??? ?????????? ????????????
|
||||
using (var imitHash = new Gost_3412_K_ImitHashAlgorithm(sharedKey))
|
||||
{
|
||||
// Считывание имитовставки из потока данных
|
||||
// ?????????? ???????????? ?? ?????? ??????
|
||||
var imitHashValue = new byte[imitHash.HashSize / 8];
|
||||
imitDataStream.Read(imitHashValue, 0, imitHashValue.Length);
|
||||
|
||||
// Вычисление реального значения имитовставки для потока данных
|
||||
// ?????????? ????????? ???????? ???????????? ??? ?????? ??????
|
||||
var expectedImitHashValue = imitHash.ComputeHash(imitDataStream);
|
||||
|
||||
// Сравнение исходной имитовставки с ожидаемой
|
||||
// ????????? ???????? ???????????? ? ?????????
|
||||
return imitHashValue.SequenceEqual(expectedImitHashValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Gost_28147_89;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_28147_89
|
||||
{
|
||||
/// <summary>
|
||||
/// Шифрование и дешифрование данных с использованием общего симметричного ключа ГОСТ Р 34.12-2015 Кузнечик.
|
||||
/// ?????????? ? ???????????? ?????? ? ?????????????? ?????? ????????????? ????? ???? ? 34.12-2015 ????????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает поток байт, шифрует его с использованием общего симметричного ключа,
|
||||
/// а затем дешифрует зашифрованные данные и проверяет корректность дешифрации.
|
||||
/// ???? ??????? ????? ????, ??????? ??? ? ?????????????? ?????? ????????????? ?????,
|
||||
/// ? ????? ????????? ????????????? ?????? ? ????????? ???????????? ??????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Шифрование и дешифрование данных с использованием общего симметричного ключа ГОСТ Р 34.12-2015 Кузнечик")]
|
||||
public class KuznyechikSymmetricAlgorithmTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldEncryptAndDecrypt(ProviderType providerType)
|
||||
{
|
||||
@@ -37,7 +36,7 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт
|
||||
// ????????? ????? ????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data to encrypt..."));
|
||||
}
|
||||
@@ -74,4 +73,4 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
return decryptedDataStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
@@ -6,23 +6,22 @@ using System.Text;
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Gost_28147_89;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_28147_89
|
||||
{
|
||||
/// <summary>
|
||||
/// Шифрование и дешифрование данных с использованием случайного сессионного ключа ГОСТ Р 34.12-2015 Магма.
|
||||
/// ?????????? ? ???????????? ?????? ? ?????????????? ?????????? ??????????? ????? ???? ? 34.12-2015 ?????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест имитирует обмен данными между условным отправителем, который шифрует заданный поток байт, и условным получателем, который дешифрует
|
||||
/// зашифрованный поток байт. Шифрация осуществляется с использованием случайного симметричного ключа, который в свою очередь шифруется
|
||||
/// с использованием открытого ключа получателя. Соответственно для дешифрации данных сначала расшифровывается случайный симметричный ключ
|
||||
/// с использованием закрытого ключа получателя.
|
||||
/// ???? ????????? ????? ??????? ????? ???????? ????????????, ??????? ??????? ???????? ????? ????, ? ???????? ???????????, ??????? ?????????
|
||||
/// ????????????? ????? ????. ???????? ?????????????? ? ?????????????? ?????????? ????????????? ?????, ??????? ? ???? ??????? ?????????
|
||||
/// ? ?????????????? ????????? ????? ??????????. ?????????????? ??? ?????????? ?????? ??????? ???????????????? ????????? ???????????? ????
|
||||
/// ? ?????????????? ????????? ????? ??????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Шифрование и дешифрование данных с использованием случайного сессионного ключа ГОСТ Р 34.12-2015 Магма")]
|
||||
public class MagmaEncryptDecryptSessionKeyTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldEncryptAndDecrypt(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -42,7 +41,7 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт
|
||||
// ????????? ????? ????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data to encrypt..."));
|
||||
}
|
||||
@@ -51,17 +50,17 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
{
|
||||
var encryptedDataStream = new MemoryStream();
|
||||
|
||||
// Отправитель создает случайный сессионный ключ для шифрации данных
|
||||
// ??????????? ??????? ????????? ?????????? ???? ??? ???????? ??????
|
||||
using (var senderSessionKey = new Gost_3412_M_SymmetricAlgorithm(publicKey.ProviderType))
|
||||
{
|
||||
// Отправитель передает получателю вектор инициализации
|
||||
// ??????????? ???????? ?????????? ?????? ?????????????
|
||||
iv = senderSessionKey.IV;
|
||||
|
||||
// Отправитель шифрует сессионный ключ и передает его получателю
|
||||
// ??????????? ??????? ?????????? ???? ? ???????? ??? ??????????
|
||||
var formatter = publicKey.CreateKeyExchangeFormatter();
|
||||
sessionKey = formatter.CreateKeyExchangeData(senderSessionKey);
|
||||
|
||||
// Отправитель шифрует данные с использованием сессионного ключа
|
||||
// ??????????? ??????? ?????? ? ?????????????? ??????????? ?????
|
||||
using (var encryptor = senderSessionKey.CreateEncryptor())
|
||||
{
|
||||
var cryptoStream = new CryptoStream(encryptedDataStream, encryptor, CryptoStreamMode.Write);
|
||||
@@ -81,13 +80,13 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
|
||||
var deformatter = privateKey.CreateKeyExchangeDeformatter();
|
||||
|
||||
// Получатель принимает от отправителя зашифрованный сессионный ключ и дешифрует его
|
||||
// ?????????? ????????? ?? ??????????? ????????????? ?????????? ???? ? ????????? ???
|
||||
using (var receiverSessionKey = deformatter.DecryptKeyExchangeAlgorithm(sessionKey))
|
||||
{
|
||||
// Получатель принимает от отправителя вектор инициализации
|
||||
// ?????????? ????????? ?? ??????????? ?????? ?????????????
|
||||
receiverSessionKey.IV = iv;
|
||||
|
||||
// Получатель дешифрует данные с использованием сессионного ключа
|
||||
// ?????????? ????????? ?????? ? ?????????????? ??????????? ?????
|
||||
using (var decryptor = receiverSessionKey.CreateDecryptor())
|
||||
{
|
||||
var cryptoStream = new CryptoStream(encryptedDataStream, decryptor, CryptoStreamMode.Read);
|
||||
@@ -100,4 +99,4 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
return decryptedDataStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Gost_28147_89;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_28147_89
|
||||
{
|
||||
/// <summary>
|
||||
/// Вычисление имитовставки на базе общего симметричного ключа ГОСТ Р 34.12-2015 Магма.
|
||||
/// ?????????? ???????????? ?? ???? ?????? ????????????? ????? ???? ? 34.12-2015 ?????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест выполняет подпись и проверку подписи потока байт с использованием имитовставки.
|
||||
/// ???? ????????? ??????? ? ???????? ??????? ?????? ???? ? ?????????????? ????????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Вычисление имитовставки на базе общего симметричного ключа ГОСТ Р 34.12-2015 Магма")]
|
||||
public class MagmaImitHashAlgorithmTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldComputeImitHash(ProviderType providerType)
|
||||
{
|
||||
@@ -31,29 +30,29 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
var isValidImitDataStream = VerifyImitDataStream(sharedKey, imitDataStream);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(isValidImitDataStream);
|
||||
Assert.True(isValidImitDataStream);
|
||||
}
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт
|
||||
// ????????? ????? ????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data for imit..."));
|
||||
}
|
||||
|
||||
private static Stream CreateImitDataStream(Gost_3412_M_SymmetricAlgorithm sharedKey, Stream dataStream)
|
||||
{
|
||||
// Создание объекта для вычисления имитовставки
|
||||
// ???????? ??????? ??? ?????????? ????????????
|
||||
using (var imitHash = new Gost_3412_M_ImitHashAlgorithm(sharedKey))
|
||||
{
|
||||
// Вычисление имитовставки для потока данных
|
||||
// ?????????? ???????????? ??? ?????? ??????
|
||||
var imitHashValue = imitHash.ComputeHash(dataStream);
|
||||
|
||||
// Запись имитовставки в начало выходного потока данных
|
||||
// ?????? ???????????? ? ?????? ????????? ?????? ??????
|
||||
var imitDataStream = new MemoryStream();
|
||||
imitDataStream.Write(imitHashValue, 0, imitHashValue.Length);
|
||||
|
||||
// Копирование исходного потока данных в выходной поток
|
||||
// ??????????? ????????? ?????? ?????? ? ???????? ?????
|
||||
dataStream.Position = 0;
|
||||
dataStream.CopyTo(imitDataStream);
|
||||
|
||||
@@ -65,19 +64,19 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
|
||||
private static bool VerifyImitDataStream(Gost_3412_M_SymmetricAlgorithm sharedKey, Stream imitDataStream)
|
||||
{
|
||||
// Создание объекта для вычисления имитовставки
|
||||
// ???????? ??????? ??? ?????????? ????????????
|
||||
using (var imitHash = new Gost_3412_M_ImitHashAlgorithm(sharedKey))
|
||||
{
|
||||
// Считывание имитовставки из потока данных
|
||||
// ?????????? ???????????? ?? ?????? ??????
|
||||
var imitHashValue = new byte[imitHash.HashSize / 8];
|
||||
imitDataStream.Read(imitHashValue, 0, imitHashValue.Length);
|
||||
|
||||
// Вычисление реального значения имитовставки для потока данных
|
||||
// ?????????? ????????? ???????? ???????????? ??? ?????? ??????
|
||||
var expectedImitHashValue = imitHash.ComputeHash(imitDataStream);
|
||||
|
||||
// Сравнение исходной имитовставки с ожидаемой
|
||||
// ????????? ???????? ???????????? ? ?????????
|
||||
return imitHashValue.SequenceEqual(expectedImitHashValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Gost_28147_89;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_28147_89
|
||||
{
|
||||
/// <summary>
|
||||
/// Шифрование и дешифрование данных с использованием общего симметричного ключа ГОСТ Р 34.12-2015 Магма.
|
||||
/// ?????????? ? ???????????? ?????? ? ?????????????? ?????? ????????????? ????? ???? ? 34.12-2015 ?????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает поток байт, шифрует его с использованием общего симметричного ключа,
|
||||
/// а затем дешифрует зашифрованные данные и проверяет корректность дешифрации.
|
||||
/// ???? ??????? ????? ????, ??????? ??? ? ?????????????? ?????? ????????????? ?????,
|
||||
/// ? ????? ????????? ????????????? ?????? ? ????????? ???????????? ??????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Шифрование и дешифрование данных с использованием общего симметричного ключа ГОСТ Р 34.12-2015 Магма")]
|
||||
public class MagmaSymmetricAlgorithmTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldEncryptAndDecrypt(ProviderType providerType)
|
||||
{
|
||||
@@ -37,7 +36,7 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт
|
||||
// ????????? ????? ????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data to encrypt..."));
|
||||
}
|
||||
@@ -74,4 +73,4 @@ namespace GostCryptography.Tests.Gost_28147_89
|
||||
return decryptedDataStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Security;
|
||||
|
||||
using GostCryptography.Gost_R3410;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
using GostCryptography.Base;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_R3410
|
||||
{
|
||||
[TestFixture(Description = "Проверка возможности установки пароля для контейнера ключей")]
|
||||
public class SetContainerPasswordTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_2001_Certificates))]
|
||||
public void ShouldSetContainerPassword_R3410_2001(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -32,10 +31,10 @@ namespace GostCryptography.Tests.Gost_R3410
|
||||
var isValidSignature = VerifySignature(privateKey, data, signature);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(isValidSignature);
|
||||
Assert.True(isValidSignature);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_2012_256_Certificates))]
|
||||
public void ShouldSetContainerPassword_R3410_2012_256(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -54,10 +53,10 @@ namespace GostCryptography.Tests.Gost_R3410
|
||||
var isValidSignature = VerifySignature(privateKey, data, signature);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(isValidSignature);
|
||||
Assert.True(isValidSignature);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_2012_512_Certificates))]
|
||||
public void ShouldSetContainerPassword_R3410_2012_512(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -76,7 +75,7 @@ namespace GostCryptography.Tests.Gost_R3410
|
||||
var isValidSignature = VerifySignature(privateKey, data, signature);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(isValidSignature);
|
||||
Assert.True(isValidSignature);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,4 +125,4 @@ namespace GostCryptography.Tests.Gost_R3410
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
@@ -6,20 +6,19 @@ using GostCryptography.Base;
|
||||
using GostCryptography.Gost_28147_89;
|
||||
using GostCryptography.Gost_R3411;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_R3411
|
||||
{
|
||||
/// <summary>
|
||||
/// Вычисление HMAC на базе алгоритма хэширования ГОСТ Р 34.11-2012/256 и общего симметричного ключа ГОСТ 28147-89.
|
||||
/// ?????????? HMAC ?? ???? ????????? ??????????? ???? ? 34.11-2012/256 ? ?????? ????????????? ????? ???? 28147-89.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест выполняет подпись и проверку подписи потока байт с использованием HMAC.
|
||||
/// ???? ????????? ??????? ? ???????? ??????? ?????? ???? ? ?????????????? HMAC.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Вычисление HMAC на базе алгоритма хэширования ГОСТ Р 34.11-2012/256 и общего симметричного ключа ГОСТ 28147-89")]
|
||||
public class Gost_R3411_2012_256_HMACTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldComputeHMAC(ProviderType providerType)
|
||||
{
|
||||
@@ -32,29 +31,29 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
var isValidHmacDataStream = VerifyHmacDataStream(sharedKey, hmacDataStream);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(isValidHmacDataStream);
|
||||
Assert.True(isValidHmacDataStream);
|
||||
}
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт
|
||||
// ????????? ????? ????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data to HMAC..."));
|
||||
}
|
||||
|
||||
private static Stream CreateHmacDataStream(GostSymmetricAlgorithm sharedKey, Stream dataStream)
|
||||
{
|
||||
// Создание объекта для вычисления HMAC
|
||||
// ???????? ??????? ??? ?????????? HMAC
|
||||
using (var hmac = new Gost_R3411_2012_256_HMAC(sharedKey))
|
||||
{
|
||||
// Вычисление HMAC для потока данных
|
||||
// ?????????? HMAC ??? ?????? ??????
|
||||
var hmacValue = hmac.ComputeHash(dataStream);
|
||||
|
||||
// Запись HMAC в начало выходного потока данных
|
||||
// ?????? HMAC ? ?????? ????????? ?????? ??????
|
||||
var hmacDataStream = new MemoryStream();
|
||||
hmacDataStream.Write(hmacValue, 0, hmacValue.Length);
|
||||
|
||||
// Копирование исходного потока данных в выходной поток
|
||||
// ??????????? ????????? ?????? ?????? ? ???????? ?????
|
||||
dataStream.Position = 0;
|
||||
dataStream.CopyTo(hmacDataStream);
|
||||
|
||||
@@ -66,19 +65,19 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
|
||||
private static bool VerifyHmacDataStream(GostSymmetricAlgorithm sharedKey, Stream hmacDataStream)
|
||||
{
|
||||
// Создание объекта для вычисления HMAC
|
||||
// ???????? ??????? ??? ?????????? HMAC
|
||||
using (var hmac = new Gost_R3411_2012_256_HMAC(sharedKey))
|
||||
{
|
||||
// Считывание HMAC из потока данных
|
||||
// ?????????? HMAC ?? ?????? ??????
|
||||
var hmacValue = new byte[hmac.HashSize / 8];
|
||||
hmacDataStream.Read(hmacValue, 0, hmacValue.Length);
|
||||
|
||||
// Вычисление реального значения HMAC для потока данных
|
||||
// ?????????? ????????? ???????? HMAC ??? ?????? ??????
|
||||
var expectedHmacValue = hmac.ComputeHash(hmacDataStream);
|
||||
|
||||
// Сравнение исходного HMAC с ожидаемым
|
||||
// ????????? ????????? HMAC ? ?????????
|
||||
return hmacValue.SequenceEqual(expectedHmacValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Gost_R3411;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_R3411
|
||||
{
|
||||
/// <summary>
|
||||
/// Вычисление хэша в соответствии с ГОСТ Р 34.11-2012/256.
|
||||
/// ?????????? ???? ? ???????????? ? ???? ? 34.11-2012/256.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает поток байт, вычисляет хэш в соответствии с ГОСТ Р 34.11-2012/256 и проверяет его корректность.
|
||||
/// ???? ??????? ????? ????, ????????? ??? ? ???????????? ? ???? ? 34.11-2012/256 ? ????????? ??? ????????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Вычисление хэша в соответствии с ГОСТ Р 34.11-2012/256")]
|
||||
public class Gost_R3411_2012_256_HashAlgorithmTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldComputeHash(ProviderType providerType)
|
||||
{
|
||||
@@ -34,15 +33,15 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
}
|
||||
|
||||
// Then
|
||||
Assert.IsNotNull(hashValue);
|
||||
Assert.AreEqual(256, 8 * hashValue.Length);
|
||||
Assert.NotNull(hashValue);
|
||||
Assert.Equal(256, 8 * hashValue.Length);
|
||||
}
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт
|
||||
// ????????? ????? ????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data to hash..."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
using System.Text;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Gost_28147_89;
|
||||
using GostCryptography.Gost_R3411;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_R3411
|
||||
{
|
||||
/// <summary>
|
||||
/// Использование PRF на базе алгоритма хэширования ГОСТ Р 34.11-2012/256.
|
||||
/// ????????????? PRF ?? ???? ????????? ??????????? ???? ? 34.11-2012/256.
|
||||
/// </summary>
|
||||
[TestFixture(Description = "Использование PRF на базе алгоритма хэширования ГОСТ Р 34.11-2012/256")]
|
||||
public class Gost_R3411_2012_256_PRFTest
|
||||
{
|
||||
private static readonly byte[] Label = { 1, 2, 3, 4, 5 };
|
||||
@@ -19,7 +18,7 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
private static readonly byte[] TestData = Encoding.UTF8.GetBytes("Some data to encrypt...");
|
||||
|
||||
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldDeriveBytes(ProviderType providerType)
|
||||
{
|
||||
@@ -40,18 +39,18 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
}
|
||||
|
||||
// Then
|
||||
Assert.IsNotNull(randomBytes1);
|
||||
Assert.IsNotNull(randomBytes2);
|
||||
Assert.IsNotNull(randomBytes3);
|
||||
Assert.AreEqual(256, 8 * randomBytes1.Length);
|
||||
Assert.AreEqual(256, 8 * randomBytes2.Length);
|
||||
Assert.AreEqual(256, 8 * randomBytes3.Length);
|
||||
Assert.NotNull(randomBytes1);
|
||||
Assert.NotNull(randomBytes2);
|
||||
Assert.NotNull(randomBytes3);
|
||||
Assert.Equal(256, 8 * randomBytes1.Length);
|
||||
Assert.Equal(256, 8 * randomBytes2.Length);
|
||||
Assert.Equal(256, 8 * randomBytes3.Length);
|
||||
CollectionAssert.AreNotEqual(randomBytes1, randomBytes2);
|
||||
CollectionAssert.AreNotEqual(randomBytes1, randomBytes3);
|
||||
CollectionAssert.AreNotEqual(randomBytes2, randomBytes3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldDeriveKey(ProviderType providerType)
|
||||
{
|
||||
@@ -78,9 +77,9 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
}
|
||||
|
||||
// Then
|
||||
Assert.IsNotNull(randomKey1);
|
||||
Assert.IsNotNull(randomKey2);
|
||||
Assert.IsNotNull(randomKey3);
|
||||
Assert.NotNull(randomKey1);
|
||||
Assert.NotNull(randomKey2);
|
||||
Assert.NotNull(randomKey3);
|
||||
AssertKeyIsValid(randomKey1);
|
||||
AssertKeyIsValid(randomKey2);
|
||||
AssertKeyIsValid(randomKey3);
|
||||
@@ -94,7 +93,7 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
{
|
||||
var encryptedData = EncryptData(key, TestData);
|
||||
var decryptedData = DecryptData(key, encryptedData);
|
||||
CollectionAssert.AreEqual(TestData, decryptedData);
|
||||
CollectionAssert.Equal(TestData, decryptedData);
|
||||
}
|
||||
|
||||
public static void AssertKeysAreNotEqual(GostSymmetricAlgorithm key1, GostSymmetricAlgorithm key2)
|
||||
@@ -117,4 +116,4 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
return transform.TransformFinalBlock(data, 0, data.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
@@ -6,20 +6,19 @@ using GostCryptography.Base;
|
||||
using GostCryptography.Gost_28147_89;
|
||||
using GostCryptography.Gost_R3411;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_R3411
|
||||
{
|
||||
/// <summary>
|
||||
/// Вычисление HMAC на базе алгоритма хэширования ГОСТ Р 34.11-2012/512 и общего симметричного ключа ГОСТ 28147-89.
|
||||
/// ?????????? HMAC ?? ???? ????????? ??????????? ???? ? 34.11-2012/512 ? ?????? ????????????? ????? ???? 28147-89.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест выполняет подпись и проверку подписи потока байт с использованием HMAC.
|
||||
/// ???? ????????? ??????? ? ???????? ??????? ?????? ???? ? ?????????????? HMAC.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Вычисление HMAC на базе алгоритма хэширования ГОСТ Р 34.11-2012/512 и общего симметричного ключа ГОСТ 28147-89")]
|
||||
public class Gost_R3411_2012_512_HMACTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldComputeHMAC(ProviderType providerType)
|
||||
{
|
||||
@@ -32,29 +31,29 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
var isValidHmacDataStream = VerifyHmacDataStream(sharedKey, hmacDataStream);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(isValidHmacDataStream);
|
||||
Assert.True(isValidHmacDataStream);
|
||||
}
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт
|
||||
// ????????? ????? ????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data to HMAC..."));
|
||||
}
|
||||
|
||||
private static Stream CreateHmacDataStream(GostSymmetricAlgorithm sharedKey, Stream dataStream)
|
||||
{
|
||||
// Создание объекта для вычисления HMAC
|
||||
// ???????? ??????? ??? ?????????? HMAC
|
||||
using (var hmac = new Gost_R3411_2012_512_HMAC(sharedKey))
|
||||
{
|
||||
// Вычисление HMAC для потока данных
|
||||
// ?????????? HMAC ??? ?????? ??????
|
||||
var hmacValue = hmac.ComputeHash(dataStream);
|
||||
|
||||
// Запись HMAC в начало выходного потока данных
|
||||
// ?????? HMAC ? ?????? ????????? ?????? ??????
|
||||
var hmacDataStream = new MemoryStream();
|
||||
hmacDataStream.Write(hmacValue, 0, hmacValue.Length);
|
||||
|
||||
// Копирование исходного потока данных в выходной поток
|
||||
// ??????????? ????????? ?????? ?????? ? ???????? ?????
|
||||
dataStream.Position = 0;
|
||||
dataStream.CopyTo(hmacDataStream);
|
||||
|
||||
@@ -66,19 +65,19 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
|
||||
private static bool VerifyHmacDataStream(GostSymmetricAlgorithm sharedKey, Stream hmacDataStream)
|
||||
{
|
||||
// Создание объекта для вычисления HMAC
|
||||
// ???????? ??????? ??? ?????????? HMAC
|
||||
using (var hmac = new Gost_R3411_2012_512_HMAC(sharedKey))
|
||||
{
|
||||
// Считывание HMAC из потока данных
|
||||
// ?????????? HMAC ?? ?????? ??????
|
||||
var hmacValue = new byte[hmac.HashSize / 8];
|
||||
hmacDataStream.Read(hmacValue, 0, hmacValue.Length);
|
||||
|
||||
// Вычисление реального значения HMAC для потока данных
|
||||
// ?????????? ????????? ???????? HMAC ??? ?????? ??????
|
||||
var expectedHmacValue = hmac.ComputeHash(hmacDataStream);
|
||||
|
||||
// Сравнение исходного HMAC с ожидаемым
|
||||
// ????????? ????????? HMAC ? ?????????
|
||||
return hmacValue.SequenceEqual(expectedHmacValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Gost_R3411;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_R3411
|
||||
{
|
||||
/// <summary>
|
||||
/// Вычисление хэша в соответствии с ГОСТ Р 34.11-2012/512.
|
||||
/// ?????????? ???? ? ???????????? ? ???? ? 34.11-2012/512.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает поток байт, вычисляет хэш в соответствии с ГОСТ Р 34.11-2012/512 и проверяет его корректность.
|
||||
/// ???? ??????? ????? ????, ????????? ??? ? ???????????? ? ???? ? 34.11-2012/512 ? ????????? ??? ????????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Вычисление хэша в соответствии с ГОСТ Р 34.11-2012/512")]
|
||||
public class Gost_R3411_2012_512_HashAlgorithmTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldComputeHash(ProviderType providerType)
|
||||
{
|
||||
@@ -34,15 +33,15 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
}
|
||||
|
||||
// Then
|
||||
Assert.IsNotNull(hashValue);
|
||||
Assert.AreEqual(512, 8 * hashValue.Length);
|
||||
Assert.NotNull(hashValue);
|
||||
Assert.Equal(512, 8 * hashValue.Length);
|
||||
}
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт
|
||||
// ????????? ????? ????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data to hash..."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
using System.Text;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Gost_28147_89;
|
||||
using GostCryptography.Gost_R3411;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_R3411
|
||||
{
|
||||
/// <summary>
|
||||
/// Использование PRF на базе алгоритма хэширования ГОСТ Р 34.11-2012/512.
|
||||
/// ????????????? PRF ?? ???? ????????? ??????????? ???? ? 34.11-2012/512.
|
||||
/// </summary>
|
||||
[TestFixture(Description = "Использование PRF на базе алгоритма хэширования ГОСТ Р 34.11-2012/512")]
|
||||
public class Gost_R3411_2012_512_PRFTest
|
||||
{
|
||||
private static readonly byte[] Label = { 1, 2, 3, 4, 5 };
|
||||
@@ -19,7 +18,7 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
private static readonly byte[] TestData = Encoding.UTF8.GetBytes("Some data to encrypt...");
|
||||
|
||||
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldDeriveBytes(ProviderType providerType)
|
||||
{
|
||||
@@ -40,18 +39,18 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
}
|
||||
|
||||
// Then
|
||||
Assert.IsNotNull(randomBytes1);
|
||||
Assert.IsNotNull(randomBytes2);
|
||||
Assert.IsNotNull(randomBytes3);
|
||||
Assert.AreEqual(512, 8 * randomBytes1.Length);
|
||||
Assert.AreEqual(512, 8 * randomBytes2.Length);
|
||||
Assert.AreEqual(512, 8 * randomBytes3.Length);
|
||||
Assert.NotNull(randomBytes1);
|
||||
Assert.NotNull(randomBytes2);
|
||||
Assert.NotNull(randomBytes3);
|
||||
Assert.Equal(512, 8 * randomBytes1.Length);
|
||||
Assert.Equal(512, 8 * randomBytes2.Length);
|
||||
Assert.Equal(512, 8 * randomBytes3.Length);
|
||||
CollectionAssert.AreNotEqual(randomBytes1, randomBytes2);
|
||||
CollectionAssert.AreNotEqual(randomBytes1, randomBytes3);
|
||||
CollectionAssert.AreNotEqual(randomBytes2, randomBytes3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldDeriveKey(ProviderType providerType)
|
||||
{
|
||||
@@ -78,9 +77,9 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
}
|
||||
|
||||
// Then
|
||||
Assert.IsNotNull(randomKey1);
|
||||
Assert.IsNotNull(randomKey2);
|
||||
Assert.IsNotNull(randomKey3);
|
||||
Assert.NotNull(randomKey1);
|
||||
Assert.NotNull(randomKey2);
|
||||
Assert.NotNull(randomKey3);
|
||||
AssertKeyIsValid(randomKey1);
|
||||
AssertKeyIsValid(randomKey2);
|
||||
AssertKeyIsValid(randomKey3);
|
||||
@@ -94,7 +93,7 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
{
|
||||
var encryptedData = EncryptData(key, TestData);
|
||||
var decryptedData = DecryptData(key, encryptedData);
|
||||
CollectionAssert.AreEqual(TestData, decryptedData);
|
||||
CollectionAssert.Equal(TestData, decryptedData);
|
||||
}
|
||||
|
||||
public static void AssertKeysAreNotEqual(GostSymmetricAlgorithm key1, GostSymmetricAlgorithm key2)
|
||||
@@ -117,4 +116,4 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
return transform.TransformFinalBlock(data, 0, data.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
@@ -6,20 +6,19 @@ using GostCryptography.Base;
|
||||
using GostCryptography.Gost_28147_89;
|
||||
using GostCryptography.Gost_R3411;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_R3411
|
||||
{
|
||||
/// <summary>
|
||||
/// Вычисление HMAC на базе алгоритма хэширования ГОСТ Р 34.11-94 и общего симметричного ключа ГОСТ 28147-89.
|
||||
/// ?????????? HMAC ?? ???? ????????? ??????????? ???? ? 34.11-94 ? ?????? ????????????? ????? ???? 28147-89.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест выполняет подпись и проверку подписи потока байт с использованием HMAC.
|
||||
/// ???? ????????? ??????? ? ???????? ??????? ?????? ???? ? ?????????????? HMAC.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Вычисление HMAC на базе алгоритма хэширования ГОСТ Р 34.11-94 и общего симметричного ключа ГОСТ 28147-89")]
|
||||
public class Gost_R3411_94_HMACTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldComputeHMAC(ProviderType providerType)
|
||||
{
|
||||
@@ -32,29 +31,29 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
var isValidHmacDataStream = VerifyHmacDataStream(sharedKey, hmacDataStream);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(isValidHmacDataStream);
|
||||
Assert.True(isValidHmacDataStream);
|
||||
}
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт
|
||||
// ????????? ????? ????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data to HMAC..."));
|
||||
}
|
||||
|
||||
private static Stream CreateHmacDataStream(GostSymmetricAlgorithm sharedKey, Stream dataStream)
|
||||
{
|
||||
// Создание объекта для вычисления HMAC
|
||||
// ???????? ??????? ??? ?????????? HMAC
|
||||
using (var hmac = new Gost_R3411_94_HMAC(sharedKey))
|
||||
{
|
||||
// Вычисление HMAC для потока данных
|
||||
// ?????????? HMAC ??? ?????? ??????
|
||||
var hmacValue = hmac.ComputeHash(dataStream);
|
||||
|
||||
// Запись HMAC в начало выходного потока данных
|
||||
// ?????? HMAC ? ?????? ????????? ?????? ??????
|
||||
var hmacDataStream = new MemoryStream();
|
||||
hmacDataStream.Write(hmacValue, 0, hmacValue.Length);
|
||||
|
||||
// Копирование исходного потока данных в выходной поток
|
||||
// ??????????? ????????? ?????? ?????? ? ???????? ?????
|
||||
dataStream.Position = 0;
|
||||
dataStream.CopyTo(hmacDataStream);
|
||||
|
||||
@@ -66,19 +65,19 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
|
||||
private static bool VerifyHmacDataStream(GostSymmetricAlgorithm sharedKey, Stream hmacDataStream)
|
||||
{
|
||||
// Создание объекта для вычисления HMAC
|
||||
// ???????? ??????? ??? ?????????? HMAC
|
||||
using (var hmac = new Gost_R3411_94_HMAC(sharedKey))
|
||||
{
|
||||
// Считывание HMAC из потока данных
|
||||
// ?????????? HMAC ?? ?????? ??????
|
||||
var hmacValue = new byte[hmac.HashSize / 8];
|
||||
hmacDataStream.Read(hmacValue, 0, hmacValue.Length);
|
||||
|
||||
// Вычисление реального значения HMAC для потока данных
|
||||
// ?????????? ????????? ???????? HMAC ??? ?????? ??????
|
||||
var expectedHmacValue = hmac.ComputeHash(hmacDataStream);
|
||||
|
||||
// Сравнение исходного HMAC с ожидаемым
|
||||
// ????????? ????????? HMAC ? ?????????
|
||||
return hmacValue.SequenceEqual(expectedHmacValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Gost_R3411;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_R3411
|
||||
{
|
||||
/// <summary>
|
||||
/// Вычисление хэша в соответствии с ГОСТ Р 34.11-94.
|
||||
/// ?????????? ???? ? ???????????? ? ???? ? 34.11-94.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает поток байт, вычисляет хэш в соответствии с ГОСТ Р 34.11-94 и проверяет его корректность.
|
||||
/// ???? ??????? ????? ????, ????????? ??? ? ???????????? ? ???? ? 34.11-94 ? ????????? ??? ????????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Вычисление хэша в соответствии с ГОСТ Р 34.11-94")]
|
||||
public class Gost_R3411_94_HashAlgorithmTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldComputeHash(ProviderType providerType)
|
||||
{
|
||||
@@ -34,15 +33,15 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
}
|
||||
|
||||
// Then
|
||||
Assert.IsNotNull(hashValue);
|
||||
Assert.AreEqual(256, 8 * hashValue.Length);
|
||||
Assert.NotNull(hashValue);
|
||||
Assert.Equal(256, 8 * hashValue.Length);
|
||||
}
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт
|
||||
// ????????? ????? ????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data to hash..."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
using System.Text;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Gost_28147_89;
|
||||
using GostCryptography.Gost_R3411;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Gost_R3411
|
||||
{
|
||||
/// <summary>
|
||||
/// Использование PRF на базе алгоритма хэширования ГОСТ Р 34.11-94.
|
||||
/// ????????????? PRF ?? ???? ????????? ??????????? ???? ? 34.11-94.
|
||||
/// </summary>
|
||||
[TestFixture(Description = "Использование PRF на базе алгоритма хэширования ГОСТ Р 34.11-94")]
|
||||
public class Gost_R3411_94_PRFTest
|
||||
{
|
||||
private static readonly byte[] Label = { 1, 2, 3, 4, 5 };
|
||||
@@ -19,7 +18,7 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
private static readonly byte[] TestData = Encoding.UTF8.GetBytes("Some data to encrypt...");
|
||||
|
||||
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldDeriveBytes(ProviderType providerType)
|
||||
{
|
||||
@@ -40,18 +39,18 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
}
|
||||
|
||||
// Then
|
||||
Assert.IsNotNull(randomBytes1);
|
||||
Assert.IsNotNull(randomBytes2);
|
||||
Assert.IsNotNull(randomBytes3);
|
||||
Assert.AreEqual(256, 8 * randomBytes1.Length);
|
||||
Assert.AreEqual(256, 8 * randomBytes2.Length);
|
||||
Assert.AreEqual(256, 8 * randomBytes3.Length);
|
||||
Assert.NotNull(randomBytes1);
|
||||
Assert.NotNull(randomBytes2);
|
||||
Assert.NotNull(randomBytes3);
|
||||
Assert.Equal(256, 8 * randomBytes1.Length);
|
||||
Assert.Equal(256, 8 * randomBytes2.Length);
|
||||
Assert.Equal(256, 8 * randomBytes3.Length);
|
||||
CollectionAssert.AreNotEqual(randomBytes1, randomBytes2);
|
||||
CollectionAssert.AreNotEqual(randomBytes1, randomBytes3);
|
||||
CollectionAssert.AreNotEqual(randomBytes2, randomBytes3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldDeriveKey(ProviderType providerType)
|
||||
{
|
||||
@@ -72,9 +71,9 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
}
|
||||
|
||||
// Then
|
||||
Assert.IsNotNull(randomKey1);
|
||||
Assert.IsNotNull(randomKey2);
|
||||
Assert.IsNotNull(randomKey3);
|
||||
Assert.NotNull(randomKey1);
|
||||
Assert.NotNull(randomKey2);
|
||||
Assert.NotNull(randomKey3);
|
||||
AssertKeyIsValid(randomKey1);
|
||||
AssertKeyIsValid(randomKey2);
|
||||
AssertKeyIsValid(randomKey3);
|
||||
@@ -88,7 +87,7 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
{
|
||||
var encryptedData = EncryptData(key, TestData);
|
||||
var decryptedData = DecryptData(key, encryptedData);
|
||||
CollectionAssert.AreEqual(TestData, decryptedData);
|
||||
CollectionAssert.Equal(TestData, decryptedData);
|
||||
}
|
||||
|
||||
public static void AssertKeysAreNotEqual(GostSymmetricAlgorithm key1, GostSymmetricAlgorithm key2)
|
||||
@@ -111,4 +110,4 @@ namespace GostCryptography.Tests.Gost_R3411
|
||||
return transform.TransformFinalBlock(data, 0, data.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography.Pkcs;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Pkcs
|
||||
{
|
||||
/// <summary>
|
||||
/// Шифрация и дешифрация сообщения CMS/PKCS#7.
|
||||
/// ???????? ? ?????????? ????????? CMS/PKCS#7.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает сообщение, шифрует его в формате CMS/PKCS#7, а затем дешифрует зашифрованное сообщение.
|
||||
/// ???? ??????? ?????????, ??????? ??? ? ??????? CMS/PKCS#7, ? ????? ????????? ????????????? ?????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Шифрация и дешифрация сообщения CMS/PKCS#7")]
|
||||
public class EnvelopedCmsEncryptTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldEncryptAndDecrypt(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -29,43 +28,43 @@ namespace GostCryptography.Tests.Pkcs
|
||||
var decryptedMessage = DecryptMessage(encryptedMessage);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(message.SequenceEqual(decryptedMessage));
|
||||
Assert.True(message.SequenceEqual(decryptedMessage));
|
||||
}
|
||||
|
||||
private static byte[] CreateMessage()
|
||||
{
|
||||
// Некоторое сообщение для подписи
|
||||
// ????????? ????????? ??? ???????
|
||||
|
||||
return Encoding.UTF8.GetBytes("Some message to sign...");
|
||||
}
|
||||
|
||||
private static byte[] EncryptMessage(X509Certificate2 certificate, byte[] message)
|
||||
{
|
||||
// Создание объекта для шифрования сообщения
|
||||
// ???????? ??????? ??? ?????????? ?????????
|
||||
var envelopedCms = new EnvelopedCms(new ContentInfo(message));
|
||||
|
||||
// Создание объект с информацией о получателе
|
||||
// ???????? ?????? ? ??????????? ? ??????????
|
||||
var recipient = new CmsRecipient(SubjectIdentifierType.IssuerAndSerialNumber, certificate);
|
||||
|
||||
// Шифрование сообщения CMS/PKCS#7
|
||||
// ?????????? ????????? CMS/PKCS#7
|
||||
envelopedCms.Encrypt(recipient);
|
||||
|
||||
// Создание сообщения CMS/PKCS#7
|
||||
// ???????? ????????? CMS/PKCS#7
|
||||
return envelopedCms.Encode();
|
||||
}
|
||||
|
||||
private static byte[] DecryptMessage(byte[] encryptedMessage)
|
||||
{
|
||||
// Создание объекта для расшифровки сообщения
|
||||
// ???????? ??????? ??? ??????????? ?????????
|
||||
var envelopedCms = new EnvelopedCms();
|
||||
|
||||
// Чтение сообщения CMS/PKCS#7
|
||||
// ?????? ????????? CMS/PKCS#7
|
||||
envelopedCms.Decode(encryptedMessage);
|
||||
|
||||
// Расшифровка сообщения CMS/PKCS#7
|
||||
// ??????????? ????????? CMS/PKCS#7
|
||||
envelopedCms.Decrypt(envelopedCms.RecipientInfos[0]);
|
||||
|
||||
return envelopedCms.ContentInfo.Content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
using System.Security.Cryptography.Pkcs;
|
||||
using System.Security.Cryptography.Pkcs;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Pkcs;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Pkcs
|
||||
{
|
||||
/// <summary>
|
||||
/// Подпись и проверка отсоединенной подписи сообщения CMS/PKCS#7.
|
||||
/// ??????? ? ???????? ????????????? ??????? ????????? CMS/PKCS#7.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает сообщение, формирует отсоединенную подпись сообщения в формате CMS/PKCS#7,
|
||||
/// а затем проверяет подпись полученную цифровую подпись.
|
||||
/// ???? ??????? ?????????, ????????? ????????????? ??????? ????????? ? ??????? CMS/PKCS#7,
|
||||
/// ? ????? ????????? ??????? ?????????? ???????? ???????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Подпись и проверка отсоединенной подписи сообщения CMS/PKCS#7")]
|
||||
public class SignedCmsDetachedSignTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldSign(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -31,45 +30,45 @@ namespace GostCryptography.Tests.Pkcs
|
||||
var isValidDetachedSignature = VerifyMessage(message, detachedSignature);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(isValidDetachedSignature);
|
||||
Assert.True(isValidDetachedSignature);
|
||||
}
|
||||
|
||||
private static byte[] CreateMessage()
|
||||
{
|
||||
// Некоторое сообщение для подписи
|
||||
// ????????? ????????? ??? ???????
|
||||
|
||||
return Encoding.UTF8.GetBytes("Some message to sign...");
|
||||
}
|
||||
|
||||
private static byte[] SignMessage(X509Certificate2 certificate, byte[] message)
|
||||
{
|
||||
// Создание объекта для подписи сообщения
|
||||
// ???????? ??????? ??? ??????? ?????????
|
||||
var signedCms = new GostSignedCms(new ContentInfo(message), true);
|
||||
|
||||
// Создание объект с информацией о подписчике
|
||||
// ???????? ?????? ? ??????????? ? ??????????
|
||||
var signer = new CmsSigner(certificate);
|
||||
|
||||
// Включение информации только о конечном сертификате (только для теста)
|
||||
// ????????? ?????????? ?????? ? ???????? ??????????? (?????? ??? ?????)
|
||||
signer.IncludeOption = X509IncludeOption.EndCertOnly;
|
||||
|
||||
// Создание подписи для сообщения CMS/PKCS#7
|
||||
// ???????? ??????? ??? ????????? CMS/PKCS#7
|
||||
signedCms.ComputeSignature(signer);
|
||||
|
||||
// Создание подписи CMS/PKCS#7
|
||||
// ???????? ??????? CMS/PKCS#7
|
||||
return signedCms.Encode();
|
||||
}
|
||||
|
||||
private static bool VerifyMessage(byte[] message, byte[] detachedSignature)
|
||||
{
|
||||
// Создание объекта для проверки подписи сообщения
|
||||
// ???????? ??????? ??? ???????? ??????? ?????????
|
||||
var signedCms = new GostSignedCms(new ContentInfo(message), true);
|
||||
|
||||
// Чтение подписи CMS/PKCS#7
|
||||
// ?????? ??????? CMS/PKCS#7
|
||||
signedCms.Decode(detachedSignature);
|
||||
|
||||
try
|
||||
{
|
||||
// Проверка подписи CMS/PKCS#7
|
||||
// ???????? ??????? CMS/PKCS#7
|
||||
signedCms.CheckSignature(true);
|
||||
}
|
||||
catch
|
||||
@@ -80,4 +79,4 @@ namespace GostCryptography.Tests.Pkcs
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
using System.Security.Cryptography.Pkcs;
|
||||
using System.Security.Cryptography.Pkcs;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Pkcs;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Pkcs
|
||||
{
|
||||
/// <summary>
|
||||
/// Подпись и проверка подписи сообщения CMS/PKCS#7.
|
||||
/// ??????? ? ???????? ??????? ????????? CMS/PKCS#7.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает сообщение, формирует подписанное сообщение в формате CMS/PKCS#7,
|
||||
/// исключая информацию о сертификате подписчика с целью минимизации размера сообщения,
|
||||
/// а затем проверяет подпись полученную цифровую подпись.
|
||||
/// ???? ??????? ?????????, ????????? ??????????? ????????? ? ??????? CMS/PKCS#7,
|
||||
/// ???????? ?????????? ? ??????????? ?????????? ? ????? ??????????? ??????? ?????????,
|
||||
/// ? ????? ????????? ??????? ?????????? ???????? ???????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Подпись и проверка подписи сообщения CMS/PKCS#7")]
|
||||
public class SignedCmsSignAndExcludeCertificates
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldSign(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -32,51 +31,51 @@ namespace GostCryptography.Tests.Pkcs
|
||||
var isValidSignedMessage = VerifyMessage(certificate, signedMessage);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(isValidSignedMessage);
|
||||
Assert.True(isValidSignedMessage);
|
||||
}
|
||||
|
||||
private static byte[] CreateMessage()
|
||||
{
|
||||
// Некоторое сообщение для подписи
|
||||
// ????????? ????????? ??? ???????
|
||||
|
||||
return Encoding.UTF8.GetBytes("Some message to sign...");
|
||||
}
|
||||
|
||||
private static byte[] SignMessage(X509Certificate2 certificate, byte[] message)
|
||||
{
|
||||
// Создание объекта для подписи сообщения
|
||||
// ???????? ??????? ??? ??????? ?????????
|
||||
var signedCms = new GostSignedCms(new ContentInfo(message));
|
||||
|
||||
// Создание объект с информацией о подписчике
|
||||
// ???????? ?????? ? ??????????? ? ??????????
|
||||
var signer = new CmsSigner(certificate);
|
||||
|
||||
// Включение информации только о конечном сертификате (только для теста)
|
||||
// ????????? ?????????? ?????? ? ???????? ??????????? (?????? ??? ?????)
|
||||
signer.IncludeOption = X509IncludeOption.EndCertOnly;
|
||||
|
||||
// Создание подписи для сообщения CMS/PKCS#7
|
||||
// ???????? ??????? ??? ????????? CMS/PKCS#7
|
||||
signedCms.ComputeSignature(signer);
|
||||
|
||||
// Исключение сертификатов для уменьшения размера сообщения
|
||||
// ?????????? ???????????? ??? ?????????? ??????? ?????????
|
||||
signedCms.RemoveCertificates();
|
||||
|
||||
// Создание сообщения CMS/PKCS#7
|
||||
// ???????? ????????? CMS/PKCS#7
|
||||
return signedCms.Encode();
|
||||
}
|
||||
|
||||
private static bool VerifyMessage(X509Certificate2 certificate, byte[] signedMessage)
|
||||
{
|
||||
// Создание объекта для проверки подписи сообщения
|
||||
// ???????? ??????? ??? ???????? ??????? ?????????
|
||||
var signedCms = new GostSignedCms();
|
||||
|
||||
// Чтение сообщения CMS/PKCS#7
|
||||
// ?????? ????????? CMS/PKCS#7
|
||||
signedCms.Decode(signedMessage);
|
||||
|
||||
// Список сертификатов подписчика
|
||||
// ?????? ???????????? ??????????
|
||||
var signerCerts = new X509Certificate2Collection(certificate);
|
||||
|
||||
try
|
||||
{
|
||||
// Проверка подписи сообщения CMS/PKCS#7
|
||||
// ???????? ??????? ????????? CMS/PKCS#7
|
||||
signedCms.CheckSignature(signerCerts, true);
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
using System.Security.Cryptography.Pkcs;
|
||||
using System.Security.Cryptography.Pkcs;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Pkcs;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Pkcs
|
||||
{
|
||||
/// <summary>
|
||||
/// Подпись и проверка подписи сообщения CMS/PKCS#7.
|
||||
/// ??????? ? ???????? ??????? ????????? CMS/PKCS#7.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает сообщение, формирует подписанное сообщение в формате CMS/PKCS#7,
|
||||
/// а затем проверяет подпись полученную цифровую подпись.
|
||||
/// ???? ??????? ?????????, ????????? ??????????? ????????? ? ??????? CMS/PKCS#7,
|
||||
/// ? ????? ????????? ??????? ?????????? ???????? ???????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Подпись и проверка подписи сообщения CMS/PKCS#7")]
|
||||
public class SignedCmsSignTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldSign(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -31,45 +30,45 @@ namespace GostCryptography.Tests.Pkcs
|
||||
var isValidSignedMessage = VerifyMessage(signedMessage);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(isValidSignedMessage);
|
||||
Assert.True(isValidSignedMessage);
|
||||
}
|
||||
|
||||
private static byte[] CreateMessage()
|
||||
{
|
||||
// Некоторое сообщение для подписи
|
||||
// ????????? ????????? ??? ???????
|
||||
|
||||
return Encoding.UTF8.GetBytes("Some message to sign...");
|
||||
}
|
||||
|
||||
private static byte[] SignMessage(X509Certificate2 certificate, byte[] message)
|
||||
{
|
||||
// Создание объекта для подписи сообщения
|
||||
// ???????? ??????? ??? ??????? ?????????
|
||||
var signedCms = new GostSignedCms(new ContentInfo(message));
|
||||
|
||||
// Создание объект с информацией о подписчике
|
||||
// ???????? ?????? ? ??????????? ? ??????????
|
||||
var signer = new CmsSigner(certificate);
|
||||
|
||||
// Включение информации только о конечном сертификате (только для теста)
|
||||
// ????????? ?????????? ?????? ? ???????? ??????????? (?????? ??? ?????)
|
||||
signer.IncludeOption = X509IncludeOption.EndCertOnly;
|
||||
|
||||
// Создание подписи для сообщения CMS/PKCS#7
|
||||
// ???????? ??????? ??? ????????? CMS/PKCS#7
|
||||
signedCms.ComputeSignature(signer);
|
||||
|
||||
// Создание сообщения CMS/PKCS#7
|
||||
// ???????? ????????? CMS/PKCS#7
|
||||
return signedCms.Encode();
|
||||
}
|
||||
|
||||
private static bool VerifyMessage(byte[] signedMessage)
|
||||
{
|
||||
// Создание объекта для проверки подписи сообщения
|
||||
// ???????? ??????? ??? ???????? ??????? ?????????
|
||||
var signedCms = new GostSignedCms();
|
||||
|
||||
// Чтение сообщения CMS/PKCS#7
|
||||
// ?????? ????????? CMS/PKCS#7
|
||||
signedCms.Decode(signedMessage);
|
||||
|
||||
try
|
||||
{
|
||||
// Проверка подписи сообщения CMS/PKCS#7
|
||||
// ???????? ??????? ????????? CMS/PKCS#7
|
||||
signedCms.CheckSignature(true);
|
||||
}
|
||||
catch
|
||||
@@ -80,4 +79,4 @@ namespace GostCryptography.Tests.Pkcs
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//------------------------------------------------------------------------------
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Base;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Sign
|
||||
{
|
||||
/// <summary>
|
||||
/// Подпись и проверка подписи потока байт с помощью сертификата.
|
||||
/// ??????? ? ???????? ??????? ?????? ???? ? ??????? ???????????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает поток байт, вычисляет цифровую подпись потока байт с использованием закрытого ключа сертификата,
|
||||
/// а затем с помощью открытого ключа сертификата проверяет полученную подпись.
|
||||
/// ???? ??????? ????? ????, ????????? ???????? ??????? ?????? ???? ? ?????????????? ????????? ????? ???????????,
|
||||
/// ? ????? ? ??????? ????????? ????? ??????????? ????????? ?????????? ???????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Подпись и проверка подписи потока байт с помощью сертификата")]
|
||||
public class SignDataStreamCertificateTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldSignDataStream(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -37,12 +36,12 @@ namespace GostCryptography.Tests.Sign
|
||||
var isValidSignature = VerifySignature(publicKey, dataStream, signature);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(isValidSignature);
|
||||
Assert.True(isValidSignature);
|
||||
}
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт для подписи
|
||||
// ????????? ????? ???? ??? ???????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data to sign..."));
|
||||
}
|
||||
@@ -71,4 +70,4 @@ namespace GostCryptography.Tests.Sign
|
||||
return publicKey.VerifySignature(hash, signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
@@ -6,23 +6,22 @@ using System.Text;
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Config;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Sign
|
||||
{
|
||||
/// <summary>
|
||||
/// Подпись и проверка подписи потока байт с помощью сертификата и информации об алгоритме цифровой подписи
|
||||
/// ??????? ? ???????? ??????? ?????? ???? ? ??????? ??????????? ? ?????????? ?? ????????? ???????? ???????
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает поток байт, вычисляет цифровую подпись потока байт с использованием закрытого ключа сертификата,
|
||||
/// а затем с помощью открытого ключа сертификата проверяет полученную подпись. Для вычисления цифровой подписи
|
||||
/// и ее проверки используется информация об алгоритме цифровой подписи <see cref="SignatureDescription"/>,
|
||||
/// получаемая с помощью метода <see cref="GostCryptoConfig.CreateFromName"/>.
|
||||
/// ???? ??????? ????? ????, ????????? ???????? ??????? ?????? ???? ? ?????????????? ????????? ????? ???????????,
|
||||
/// ? ????? ? ??????? ????????? ????? ??????????? ????????? ?????????? ???????. ??? ?????????? ???????? ???????
|
||||
/// ? ?? ???????? ???????????? ?????????? ?? ????????? ???????? ??????? <see cref="SignatureDescription"/>,
|
||||
/// ?????????? ? ??????? ?????? <see cref="GostCryptoConfig.CreateFromName"/>.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Подпись и проверка подписи потока байт с помощью сертификата и информации об алгоритме цифровой подписи")]
|
||||
public class SignDataStreamSignatureDescriptionTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldSignDataStream(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -41,12 +40,12 @@ namespace GostCryptography.Tests.Sign
|
||||
var isValidSignature = VerifySignature(publicKey, dataStream, signature);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(isValidSignature);
|
||||
Assert.True(isValidSignature);
|
||||
}
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт для подписи
|
||||
// ????????? ????? ???? ??? ???????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data to sign..."));
|
||||
}
|
||||
@@ -85,4 +84,4 @@ namespace GostCryptography.Tests.Sign
|
||||
return deformatter.VerifySignature(hash, signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
|
||||
using GostCryptography.Base;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Sign
|
||||
{
|
||||
/// <summary>
|
||||
/// Подпись и проверка подписи потока байт с помощью сертификата и классов форматирования.
|
||||
/// ??????? ? ???????? ??????? ?????? ???? ? ??????? ??????????? ? ??????? ??????????????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает поток байт, вычисляет цифровую подпись потока байт с использованием закрытого ключа сертификата,
|
||||
/// а затем с помощью открытого ключа сертификата проверяет полученную подпись. Для вычисления цифровой подписи
|
||||
/// используется класс <see cref="GostSignatureFormatter"/>, для проверки цифровой подписи используется класс
|
||||
/// ???? ??????? ????? ????, ????????? ???????? ??????? ?????? ???? ? ?????????????? ????????? ????? ???????????,
|
||||
/// ? ????? ? ??????? ????????? ????? ??????????? ????????? ?????????? ???????. ??? ?????????? ???????? ???????
|
||||
/// ???????????? ????? <see cref="GostSignatureFormatter"/>, ??? ???????? ???????? ??????? ???????????? ?????
|
||||
/// <see cref="GostSignatureDeformatter"/>.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Подпись и проверка подписи потока байт с помощью сертификата и классов форматирования")]
|
||||
public class SignDataStreamSignatureFormatterTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldSignDataStream(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -39,12 +38,12 @@ namespace GostCryptography.Tests.Sign
|
||||
var isValidSignature = VerifySignature(publicKey, dataStream, signature);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(isValidSignature);
|
||||
Assert.True(isValidSignature);
|
||||
}
|
||||
|
||||
private static Stream CreateDataStream()
|
||||
{
|
||||
// Некоторый поток байт для подписи
|
||||
// ????????? ????? ???? ??? ???????
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes("Some data to sign..."));
|
||||
}
|
||||
@@ -77,4 +76,4 @@ namespace GostCryptography.Tests.Sign
|
||||
return deformatter.VerifySignature(hash, signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace GostCryptography.Tests
|
||||
{
|
||||
@@ -18,4 +18,4 @@ namespace GostCryptography.Tests
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
@@ -14,9 +14,9 @@ namespace GostCryptography.Tests
|
||||
{
|
||||
Providers = new[] { GostCryptoConfig.ProviderType, GostCryptoConfig.ProviderType_2012_512, GostCryptoConfig.ProviderType_2012_1024 };
|
||||
|
||||
var gost_R3410_2001 = new TestCertificateInfo("ГОСТ Р 34.10-2001", FindGostCertificate(filter: c => c.IsGost_R3410_2001()));
|
||||
var gost_R3410_2012_256 = new TestCertificateInfo("ГОСТ Р 34.10-2012/256", FindGostCertificate(filter: c => c.IsGost_R3410_2012_256()));
|
||||
var gost_R3410_2012_512 = new TestCertificateInfo("ГОСТ Р 34.10-2012/512", FindGostCertificate(filter: c => c.IsGost_R3410_2012_512()));
|
||||
var gost_R3410_2001 = new TestCertificateInfo("???? ? 34.10-2001", FindGostCertificate(filter: c => c.IsGost_R3410_2001()));
|
||||
var gost_R3410_2012_256 = new TestCertificateInfo("???? ? 34.10-2012/256", FindGostCertificate(filter: c => c.IsGost_R3410_2012_256()));
|
||||
var gost_R3410_2012_512 = new TestCertificateInfo("???? ? 34.10-2012/512", FindGostCertificate(filter: c => c.IsGost_R3410_2012_512()));
|
||||
|
||||
var gost_R3410_Certificates = new List<TestCertificateInfo> { gost_R3410_2001, gost_R3410_2012_256, gost_R3410_2012_512 };
|
||||
var gost_R3410_2001_Certificates = new List<TestCertificateInfo> { gost_R3410_2001 };
|
||||
@@ -76,4 +76,4 @@ namespace GostCryptography.Tests
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
@@ -10,202 +10,16 @@ using GostCryptography.Gost_28147_89;
|
||||
using GostCryptography.Tests.Properties;
|
||||
using GostCryptography.Xml;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Xml.Encrypt
|
||||
{
|
||||
/// <summary>
|
||||
/// Шифрация и дешифрация XML для широковещательной рассылки.
|
||||
/// ???????? ? ?????????? XML ??? ????????????????? ????????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает XML-документ, выборочно шифрует элементы данного документа, а затем дешифрует полученный зашифрованный документ.
|
||||
/// Элементы шифруются с использованием случайного сессионного ключа, который в свою очередь кодируется (экспортируется)
|
||||
/// с использованием публичного ключа сертификата получателя. Расшифровка документа происходит с использованием первого
|
||||
/// найденного секретного ключа сертификата получателя.
|
||||
/// ???? ??????? XML-????????, ????????? ??????? ???????? ??????? ?????????, ? ????? ????????? ?????????? ????????????? ????????.
|
||||
/// ???????? ????????? ? ?????????????? ?????????? ??????????? ?????, ??????? ? ???? ??????? ?????????? (??????????????)
|
||||
/// ? ?????????????? ?????????? ????? ??????????? ??????????. ??????????? ????????? ?????????? ? ?????????????? ???????
|
||||
/// ?????????? ?????????? ????? ??????????? ??????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Шифрация и дешифрация XML для широковещательной рассылки")]
|
||||
public sealed class EncryptedXmlBroadcastTest
|
||||
{
|
||||
[Test]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldEncryptXml(TestCertificateInfo testCase)
|
||||
{
|
||||
// Given
|
||||
var certificate = testCase.Certificate;
|
||||
var certificates = new[] { certificate };
|
||||
var xmlDocument = CreateXmlDocument();
|
||||
var expectedXml = xmlDocument.OuterXml;
|
||||
|
||||
// When
|
||||
var encryptedXmlDocument = EncryptXmlDocument(xmlDocument, certificates);
|
||||
var decryptedXmlDocument = DecryptXmlDocument(encryptedXmlDocument);
|
||||
var actualXml = decryptedXmlDocument.OuterXml;
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(expectedXml, actualXml);
|
||||
}
|
||||
|
||||
private static XmlDocument CreateXmlDocument()
|
||||
{
|
||||
var document = new XmlDocument();
|
||||
document.LoadXml(Resources.EncryptedXmlExample);
|
||||
return document;
|
||||
}
|
||||
|
||||
private static XmlDocument EncryptXmlDocument(XmlDocument xmlDocument, IEnumerable<X509Certificate2> certificates)
|
||||
{
|
||||
// Создание объекта для шифрации XML
|
||||
var encryptedXml = new GostEncryptedXml();
|
||||
|
||||
// Поиск элементов для шифрации
|
||||
var elements = xmlDocument.SelectNodes("//SomeElement[@Encrypt='true']");
|
||||
|
||||
if (elements != null)
|
||||
{
|
||||
var elementIndex = 0;
|
||||
|
||||
foreach (XmlElement element in elements)
|
||||
{
|
||||
// Формирование элемента EncryptedData
|
||||
var elementEncryptedData = new EncryptedData();
|
||||
elementEncryptedData.Id = "EncryptedElement" + elementIndex++;
|
||||
elementEncryptedData.Type = EncryptedXml.XmlEncElementUrl;
|
||||
elementEncryptedData.KeyInfo = new KeyInfo();
|
||||
|
||||
using (var sessionKey = new Gost_28147_89_SymmetricAlgorithm())
|
||||
{
|
||||
elementEncryptedData.EncryptionMethod = new EncryptionMethod(sessionKey.AlgorithmName);
|
||||
|
||||
// Шифрация элемента с использованием симметричного ключа
|
||||
var encryptedElement = encryptedXml.EncryptData(element, sessionKey, false);
|
||||
|
||||
foreach (var certificate in certificates)
|
||||
{
|
||||
// Шифрация сессионного ключа с использованием открытого ключа сертификата
|
||||
var encryptedSessionKeyData = GostEncryptedXml.EncryptKey(sessionKey, (GostAsymmetricAlgorithm)certificate.GetPublicKeyAlgorithm());
|
||||
|
||||
// Формирование информации о зашифрованном сессионном ключе
|
||||
var encryptedSessionKey = new EncryptedKey();
|
||||
encryptedSessionKey.CipherData = new CipherData(encryptedSessionKeyData);
|
||||
encryptedSessionKey.EncryptionMethod = new EncryptionMethod(GostEncryptedXml.XmlEncGostCryptoProKeyExportUrl);
|
||||
encryptedSessionKey.AddReference(new DataReference { Uri = "#" + elementEncryptedData.Id });
|
||||
encryptedSessionKey.KeyInfo.AddClause(new KeyInfoX509Data(certificate));
|
||||
|
||||
// Добавление ссылки на зашифрованный ключ, используемый при шифровании данных
|
||||
elementEncryptedData.KeyInfo.AddClause(new KeyInfoEncryptedKey(encryptedSessionKey));
|
||||
}
|
||||
|
||||
// Установка зашифрованных данных у объекта EncryptedData
|
||||
elementEncryptedData.CipherData.CipherValue = encryptedElement;
|
||||
}
|
||||
|
||||
// Замена элемента его зашифрованным представлением
|
||||
GostEncryptedXml.ReplaceElement(element, elementEncryptedData, false);
|
||||
}
|
||||
}
|
||||
|
||||
return xmlDocument;
|
||||
}
|
||||
|
||||
private static XmlDocument DecryptXmlDocument(XmlDocument encryptedXmlDocument)
|
||||
{
|
||||
// Создание объекта для дешифрации XML
|
||||
var encryptedXml = new GostEncryptedXml(encryptedXmlDocument);
|
||||
|
||||
var nsManager = new XmlNamespaceManager(encryptedXmlDocument.NameTable);
|
||||
nsManager.AddNamespace("enc", EncryptedXml.XmlEncNamespaceUrl);
|
||||
|
||||
// Поиск всех зашифрованных XML-элементов
|
||||
var encryptedDataList = encryptedXmlDocument.SelectNodes("//enc:EncryptedData", nsManager);
|
||||
|
||||
if (encryptedDataList != null)
|
||||
{
|
||||
foreach (XmlElement encryptedData in encryptedDataList)
|
||||
{
|
||||
// Загрузка элемента EncryptedData
|
||||
var elementEncryptedData = new EncryptedData();
|
||||
elementEncryptedData.LoadXml(encryptedData);
|
||||
|
||||
// Извлечение симметричный ключ для расшифровки элемента EncryptedData
|
||||
var sessionKey = GetDecryptionKey(elementEncryptedData);
|
||||
|
||||
if (sessionKey != null)
|
||||
{
|
||||
// Расшифровка элемента EncryptedData
|
||||
var decryptedData = encryptedXml.DecryptData(elementEncryptedData, sessionKey);
|
||||
|
||||
// Замена элемента EncryptedData его расшифрованным представлением
|
||||
encryptedXml.ReplaceData(encryptedData, decryptedData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return encryptedXmlDocument;
|
||||
}
|
||||
|
||||
private static SymmetricAlgorithm GetDecryptionKey(EncryptedData encryptedData)
|
||||
{
|
||||
SymmetricAlgorithm sessionKey = null;
|
||||
|
||||
foreach (var keyInfo in encryptedData.KeyInfo)
|
||||
{
|
||||
if (keyInfo is KeyInfoEncryptedKey)
|
||||
{
|
||||
var encryptedKey = ((KeyInfoEncryptedKey)keyInfo).EncryptedKey;
|
||||
|
||||
if (encryptedKey != null)
|
||||
{
|
||||
foreach (var ekKeyInfo in encryptedKey.KeyInfo)
|
||||
{
|
||||
if (ekKeyInfo is KeyInfoX509Data)
|
||||
{
|
||||
var certificates = ((KeyInfoX509Data)ekKeyInfo).Certificates;
|
||||
|
||||
// Поиск закрытого ключа для дешифрации сессионного ключа
|
||||
var privateKey = FindPrivateKey(certificates);
|
||||
|
||||
if (privateKey != null)
|
||||
{
|
||||
// Дешифрация сессионного ключа с использованием закрытого ключа сертификата
|
||||
sessionKey = GostEncryptedXml.DecryptKey(encryptedKey.CipherData.CipherValue, privateKey);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
private static GostAsymmetricAlgorithm FindPrivateKey(IEnumerable certificates)
|
||||
{
|
||||
// Какая-то логика поиска закрытого ключа
|
||||
|
||||
GostAsymmetricAlgorithm privateKey = null;
|
||||
|
||||
var store = new X509Store(TestConfig.DefaultStoreName, TestConfig.DefaultStoreLocation);
|
||||
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
|
||||
var storeCertificates = store.Certificates;
|
||||
store.Close();
|
||||
|
||||
foreach (X509Certificate2 certificate in certificates)
|
||||
{
|
||||
var index = storeCertificates.IndexOf(certificate);
|
||||
|
||||
if (index >= 0)
|
||||
{
|
||||
privateKey = storeCertificates[index].GetPrivateKeyAlgorithm() as GostAsymmetricAlgorithm;
|
||||
|
||||
if (privateKey != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return privateKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +1,17 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Xml;
|
||||
|
||||
using GostCryptography.Tests.Properties;
|
||||
using GostCryptography.Xml;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Xml.Encrypt
|
||||
{
|
||||
/// <summary>
|
||||
/// Шифрация и дешифрация XML документа с использованием сертификата.
|
||||
/// ???????? ? ?????????? XML ????????? ? ?????????????? ???????????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает XML-документ, выборочно шифрует элементы данного документа с использованием сертификата,
|
||||
/// а затем дешифрует полученный зашифрованный документ.
|
||||
/// ???? ??????? XML-????????, ????????? ??????? ???????? ??????? ????????? ? ?????????????? ???????????,
|
||||
/// ? ????? ????????? ?????????? ????????????? ????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Шифрация и дешифрация XML документа с использованием сертификата")]
|
||||
public sealed class EncryptedXmlCertificateTest
|
||||
{
|
||||
[Test]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldEncryptXml(TestCertificateInfo testCase)
|
||||
{
|
||||
// Given
|
||||
var certificate = testCase.Certificate;
|
||||
var xmlDocument = CreateXmlDocument();
|
||||
var expectedXml = xmlDocument.OuterXml;
|
||||
|
||||
// When
|
||||
var encryptedXmlDocument = EncryptXmlDocument(xmlDocument, certificate);
|
||||
var decryptedXmlDocument = DecryptXmlDocument(encryptedXmlDocument);
|
||||
var actualXml = decryptedXmlDocument.OuterXml;
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(expectedXml, actualXml);
|
||||
}
|
||||
|
||||
private static XmlDocument CreateXmlDocument()
|
||||
{
|
||||
var document = new XmlDocument();
|
||||
document.LoadXml(Resources.EncryptedXmlExample);
|
||||
return document;
|
||||
}
|
||||
|
||||
private static XmlDocument EncryptXmlDocument(XmlDocument xmlDocument, X509Certificate2 certificate)
|
||||
{
|
||||
// Создание объекта для шифрации XML
|
||||
var encryptedXml = new GostEncryptedXml();
|
||||
|
||||
// Поиск элементов для шифрации
|
||||
var elements = xmlDocument.SelectNodes("//SomeElement[@Encrypt='true']");
|
||||
|
||||
if (elements != null)
|
||||
{
|
||||
foreach (XmlElement element in elements)
|
||||
{
|
||||
// Шифрация элемента
|
||||
var elementEncryptedData = encryptedXml.Encrypt(element, certificate);
|
||||
|
||||
// Замена элемента его зашифрованным представлением
|
||||
GostEncryptedXml.ReplaceElement(element, elementEncryptedData, false);
|
||||
}
|
||||
}
|
||||
|
||||
return xmlDocument;
|
||||
}
|
||||
|
||||
private static XmlDocument DecryptXmlDocument(XmlDocument encryptedXmlDocument)
|
||||
{
|
||||
// Создание объекта для дешифрации XML
|
||||
var encryptedXml = new GostEncryptedXml(encryptedXmlDocument);
|
||||
|
||||
// Расшифровка зашифрованных элементов документа
|
||||
encryptedXml.DecryptDocument();
|
||||
|
||||
return encryptedXmlDocument;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.Xml;
|
||||
using System.Xml;
|
||||
|
||||
@@ -8,31 +8,30 @@ using GostCryptography.Gost_R3410;
|
||||
using GostCryptography.Tests.Properties;
|
||||
using GostCryptography.Xml;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Xml.Encrypt
|
||||
{
|
||||
/// <summary>
|
||||
/// Шифрация и дешифрация XML с использованием контейнера ключей.
|
||||
/// ???????? ? ?????????? XML ? ?????????????? ?????????? ??????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест имитирует обмен данными между условным отправителем, который шифрует заданный XML-документ, и условным получателем, который дешифрует
|
||||
/// зашифрованный XML-документ. Шифрация и дешифрация осуществляется без использования сертификатов. Шифрация осуществляется с использованием
|
||||
/// случайного симметричного ключа, который в свою очередь шифруется с использованием открытого ключа получателя. Соответственно для дешифрации
|
||||
/// данных сначала расшифровывается случайный симметричный ключ с использованием закрытого ключа получателя.
|
||||
/// ???? ????????? ????? ??????? ????? ???????? ????????????, ??????? ??????? ???????? XML-????????, ? ???????? ???????????, ??????? ?????????
|
||||
/// ????????????? XML-????????. ???????? ? ?????????? ?????????????? ??? ????????????? ????????????. ???????? ?????????????? ? ??????????????
|
||||
/// ?????????? ????????????? ?????, ??????? ? ???? ??????? ????????? ? ?????????????? ????????? ????? ??????????. ?????????????? ??? ??????????
|
||||
/// ?????? ??????? ???????????????? ????????? ???????????? ???? ? ?????????????? ????????? ????? ??????????.
|
||||
///
|
||||
/// Перед началом теста имитируется передача получателем своего открытого ключа отправителю. Для этого получатель извлекает информацию о закрытом
|
||||
/// ключе из контейнера ключей, формирует закрытый ключ для дешифрации XML и условно передает (экспортирует) отправителю информацию о своем открытом
|
||||
/// ключе. Отправитель в свою очередь принимает (импортирует) от получателя информацию о его открытом ключе и формирует открытый ключ для шифрации XML.
|
||||
/// ????? ??????? ????? ??????????? ???????? ??????????? ?????? ????????? ????? ???????????. ??? ????? ?????????? ????????? ?????????? ? ????????
|
||||
/// ????? ?? ?????????? ??????, ????????? ???????? ???? ??? ?????????? XML ? ??????? ???????? (????????????) ??????????? ?????????? ? ????? ????????
|
||||
/// ?????. ??????????? ? ???? ??????? ????????? (???????????) ?? ?????????? ?????????? ? ??? ???????? ????? ? ????????? ???????? ???? ??? ???????? XML.
|
||||
///
|
||||
/// Тест создает XML-документ, выборочно шифрует элементы данного документа с использованием случайного симметричного ключа, а затем дешифрует
|
||||
/// полученный зашифрованный документ. Случайный симметричного ключ в свою очередь шифруется открытым асимметричным ключом получателя и в зашифрованном
|
||||
/// виде добавляется в зашифрованный документ.
|
||||
/// ???? ??????? XML-????????, ????????? ??????? ???????? ??????? ????????? ? ?????????????? ?????????? ????????????? ?????, ? ????? ?????????
|
||||
/// ?????????? ????????????? ????????. ????????? ????????????? ???? ? ???? ??????? ????????? ???????? ????????????? ?????? ?????????? ? ? ?????????????
|
||||
/// ???? ??????????? ? ????????????? ????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Шифрация и дешифрация XML с использованием контейнера ключей")]
|
||||
public class EncryptedXmlKeyContainerTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_2001_Certificates))]
|
||||
public void ShouldEncryptXmlWithGost_R3410_2001(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -40,12 +39,12 @@ namespace GostCryptography.Tests.Xml.Encrypt
|
||||
|
||||
var certificate = testCase.Certificate;
|
||||
|
||||
// Получатель экспортирует отправителю информацию о своем открытом ключе
|
||||
// ?????????? ???????????? ??????????? ?????????? ? ????? ???????? ?????
|
||||
var keyContainer = certificate.GetPrivateKeyInfo();
|
||||
var privateKey = new Gost_R3410_2001_AsymmetricAlgorithm(keyContainer);
|
||||
var publicKeyInfo = privateKey.ExportParameters(false);
|
||||
|
||||
// Отправитель импортирует от получателя информацию о его открытом ключе
|
||||
// ??????????? ??????????? ?? ?????????? ?????????? ? ??? ???????? ?????
|
||||
var publicKey = new Gost_R3410_2001_AsymmetricAlgorithm();
|
||||
publicKey.ImportParameters(publicKeyInfo);
|
||||
|
||||
@@ -58,10 +57,10 @@ namespace GostCryptography.Tests.Xml.Encrypt
|
||||
var actualXml = decryptedXmlDocument.OuterXml;
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(expectedXml, actualXml);
|
||||
Assert.Equal(expectedXml, actualXml);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_2012_256_Certificates))]
|
||||
public void ShouldEncryptXmlWithGost_R3410_2012_256(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -69,12 +68,12 @@ namespace GostCryptography.Tests.Xml.Encrypt
|
||||
|
||||
var certificate = testCase.Certificate;
|
||||
|
||||
// Получатель экспортирует отправителю информацию о своем открытом ключе
|
||||
// ?????????? ???????????? ??????????? ?????????? ? ????? ???????? ?????
|
||||
var keyContainer = certificate.GetPrivateKeyInfo();
|
||||
var privateKey = new Gost_R3410_2012_256_AsymmetricAlgorithm(keyContainer);
|
||||
var publicKeyInfo = privateKey.ExportParameters(false);
|
||||
|
||||
// Отправитель импортирует от получателя информацию о его открытом ключе
|
||||
// ??????????? ??????????? ?? ?????????? ?????????? ? ??? ???????? ?????
|
||||
var publicKey = new Gost_R3410_2012_256_AsymmetricAlgorithm();
|
||||
publicKey.ImportParameters(publicKeyInfo);
|
||||
|
||||
@@ -87,10 +86,10 @@ namespace GostCryptography.Tests.Xml.Encrypt
|
||||
var actualXml = decryptedXmlDocument.OuterXml;
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(expectedXml, actualXml);
|
||||
Assert.Equal(expectedXml, actualXml);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_2012_512_Certificates))]
|
||||
public void ShouldEncryptXmlWithGost_R3410_2012_512(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -98,12 +97,12 @@ namespace GostCryptography.Tests.Xml.Encrypt
|
||||
|
||||
var certificate = testCase.Certificate;
|
||||
|
||||
// Получатель экспортирует отправителю информацию о своем открытом ключе
|
||||
// ?????????? ???????????? ??????????? ?????????? ? ????? ???????? ?????
|
||||
var keyContainer = certificate.GetPrivateKeyInfo();
|
||||
var privateKey = new Gost_R3410_2012_512_AsymmetricAlgorithm(keyContainer);
|
||||
var publicKeyInfo = privateKey.ExportParameters(false);
|
||||
|
||||
// Отправитель импортирует от получателя информацию о его открытом ключе
|
||||
// ??????????? ??????????? ?? ?????????? ?????????? ? ??? ???????? ?????
|
||||
var publicKey = new Gost_R3410_2012_512_AsymmetricAlgorithm();
|
||||
publicKey.ImportParameters(publicKeyInfo);
|
||||
|
||||
@@ -116,7 +115,7 @@ namespace GostCryptography.Tests.Xml.Encrypt
|
||||
var actualXml = decryptedXmlDocument.OuterXml;
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(expectedXml, actualXml);
|
||||
Assert.Equal(expectedXml, actualXml);
|
||||
}
|
||||
|
||||
private static XmlDocument CreateXmlDocument()
|
||||
@@ -128,10 +127,10 @@ namespace GostCryptography.Tests.Xml.Encrypt
|
||||
|
||||
private static XmlDocument EncryptXmlDocument(XmlDocument xmlDocument, GostAsymmetricAlgorithm publicKey)
|
||||
{
|
||||
// Создание объекта для шифрации XML
|
||||
// ???????? ??????? ??? ???????? XML
|
||||
var encryptedXml = new GostEncryptedXml(publicKey.ProviderType);
|
||||
|
||||
// Поиск элементов для шифрации
|
||||
// ????? ????????? ??? ????????
|
||||
var elements = xmlDocument.SelectNodes("//SomeElement[@Encrypt='true']");
|
||||
|
||||
if (elements != null)
|
||||
@@ -140,16 +139,16 @@ namespace GostCryptography.Tests.Xml.Encrypt
|
||||
|
||||
foreach (XmlElement element in elements)
|
||||
{
|
||||
// Создание случайного сессионного ключа
|
||||
// ???????? ?????????? ??????????? ?????
|
||||
using (var sessionKey = new Gost_28147_89_SymmetricAlgorithm(publicKey.ProviderType))
|
||||
{
|
||||
// Шифрация элемента
|
||||
// ???????? ????????
|
||||
var encryptedData = encryptedXml.EncryptData(element, sessionKey, false);
|
||||
|
||||
// Шифрация сессионного ключа с использованием публичного асимметричного ключа
|
||||
// ???????? ??????????? ????? ? ?????????????? ?????????? ?????????????? ?????
|
||||
var encryptedSessionKeyData = GostEncryptedXml.EncryptKey(sessionKey, publicKey);
|
||||
|
||||
// Формирование элемента EncryptedData
|
||||
// ???????????? ???????? EncryptedData
|
||||
var elementEncryptedData = new EncryptedData();
|
||||
elementEncryptedData.Id = "EncryptedElement" + elementIndex++;
|
||||
elementEncryptedData.Type = EncryptedXml.XmlEncElementUrl;
|
||||
@@ -157,17 +156,17 @@ namespace GostCryptography.Tests.Xml.Encrypt
|
||||
elementEncryptedData.CipherData.CipherValue = encryptedData;
|
||||
elementEncryptedData.KeyInfo = new KeyInfo();
|
||||
|
||||
// Формирование информации о зашифрованном сессионном ключе
|
||||
// ???????????? ?????????? ? ????????????? ?????????? ?????
|
||||
var encryptedSessionKey = new EncryptedKey();
|
||||
encryptedSessionKey.CipherData = new CipherData(encryptedSessionKeyData);
|
||||
encryptedSessionKey.EncryptionMethod = new EncryptionMethod(publicKey.KeyExchangeAlgorithm);
|
||||
encryptedSessionKey.AddReference(new DataReference { Uri = "#" + elementEncryptedData.Id });
|
||||
encryptedSessionKey.KeyInfo.AddClause(new KeyInfoName { Value = "KeyName1" });
|
||||
|
||||
// Добавление ссылки на зашифрованный ключ, используемый при шифровании данных
|
||||
// ?????????? ?????? ?? ????????????? ????, ???????????? ??? ?????????? ??????
|
||||
elementEncryptedData.KeyInfo.AddClause(new KeyInfoEncryptedKey(encryptedSessionKey));
|
||||
|
||||
// Замена элемента его зашифрованным представлением
|
||||
// ?????? ???????? ??? ????????????? ??????????????
|
||||
GostEncryptedXml.ReplaceElement(element, elementEncryptedData, false);
|
||||
}
|
||||
}
|
||||
@@ -178,16 +177,16 @@ namespace GostCryptography.Tests.Xml.Encrypt
|
||||
|
||||
private static XmlDocument DecryptXmlDocument(XmlDocument encryptedXmlDocument, GostAsymmetricAlgorithm privateKey)
|
||||
{
|
||||
// Создание объекта для дешифрации XML
|
||||
// ???????? ??????? ??? ?????????? XML
|
||||
var encryptedXml = new GostEncryptedXml(privateKey.ProviderType, encryptedXmlDocument);
|
||||
|
||||
// Добавление ссылки на приватный асимметричный ключ
|
||||
// ?????????? ?????? ?? ????????? ????????????? ????
|
||||
encryptedXml.AddKeyNameMapping("KeyName1", privateKey);
|
||||
|
||||
// Расшифровка зашифрованных элементов документа
|
||||
// ??????????? ????????????? ????????? ?????????
|
||||
encryptedXml.DecryptDocument();
|
||||
|
||||
return encryptedXmlDocument;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Security.Cryptography.Xml;
|
||||
using System.Security.Cryptography.Xml;
|
||||
using System.Xml;
|
||||
|
||||
using GostCryptography.Base;
|
||||
@@ -6,22 +6,21 @@ using GostCryptography.Gost_28147_89;
|
||||
using GostCryptography.Tests.Properties;
|
||||
using GostCryptography.Xml;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Xml.Encrypt
|
||||
{
|
||||
/// <summary>
|
||||
/// Шифрация и дешифрация XML с использованием случайного сессионного ключа.
|
||||
/// ???????? ? ?????????? XML ? ?????????????? ?????????? ??????????? ?????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает XML-документ, выборочно шифрует элементы данного документа с использованием случайного симметричного ключа,
|
||||
/// а затем дешифрует полученный зашифрованный документ. Случайный симметричного ключ в свою очередь шифруется общим симметричным
|
||||
/// ключом и в зашифрованном виде добавляется в зашифрованный документ.
|
||||
/// ???? ??????? XML-????????, ????????? ??????? ???????? ??????? ????????? ? ?????????????? ?????????? ????????????? ?????,
|
||||
/// ? ????? ????????? ?????????? ????????????? ????????. ????????? ????????????? ???? ? ???? ??????? ????????? ????? ????????????
|
||||
/// ?????? ? ? ????????????? ???? ??????????? ? ????????????? ????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Шифрация и дешифрация XML с использованием случайного сессионного ключа")]
|
||||
public class EncryptedXmlSessionKey
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldEncryptXml(ProviderType providerType)
|
||||
{
|
||||
@@ -36,7 +35,7 @@ namespace GostCryptography.Tests.Xml.Encrypt
|
||||
var actualXml = decryptedXmlDocument.OuterXml;
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(expectedXml, actualXml);
|
||||
Assert.Equal(expectedXml, actualXml);
|
||||
}
|
||||
|
||||
private static XmlDocument CreateXmlDocument()
|
||||
@@ -48,10 +47,10 @@ namespace GostCryptography.Tests.Xml.Encrypt
|
||||
|
||||
private static XmlDocument EncryptXmlDocument(XmlDocument xmlDocument, GostSymmetricAlgorithm sharedKey)
|
||||
{
|
||||
// Создание объекта для шифрации XML
|
||||
// ???????? ??????? ??? ???????? XML
|
||||
var encryptedXml = new GostEncryptedXml(sharedKey.ProviderType);
|
||||
|
||||
// Поиск элементов для шифрации
|
||||
// ????? ????????? ??? ????????
|
||||
var elements = xmlDocument.SelectNodes("//SomeElement[@Encrypt='true']");
|
||||
|
||||
if (elements != null)
|
||||
@@ -60,16 +59,16 @@ namespace GostCryptography.Tests.Xml.Encrypt
|
||||
|
||||
foreach (XmlElement element in elements)
|
||||
{
|
||||
// Создание случайного сессионного ключа
|
||||
// ???????? ?????????? ??????????? ?????
|
||||
using (var sessionKey = new Gost_28147_89_SymmetricAlgorithm(sharedKey.ProviderType))
|
||||
{
|
||||
// Шифрация элемента
|
||||
// ???????? ????????
|
||||
var encryptedData = encryptedXml.EncryptData(element, sessionKey, false);
|
||||
|
||||
// Шифрация сессионного ключа с использованием общего симметричного ключа
|
||||
// ???????? ??????????? ????? ? ?????????????? ?????? ????????????? ?????
|
||||
var encryptedSessionKeyData = GostEncryptedXml.EncryptKey(sessionKey, sharedKey, GostKeyExchangeExportMethod.CryptoProKeyExport);
|
||||
|
||||
// Формирование элемента EncryptedData
|
||||
// ???????????? ???????? EncryptedData
|
||||
var elementEncryptedData = new EncryptedData();
|
||||
elementEncryptedData.Id = "EncryptedElement" + elementIndex++;
|
||||
elementEncryptedData.Type = EncryptedXml.XmlEncElementUrl;
|
||||
@@ -77,17 +76,17 @@ namespace GostCryptography.Tests.Xml.Encrypt
|
||||
elementEncryptedData.CipherData.CipherValue = encryptedData;
|
||||
elementEncryptedData.KeyInfo = new KeyInfo();
|
||||
|
||||
// Формирование информации о зашифрованном сессионном ключе
|
||||
// ???????????? ?????????? ? ????????????? ?????????? ?????
|
||||
var encryptedSessionKey = new EncryptedKey();
|
||||
encryptedSessionKey.CipherData = new CipherData(encryptedSessionKeyData);
|
||||
encryptedSessionKey.EncryptionMethod = new EncryptionMethod(GostEncryptedXml.XmlEncGostCryptoProKeyExportUrl);
|
||||
encryptedSessionKey.AddReference(new DataReference { Uri = "#" + elementEncryptedData.Id });
|
||||
encryptedSessionKey.KeyInfo.AddClause(new KeyInfoName { Value = "SharedKey1" });
|
||||
|
||||
// Добавление ссылки на зашифрованный ключ, используемый при шифровании данных
|
||||
// ?????????? ?????? ?? ????????????? ????, ???????????? ??? ?????????? ??????
|
||||
elementEncryptedData.KeyInfo.AddClause(new KeyInfoEncryptedKey(encryptedSessionKey));
|
||||
|
||||
// Замена элемента его зашифрованным представлением
|
||||
// ?????? ???????? ??? ????????????? ??????????????
|
||||
GostEncryptedXml.ReplaceElement(element, elementEncryptedData, false);
|
||||
}
|
||||
}
|
||||
@@ -98,16 +97,16 @@ namespace GostCryptography.Tests.Xml.Encrypt
|
||||
|
||||
private static XmlDocument DecryptXmlDocument(XmlDocument encryptedXmlDocument, GostSymmetricAlgorithm sharedKey)
|
||||
{
|
||||
// Создание объекта для дешифрации XML
|
||||
// ???????? ??????? ??? ?????????? XML
|
||||
var encryptedXml = new GostEncryptedXml(sharedKey.ProviderType, encryptedXmlDocument);
|
||||
|
||||
// Добавление ссылки на общий симметричный ключ
|
||||
// ?????????? ?????? ?? ????? ???????????? ????
|
||||
encryptedXml.AddKeyNameMapping("SharedKey1", sharedKey);
|
||||
|
||||
// Расшифровка зашифрованных элементов документа
|
||||
// ??????????? ????????????? ????????? ?????????
|
||||
encryptedXml.DecryptDocument();
|
||||
|
||||
return encryptedXmlDocument;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Security.Cryptography.Xml;
|
||||
using System.Security.Cryptography.Xml;
|
||||
using System.Xml;
|
||||
|
||||
using GostCryptography.Base;
|
||||
@@ -6,102 +6,14 @@ using GostCryptography.Gost_28147_89;
|
||||
using GostCryptography.Tests.Properties;
|
||||
using GostCryptography.Xml;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Xml.Encrypt
|
||||
{
|
||||
/// <summary>
|
||||
/// Шифрация и дешифрация XML с использованием общего симметричного ключа.
|
||||
/// ???????? ? ?????????? XML ? ?????????????? ?????? ????????????? ?????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает XML-документ, выборочно шифрует элементы данного документа с использованием общего симметричного ключа,
|
||||
/// а затем дешифрует полученный зашифрованный документ.
|
||||
/// ???? ??????? XML-????????, ????????? ??????? ???????? ??????? ????????? ? ?????????????? ?????? ????????????? ?????,
|
||||
/// ? ????? ????????? ?????????? ????????????? ????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Шифрация и дешифрация XML с использованием общего симметричного ключа")]
|
||||
public sealed class EncryptedXmlSharedKeyTest
|
||||
{
|
||||
[Test]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Providers))]
|
||||
public void ShouldEncryptXml(ProviderType providerType)
|
||||
{
|
||||
// Given
|
||||
var sharedKey = new Gost_28147_89_SymmetricAlgorithm(providerType);
|
||||
var xmlDocument = CreateXmlDocument();
|
||||
var expectedXml = xmlDocument.OuterXml;
|
||||
|
||||
// When
|
||||
var encryptedXmlDocument = EncryptXmlDocument(xmlDocument, sharedKey);
|
||||
var decryptedXmlDocument = DecryptXmlDocument(encryptedXmlDocument, sharedKey);
|
||||
var actualXml = decryptedXmlDocument.OuterXml;
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(expectedXml, actualXml);
|
||||
}
|
||||
|
||||
private static XmlDocument CreateXmlDocument()
|
||||
{
|
||||
var document = new XmlDocument();
|
||||
document.LoadXml(Resources.EncryptedXmlExample);
|
||||
return document;
|
||||
}
|
||||
|
||||
private static XmlDocument EncryptXmlDocument(XmlDocument xmlDocument, Gost_28147_89_SymmetricAlgorithm sharedKey)
|
||||
{
|
||||
// Создание объекта для шифрации XML
|
||||
var encryptedXml = new GostEncryptedXml(sharedKey.ProviderType);
|
||||
|
||||
// Поиск элементов для шифрации
|
||||
var elements = xmlDocument.SelectNodes("//SomeElement[@Encrypt='true']");
|
||||
|
||||
if (elements != null)
|
||||
{
|
||||
foreach (XmlElement element in elements)
|
||||
{
|
||||
// Шифрация элемента
|
||||
var encryptedData = encryptedXml.EncryptData(element, sharedKey, false);
|
||||
|
||||
// Формирование элемента EncryptedData
|
||||
var elementEncryptedData = new EncryptedData();
|
||||
elementEncryptedData.Type = EncryptedXml.XmlEncElementUrl;
|
||||
elementEncryptedData.EncryptionMethod = new EncryptionMethod(sharedKey.AlgorithmName);
|
||||
elementEncryptedData.CipherData.CipherValue = encryptedData;
|
||||
|
||||
// Замена элемента его зашифрованным представлением
|
||||
GostEncryptedXml.ReplaceElement(element, elementEncryptedData, false);
|
||||
}
|
||||
}
|
||||
|
||||
return xmlDocument;
|
||||
}
|
||||
|
||||
private static XmlDocument DecryptXmlDocument(XmlDocument encryptedXmlDocument, Gost_28147_89_SymmetricAlgorithm sharedKey)
|
||||
{
|
||||
// Создание объекта для дешифрации XML
|
||||
var encryptedXml = new GostEncryptedXml(sharedKey.ProviderType, encryptedXmlDocument);
|
||||
|
||||
var nsManager = new XmlNamespaceManager(encryptedXmlDocument.NameTable);
|
||||
nsManager.AddNamespace("enc", EncryptedXml.XmlEncNamespaceUrl);
|
||||
|
||||
// Поиск всех зашифрованных XML-элементов
|
||||
var encryptedDataList = encryptedXmlDocument.SelectNodes("//enc:EncryptedData", nsManager);
|
||||
|
||||
if (encryptedDataList != null)
|
||||
{
|
||||
foreach (XmlElement encryptedData in encryptedDataList)
|
||||
{
|
||||
// Загрузка элемента EncryptedData
|
||||
var elementEncryptedData = new EncryptedData();
|
||||
elementEncryptedData.LoadXml(encryptedData);
|
||||
|
||||
// Расшифровка элемента EncryptedData
|
||||
var decryptedData = encryptedXml.DecryptData(elementEncryptedData, sharedKey);
|
||||
|
||||
// Замена элемента EncryptedData его расшифрованным представлением
|
||||
encryptedXml.ReplaceData(encryptedData, decryptedData);
|
||||
}
|
||||
}
|
||||
|
||||
return encryptedXmlDocument;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.Xml;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
@@ -8,87 +8,13 @@ using GostCryptography.Gost_28147_89;
|
||||
using GostCryptography.Tests.Properties;
|
||||
using GostCryptography.Xml;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Xml.Encrypt
|
||||
{
|
||||
/// <summary>
|
||||
/// Шифрация и дешифрация XML документа с использованием сертификата и алгоритма ГОСТ Р 34.12-2015 Кузнечик.
|
||||
/// ???????? ? ?????????? XML ????????? ? ?????????????? ??????????? ? ????????? ???? ? 34.12-2015 ????????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает XML-документ, шифрует его целиком с использованием сертификата, а затем дешифрует зашифрованный документ.
|
||||
/// ???? ??????? XML-????????, ??????? ??? ??????? ? ?????????????? ???????????, ? ????? ????????? ????????????? ????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Шифрация и дешифрация XML документа с использованием сертификата и алгоритма ГОСТ Р 34.12-2015 Кузнечик")]
|
||||
public sealed class KuznyechikEncryptedXmlCertificateTest
|
||||
{
|
||||
[Test]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldEncryptXml(TestCertificateInfo testCase)
|
||||
{
|
||||
// Given
|
||||
var certificate = testCase.Certificate;
|
||||
var xmlDocument = CreateXmlDocument();
|
||||
var expectedXml = xmlDocument.OuterXml.Replace("\r\n", "\n");
|
||||
|
||||
// When
|
||||
var encryptedXmlDocument = EncryptXmlDocument(xmlDocument, certificate);
|
||||
var decryptedXmlDocument = DecryptXmlDocument(encryptedXmlDocument);
|
||||
var actualXml = decryptedXmlDocument.OuterXml.Replace("\r\n", "\n");
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(expectedXml, actualXml);
|
||||
}
|
||||
|
||||
private static XmlDocument CreateXmlDocument()
|
||||
{
|
||||
var document = new XmlDocument();
|
||||
document.LoadXml(Resources.EncryptedXmlExample);
|
||||
return document;
|
||||
}
|
||||
|
||||
private static XmlDocument EncryptXmlDocument(XmlDocument xmlDocument, X509Certificate2 certificate)
|
||||
{
|
||||
var publicKeyAlgorithm = (GostAsymmetricAlgorithm)certificate.GetPublicKeyAlgorithm();
|
||||
|
||||
using (var sessionKey = new Gost_3412_K_SymmetricAlgorithm(publicKeyAlgorithm.ProviderType))
|
||||
{
|
||||
var encryptedSessionKeyData = GostEncryptedXml.EncryptKey(sessionKey, publicKeyAlgorithm);
|
||||
|
||||
var encryptedSessionKey = new EncryptedKey
|
||||
{
|
||||
CipherData = new CipherData(encryptedSessionKeyData),
|
||||
EncryptionMethod = new EncryptionMethod(publicKeyAlgorithm.KeyExchangeAlgorithm),
|
||||
};
|
||||
|
||||
encryptedSessionKey.KeyInfo.AddClause(new KeyInfoX509Data(certificate));
|
||||
|
||||
var elementEncryptedData = new EncryptedData
|
||||
{
|
||||
EncryptionMethod = new EncryptionMethod(sessionKey.AlgorithmName),
|
||||
};
|
||||
|
||||
var encryptedXml = new GostEncryptedXml();
|
||||
var xmlBytes = Encoding.UTF8.GetBytes(xmlDocument.OuterXml);
|
||||
var encryptedData = encryptedXml.EncryptData(xmlBytes, sessionKey);
|
||||
|
||||
elementEncryptedData.CipherData.CipherValue = encryptedData;
|
||||
elementEncryptedData.KeyInfo.AddClause(new KeyInfoEncryptedKey(encryptedSessionKey));
|
||||
|
||||
GostEncryptedXml.ReplaceElement(xmlDocument.DocumentElement, elementEncryptedData, false);
|
||||
}
|
||||
|
||||
return xmlDocument;
|
||||
}
|
||||
|
||||
private static XmlDocument DecryptXmlDocument(XmlDocument encryptedXmlDocument)
|
||||
{
|
||||
// Создание объекта для дешифрации XML
|
||||
var encryptedXml = new GostEncryptedXml(encryptedXmlDocument);
|
||||
|
||||
// Расшифровка зашифрованных элементов документа
|
||||
encryptedXml.DecryptDocument();
|
||||
|
||||
return encryptedXmlDocument;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.Xml;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
@@ -8,87 +8,13 @@ using GostCryptography.Gost_28147_89;
|
||||
using GostCryptography.Tests.Properties;
|
||||
using GostCryptography.Xml;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Xml.Encrypt
|
||||
{
|
||||
/// <summary>
|
||||
/// Шифрация и дешифрация XML документа с использованием сертификата и алгоритма ГОСТ Р 34.12-2015 Магма.
|
||||
/// ???????? ? ?????????? XML ????????? ? ?????????????? ??????????? ? ????????? ???? ? 34.12-2015 ?????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает XML-документ, шифрует его целиком с использованием сертификата, а затем дешифрует зашифрованный документ.
|
||||
/// ???? ??????? XML-????????, ??????? ??? ??????? ? ?????????????? ???????????, ? ????? ????????? ????????????? ????????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Шифрация и дешифрация XML документа с использованием сертификата и алгоритма ГОСТ Р 34.12-2015 Магма")]
|
||||
public sealed class MagmaEncryptedXmlCertificateTest
|
||||
{
|
||||
[Test]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldEncryptXml(TestCertificateInfo testCase)
|
||||
{
|
||||
// Given
|
||||
var certificate = testCase.Certificate;
|
||||
var xmlDocument = CreateXmlDocument();
|
||||
var expectedXml = xmlDocument.OuterXml.Replace("\r\n", "\n");
|
||||
|
||||
// When
|
||||
var encryptedXmlDocument = EncryptXmlDocument(xmlDocument, certificate);
|
||||
var decryptedXmlDocument = DecryptXmlDocument(encryptedXmlDocument);
|
||||
var actualXml = decryptedXmlDocument.OuterXml.Replace("\r\n", "\n");
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(expectedXml, actualXml);
|
||||
}
|
||||
|
||||
private static XmlDocument CreateXmlDocument()
|
||||
{
|
||||
var document = new XmlDocument();
|
||||
document.LoadXml(Resources.EncryptedXmlExample);
|
||||
return document;
|
||||
}
|
||||
|
||||
private static XmlDocument EncryptXmlDocument(XmlDocument xmlDocument, X509Certificate2 certificate)
|
||||
{
|
||||
var publicKeyAlgorithm = (GostAsymmetricAlgorithm)certificate.GetPublicKeyAlgorithm();
|
||||
|
||||
using (var sessionKey = new Gost_3412_M_SymmetricAlgorithm(publicKeyAlgorithm.ProviderType))
|
||||
{
|
||||
var encryptedSessionKeyData = GostEncryptedXml.EncryptKey(sessionKey, publicKeyAlgorithm);
|
||||
|
||||
var encryptedSessionKey = new EncryptedKey
|
||||
{
|
||||
CipherData = new CipherData(encryptedSessionKeyData),
|
||||
EncryptionMethod = new EncryptionMethod(publicKeyAlgorithm.KeyExchangeAlgorithm),
|
||||
};
|
||||
|
||||
encryptedSessionKey.KeyInfo.AddClause(new KeyInfoX509Data(certificate));
|
||||
|
||||
var elementEncryptedData = new EncryptedData
|
||||
{
|
||||
EncryptionMethod = new EncryptionMethod(sessionKey.AlgorithmName),
|
||||
};
|
||||
|
||||
var encryptedXml = new GostEncryptedXml();
|
||||
var xmlBytes = Encoding.UTF8.GetBytes(xmlDocument.OuterXml);
|
||||
var encryptedData = encryptedXml.EncryptData(xmlBytes, sessionKey);
|
||||
|
||||
elementEncryptedData.CipherData.CipherValue = encryptedData;
|
||||
elementEncryptedData.KeyInfo.AddClause(new KeyInfoEncryptedKey(encryptedSessionKey));
|
||||
|
||||
GostEncryptedXml.ReplaceElement(xmlDocument.DocumentElement, elementEncryptedData, false);
|
||||
}
|
||||
|
||||
return xmlDocument;
|
||||
}
|
||||
|
||||
private static XmlDocument DecryptXmlDocument(XmlDocument encryptedXmlDocument)
|
||||
{
|
||||
// Создание объекта для дешифрации XML
|
||||
var encryptedXml = new GostEncryptedXml(encryptedXmlDocument);
|
||||
|
||||
// Расшифровка зашифрованных элементов документа
|
||||
encryptedXml.DecryptDocument();
|
||||
|
||||
return encryptedXmlDocument;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.Xml;
|
||||
using System.Xml;
|
||||
|
||||
@@ -6,21 +6,20 @@ using GostCryptography.Base;
|
||||
using GostCryptography.Tests.Properties;
|
||||
using GostCryptography.Xml;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Xml.Sign
|
||||
{
|
||||
/// <summary>
|
||||
/// Подпись и проверка подписи XML-документа с использованием сертификата.
|
||||
/// ??????? ? ???????? ??????? XML-????????? ? ?????????????? ???????????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает XML-документ, подписывает определенную часть данного документа с использованием сертификата,
|
||||
/// а затем проверяет полученную цифровую подпись.
|
||||
/// ???? ??????? XML-????????, ??????????? ???????????? ????? ??????? ????????? ? ?????????????? ???????????,
|
||||
/// ? ????? ????????? ?????????? ???????? ???????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Подпись и проверка подписи XML-документа с использованием сертификата")]
|
||||
public class SignedXmlCertificateTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldSignXml(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -32,7 +31,7 @@ namespace GostCryptography.Tests.Xml.Sign
|
||||
var signedXmlDocument = SignXmlDocument(xmlDocument, certificate);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(VerifyXmlDocumentSignature(signedXmlDocument));
|
||||
Assert.True(VerifyXmlDocumentSignature(signedXmlDocument));
|
||||
}
|
||||
|
||||
private static XmlDocument CreateXmlDocument()
|
||||
@@ -44,30 +43,30 @@ namespace GostCryptography.Tests.Xml.Sign
|
||||
|
||||
private static XmlDocument SignXmlDocument(XmlDocument xmlDocument, X509Certificate2 certificate)
|
||||
{
|
||||
// Создание подписчика XML-документа
|
||||
// ???????? ?????????? XML-?????????
|
||||
var signedXml = new GostSignedXml(xmlDocument);
|
||||
|
||||
// Установка ключа для создания подписи
|
||||
// ????????? ????? ??? ???????? ???????
|
||||
signedXml.SetSigningCertificate(certificate);
|
||||
|
||||
// Ссылка на узел, который нужно подписать, с указанием алгоритма хэширования
|
||||
// ?????? ?? ????, ??????? ????? ?????????, ? ????????? ????????? ???????????
|
||||
var dataReference = new Reference { Uri = "#Id1", DigestMethod = GetDigestMethod(certificate) };
|
||||
|
||||
// Установка ссылки на узел
|
||||
// ????????? ?????? ?? ????
|
||||
signedXml.AddReference(dataReference);
|
||||
|
||||
// Установка информации о сертификате, который использовался для создания подписи
|
||||
// ????????? ?????????? ? ???????????, ??????? ????????????? ??? ???????? ???????
|
||||
var keyInfo = new KeyInfo();
|
||||
keyInfo.AddClause(new KeyInfoX509Data(certificate));
|
||||
signedXml.KeyInfo = keyInfo;
|
||||
|
||||
// Вычисление подписи
|
||||
// ?????????? ???????
|
||||
signedXml.ComputeSignature();
|
||||
|
||||
// Получение XML-представления подписи
|
||||
// ????????? XML-????????????? ???????
|
||||
var signatureXml = signedXml.GetXml();
|
||||
|
||||
// Добавление подписи в исходный документ
|
||||
// ?????????? ??????? ? ???????? ????????
|
||||
xmlDocument.DocumentElement.AppendChild(xmlDocument.ImportNode(signatureXml, true));
|
||||
|
||||
return xmlDocument;
|
||||
@@ -75,22 +74,22 @@ namespace GostCryptography.Tests.Xml.Sign
|
||||
|
||||
private static bool VerifyXmlDocumentSignature(XmlDocument signedXmlDocument)
|
||||
{
|
||||
// Создание подписчика XML-документа
|
||||
// ???????? ?????????? XML-?????????
|
||||
var signedXml = new GostSignedXml(signedXmlDocument);
|
||||
|
||||
// Поиск узла с подписью
|
||||
// ????? ???? ? ????????
|
||||
var nodeList = signedXmlDocument.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl);
|
||||
|
||||
// Загрузка найденной подписи
|
||||
// ???????? ????????? ???????
|
||||
signedXml.LoadXml((XmlElement)nodeList[0]);
|
||||
|
||||
// Проверка подписи
|
||||
// ???????? ???????
|
||||
return signedXml.CheckSignature();
|
||||
}
|
||||
|
||||
private static string GetDigestMethod(X509Certificate2 certificate)
|
||||
{
|
||||
// Имя алгоритма вычисляем динамически, чтобы сделать код теста универсальным
|
||||
// ??? ????????? ????????? ???????????, ????? ??????? ??? ????? ?????????????
|
||||
|
||||
using (var publicKey = (GostAsymmetricAlgorithm)certificate.GetPublicKeyAlgorithm())
|
||||
using (var hashAlgorithm = publicKey.CreateHashAlgorithm())
|
||||
@@ -99,4 +98,4 @@ namespace GostCryptography.Tests.Xml.Sign
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.Xml;
|
||||
using System.Xml;
|
||||
|
||||
@@ -6,21 +6,20 @@ using GostCryptography.Base;
|
||||
using GostCryptography.Tests.Properties;
|
||||
using GostCryptography.Xml;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Xml.Sign
|
||||
{
|
||||
/// <summary>
|
||||
/// Подпись и проверка подписи всего XML документа с использованием сертификата
|
||||
/// ??????? ? ???????? ??????? ????? XML ????????? ? ?????????????? ???????????
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает XML-документ, подписывает весь документ с использованием сертификата,
|
||||
/// а затем проверяет полученную цифровую подпись.
|
||||
/// ???? ??????? XML-????????, ??????????? ???? ???????? ? ?????????????? ???????????,
|
||||
/// ? ????? ????????? ?????????? ???????? ???????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Подпись и проверка подписи всего XML документа с использованием сертификата")]
|
||||
public class SignedXmlDocumentTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldSignXml(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -32,7 +31,7 @@ namespace GostCryptography.Tests.Xml.Sign
|
||||
var signedXmlDocument = SignXmlDocument(xmlDocument, certificate);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(VerifyXmlDocumentSignature(signedXmlDocument));
|
||||
Assert.True(VerifyXmlDocumentSignature(signedXmlDocument));
|
||||
}
|
||||
|
||||
private static XmlDocument CreateXmlDocument()
|
||||
@@ -44,33 +43,33 @@ namespace GostCryptography.Tests.Xml.Sign
|
||||
|
||||
private static XmlDocument SignXmlDocument(XmlDocument xmlDocument, X509Certificate2 certificate)
|
||||
{
|
||||
// Создание подписчика XML-документа
|
||||
// ???????? ?????????? XML-?????????
|
||||
var signedXml = new GostSignedXml(xmlDocument);
|
||||
|
||||
// Установка ключа для создания подписи
|
||||
// ????????? ????? ??? ???????? ???????
|
||||
signedXml.SetSigningCertificate(certificate);
|
||||
|
||||
// Ссылка на весь документ и указание алгоритма хэширования
|
||||
// ?????? ?? ???? ???????? ? ???????? ????????? ???????????
|
||||
var dataReference = new Reference { Uri = "", DigestMethod = GetDigestMethod(certificate) };
|
||||
|
||||
// Метод преобразования для подписи всего документа
|
||||
// ????? ?????????????? ??? ??????? ????? ?????????
|
||||
dataReference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
|
||||
|
||||
// Установка ссылки на узел
|
||||
// ????????? ?????? ?? ????
|
||||
signedXml.AddReference(dataReference);
|
||||
|
||||
// Установка информации о сертификате, который использовался для создания подписи
|
||||
// ????????? ?????????? ? ???????????, ??????? ????????????? ??? ???????? ???????
|
||||
var keyInfo = new KeyInfo();
|
||||
keyInfo.AddClause(new KeyInfoX509Data(certificate));
|
||||
signedXml.KeyInfo = keyInfo;
|
||||
|
||||
// Вычисление подписи
|
||||
// ?????????? ???????
|
||||
signedXml.ComputeSignature();
|
||||
|
||||
// Получение XML-представления подписи
|
||||
// ????????? XML-????????????? ???????
|
||||
var signatureXml = signedXml.GetXml();
|
||||
|
||||
// Добавление подписи в исходный документ
|
||||
// ?????????? ??????? ? ???????? ????????
|
||||
xmlDocument.DocumentElement.AppendChild(xmlDocument.ImportNode(signatureXml, true));
|
||||
|
||||
return xmlDocument;
|
||||
@@ -78,22 +77,22 @@ namespace GostCryptography.Tests.Xml.Sign
|
||||
|
||||
private static bool VerifyXmlDocumentSignature(XmlDocument signedXmlDocument)
|
||||
{
|
||||
// Создание подписчика XML-документа
|
||||
// ???????? ?????????? XML-?????????
|
||||
var signedXml = new GostSignedXml(signedXmlDocument);
|
||||
|
||||
// Поиск узла с подписью
|
||||
// ????? ???? ? ????????
|
||||
var nodeList = signedXmlDocument.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl);
|
||||
|
||||
// Загрузка найденной подписи
|
||||
// ???????? ????????? ???????
|
||||
signedXml.LoadXml((XmlElement)nodeList[0]);
|
||||
|
||||
// Проверка подписи
|
||||
// ???????? ???????
|
||||
return signedXml.CheckSignature();
|
||||
}
|
||||
|
||||
private static string GetDigestMethod(X509Certificate2 certificate)
|
||||
{
|
||||
// Имя алгоритма вычисляем динамически, чтобы сделать код теста универсальным
|
||||
// ??? ????????? ????????? ???????????, ????? ??????? ??? ????? ?????????????
|
||||
|
||||
using (var publicKey = (GostAsymmetricAlgorithm)certificate.GetPublicKeyAlgorithm())
|
||||
using (var hashAlgorithm = publicKey.CreateHashAlgorithm())
|
||||
@@ -102,4 +101,4 @@ namespace GostCryptography.Tests.Xml.Sign
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.Xml;
|
||||
using System.Xml;
|
||||
|
||||
@@ -7,21 +7,20 @@ using GostCryptography.Gost_R3410;
|
||||
using GostCryptography.Tests.Properties;
|
||||
using GostCryptography.Xml;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Xml.Sign
|
||||
{
|
||||
/// <summary>
|
||||
/// Подпись и проверка подписи XML-документа с использованием контейнера ключей.
|
||||
/// ??????? ? ???????? ??????? XML-????????? ? ?????????????? ?????????? ??????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает XML-документ, подписывает определенную часть данного документа с использованием контейнера ключей,
|
||||
/// а затем проверяет полученную цифровую подпись.
|
||||
/// ???? ??????? XML-????????, ??????????? ???????????? ????? ??????? ????????? ? ?????????????? ?????????? ??????,
|
||||
/// ? ????? ????????? ?????????? ???????? ???????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Подпись и проверка подписи XML-документа с использованием контейнера ключей")]
|
||||
public class SignedXmlKeyContainerTest
|
||||
{
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_2001_Certificates))]
|
||||
public void ShouldSignXmlWithGost_R3410_2001(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -35,10 +34,10 @@ namespace GostCryptography.Tests.Xml.Sign
|
||||
var signedXmlDocument = SignXmlDocument(xmlDocument, new Gost_R3410_2001_KeyValue(signingKey));
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(VerifyXmlDocumentSignature(signedXmlDocument));
|
||||
Assert.True(VerifyXmlDocumentSignature(signedXmlDocument));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_2012_256_Certificates))]
|
||||
public void ShouldSignXmlWithGost_R3410_2012_256(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -52,10 +51,10 @@ namespace GostCryptography.Tests.Xml.Sign
|
||||
var signedXmlDocument = SignXmlDocument(xmlDocument, new Gost_R3410_2012_256_KeyValue(signingKey));
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(VerifyXmlDocumentSignature(signedXmlDocument));
|
||||
Assert.True(VerifyXmlDocumentSignature(signedXmlDocument));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Fact]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_2012_512_Certificates))]
|
||||
public void ShouldSignXmlWithGost_R3410_2012_512(TestCertificateInfo testCase)
|
||||
{
|
||||
@@ -69,7 +68,7 @@ namespace GostCryptography.Tests.Xml.Sign
|
||||
var signedXmlDocument = SignXmlDocument(xmlDocument, new Gost_R3410_2012_512_KeyValue(signingKey));
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(VerifyXmlDocumentSignature(signedXmlDocument));
|
||||
Assert.True(VerifyXmlDocumentSignature(signedXmlDocument));
|
||||
}
|
||||
|
||||
private static XmlDocument CreateXmlDocument()
|
||||
@@ -83,30 +82,30 @@ namespace GostCryptography.Tests.Xml.Sign
|
||||
{
|
||||
var signingKey = keyValue.PublicKey;
|
||||
|
||||
// Создание подписчика XML-документа
|
||||
// ???????? ?????????? XML-?????????
|
||||
var signedXml = new GostSignedXml(xmlDocument);
|
||||
|
||||
// Установка ключа для создания подписи
|
||||
// ????????? ????? ??? ???????? ???????
|
||||
signedXml.SigningKey = signingKey;
|
||||
|
||||
// Ссылка на узел, который нужно подписать, с указанием алгоритма хэширования
|
||||
// ?????? ?? ????, ??????? ????? ?????????, ? ????????? ????????? ???????????
|
||||
var dataReference = new Reference { Uri = "#Id1", DigestMethod = GetDigestMethod(signingKey) };
|
||||
|
||||
// Установка ссылки на узел
|
||||
// ????????? ?????? ?? ????
|
||||
signedXml.AddReference(dataReference);
|
||||
|
||||
// Установка информации о ключе, который использовался для создания подписи
|
||||
// ????????? ?????????? ? ?????, ??????? ????????????? ??? ???????? ???????
|
||||
var keyInfo = new KeyInfo();
|
||||
keyInfo.AddClause(keyValue);
|
||||
signedXml.KeyInfo = keyInfo;
|
||||
|
||||
// Вычисление подписи
|
||||
// ?????????? ???????
|
||||
signedXml.ComputeSignature();
|
||||
|
||||
// Получение XML-представления подписи
|
||||
// ????????? XML-????????????? ???????
|
||||
var signatureXml = signedXml.GetXml();
|
||||
|
||||
// Добавление подписи в исходный документ
|
||||
// ?????????? ??????? ? ???????? ????????
|
||||
xmlDocument.DocumentElement.AppendChild(xmlDocument.ImportNode(signatureXml, true));
|
||||
|
||||
return xmlDocument;
|
||||
@@ -114,22 +113,22 @@ namespace GostCryptography.Tests.Xml.Sign
|
||||
|
||||
private static bool VerifyXmlDocumentSignature(XmlDocument signedXmlDocument)
|
||||
{
|
||||
// Создание подписчика XML-документа
|
||||
// ???????? ?????????? XML-?????????
|
||||
var signedXml = new GostSignedXml(signedXmlDocument);
|
||||
|
||||
// Поиск узла с подписью
|
||||
// ????? ???? ? ????????
|
||||
var nodeList = signedXmlDocument.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl);
|
||||
|
||||
// Загрузка найденной подписи
|
||||
// ???????? ????????? ???????
|
||||
signedXml.LoadXml((XmlElement)nodeList[0]);
|
||||
|
||||
// Проверка подписи
|
||||
// ???????? ???????
|
||||
return signedXml.CheckSignature();
|
||||
}
|
||||
|
||||
private static string GetDigestMethod(GostAsymmetricAlgorithm signingKey)
|
||||
{
|
||||
// Имя алгоритма вычисляем динамически, чтобы сделать код теста универсальным
|
||||
// ??? ????????? ????????? ???????????, ????? ??????? ??? ????? ?????????????
|
||||
|
||||
using (var hashAlgorithm = signingKey.CreateHashAlgorithm())
|
||||
{
|
||||
@@ -137,4 +136,4 @@ namespace GostCryptography.Tests.Xml.Sign
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.Xml;
|
||||
using System.Xml;
|
||||
@@ -7,148 +7,14 @@ using GostCryptography.Base;
|
||||
using GostCryptography.Tests.Properties;
|
||||
using GostCryptography.Xml;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Xml.Sign
|
||||
{
|
||||
/// <summary>
|
||||
/// Подпись и проверка подписи запроса к сервису СМЭВ (Система межведомственного электронного взаимодействия).
|
||||
/// ??????? ? ???????? ??????? ??????? ? ??????? ???? (??????? ????????????????? ???????????? ??????????????).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает запрос к сервису СМЭВ, подписывает определенную часть данного запроса с использованием сертификата,
|
||||
/// а затем проверяет полученную цифровую подпись.
|
||||
/// ???? ??????? ?????? ? ??????? ????, ??????????? ???????????? ????? ??????? ??????? ? ?????????????? ???????????,
|
||||
/// ? ????? ????????? ?????????? ???????? ???????.
|
||||
/// </remarks>
|
||||
[TestFixture(Description = "Подпись и проверка подписи запроса к сервису СМЭВ (Система межведомственного электронного взаимодействия)")]
|
||||
public sealed class SignedXmlSmevTest
|
||||
{
|
||||
private const string WsSecurityExtNamespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
|
||||
private const string WsSecurityUtilityNamespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldSignXml(TestCertificateInfo testCase)
|
||||
{
|
||||
// Given
|
||||
var certificate = testCase.Certificate;
|
||||
var smevRequest = CreateSmevRequest();
|
||||
|
||||
// When
|
||||
var signedXmlDocument = SignSmevRequest(smevRequest, certificate);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(VerifySmevRequestSignature(signedXmlDocument));
|
||||
}
|
||||
|
||||
private static XmlDocument CreateSmevRequest()
|
||||
{
|
||||
var document = new XmlDocument();
|
||||
document.LoadXml(Resources.SmevExample);
|
||||
return document;
|
||||
}
|
||||
|
||||
private static XmlDocument SignSmevRequest(XmlDocument smevRequest, X509Certificate2 certificate)
|
||||
{
|
||||
// Создание подписчика XML-документа
|
||||
var signedXml = new GostSignedXml(smevRequest) { GetIdElementHandler = GetSmevIdElement };
|
||||
|
||||
// Установка ключа для создания подписи
|
||||
signedXml.SetSigningCertificate(certificate);
|
||||
|
||||
// Ссылка на узел, который нужно подписать, с указанием алгоритма хэширования
|
||||
var dataReference = new Reference { Uri = "#body", DigestMethod = GetDigestMethod(certificate) };
|
||||
|
||||
// Метод преобразования, применяемый к данным перед их подписью (в соответствии с методическими рекомендациями СМЭВ)
|
||||
var dataTransform = new XmlDsigExcC14NTransform();
|
||||
dataReference.AddTransform(dataTransform);
|
||||
|
||||
// Установка ссылки на узел
|
||||
signedXml.AddReference(dataReference);
|
||||
|
||||
// Установка алгоритма нормализации узла SignedInfo (в соответствии с методическими рекомендациями СМЭВ)
|
||||
signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;
|
||||
|
||||
// Установка алгоритма хэширования (в соответствии с методическими рекомендациями СМЭВ)
|
||||
signedXml.SignedInfo.SignatureMethod = GetSignatureMethod(certificate);
|
||||
|
||||
// Вычисление подписи
|
||||
signedXml.ComputeSignature();
|
||||
|
||||
// Получение XML-представления подписи
|
||||
var signatureXml = signedXml.GetXml();
|
||||
|
||||
// Добавление подписи в исходный документ
|
||||
smevRequest.GetElementsByTagName("ds:Signature")[0].PrependChild(smevRequest.ImportNode(signatureXml.GetElementsByTagName("SignatureValue")[0], true));
|
||||
smevRequest.GetElementsByTagName("ds:Signature")[0].PrependChild(smevRequest.ImportNode(signatureXml.GetElementsByTagName("SignedInfo")[0], true));
|
||||
smevRequest.GetElementsByTagName("wsse:BinarySecurityToken")[0].InnerText = Convert.ToBase64String(certificate.RawData);
|
||||
|
||||
return smevRequest;
|
||||
}
|
||||
|
||||
private static bool VerifySmevRequestSignature(XmlDocument signedSmevRequest)
|
||||
{
|
||||
// Создание подписчика XML-документа
|
||||
var signedXml = new GostSignedXml(signedSmevRequest) { GetIdElementHandler = GetSmevIdElement };
|
||||
|
||||
// Поиск узла с подписью
|
||||
var nodeList = signedSmevRequest.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl);
|
||||
|
||||
// Загрузка найденной подписи
|
||||
signedXml.LoadXml((XmlElement)nodeList[0]);
|
||||
|
||||
// Поиск ссылки на BinarySecurityToken
|
||||
var references = signedXml.KeyInfo.GetXml().GetElementsByTagName("Reference", WsSecurityExtNamespace);
|
||||
|
||||
if (references.Count > 0)
|
||||
{
|
||||
// Определение ссылки на сертификат (ссылка на узел документа)
|
||||
var binaryTokenReference = ((XmlElement)references[0]).GetAttribute("URI");
|
||||
|
||||
if (!string.IsNullOrEmpty(binaryTokenReference) && binaryTokenReference[0] == '#')
|
||||
{
|
||||
// Поиск элемента с закодированным в Base64 сертификатом
|
||||
var binaryTokenElement = signedXml.GetIdElement(signedSmevRequest, binaryTokenReference.Substring(1));
|
||||
|
||||
if (binaryTokenElement != null)
|
||||
{
|
||||
// Загрузка сертификата, который был использован для подписи
|
||||
var certificate = new X509Certificate2(Convert.FromBase64String(binaryTokenElement.InnerText));
|
||||
|
||||
// Проверка подписи
|
||||
return signedXml.CheckSignature(certificate.GetPublicKeyAlgorithm());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static XmlElement GetSmevIdElement(XmlDocument document, string idValue)
|
||||
{
|
||||
var namespaceManager = new XmlNamespaceManager(document.NameTable);
|
||||
namespaceManager.AddNamespace("wsu", WsSecurityUtilityNamespace);
|
||||
|
||||
return document.SelectSingleNode("//*[@wsu:Id='" + idValue + "']", namespaceManager) as XmlElement;
|
||||
}
|
||||
|
||||
private static string GetSignatureMethod(X509Certificate2 certificate)
|
||||
{
|
||||
// Имя алгоритма вычисляем динамически, чтобы сделать код теста универсальным
|
||||
|
||||
using (var publicKey = (GostAsymmetricAlgorithm)certificate.GetPublicKeyAlgorithm())
|
||||
{
|
||||
return publicKey.SignatureAlgorithm;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDigestMethod(X509Certificate2 certificate)
|
||||
{
|
||||
// Имя алгоритма вычисляем динамически, чтобы сделать код теста универсальным
|
||||
|
||||
using (var publicKey = (GostAsymmetricAlgorithm)certificate.GetPublicKeyAlgorithm())
|
||||
using (var hashAlgorithm = publicKey.CreateHashAlgorithm())
|
||||
{
|
||||
return hashAlgorithm.AlgorithmName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.Xml;
|
||||
using System.Xml;
|
||||
|
||||
@@ -6,126 +6,15 @@ using GostCryptography.Base;
|
||||
using GostCryptography.Tests.Properties;
|
||||
using GostCryptography.Xml;
|
||||
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace GostCryptography.Tests.Xml.Sign
|
||||
{
|
||||
/// <summary>
|
||||
/// Подпись и проверка подписи XML-документа с предварительным XSLT-преобразованием подписываемых данных.
|
||||
/// ??????? ? ???????? ??????? XML-????????? ? ??????????????? XSLT-??????????????? ????????????? ??????.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тест создает XML-документ, подписывает определенную часть данного документа с использованием сертификата,
|
||||
/// предварительно осуществляя XSLT-преобразование подписываемых данных, а затем проверяет полученную цифровую подпись.
|
||||
/// ???? ??????? XML-????????, ??????????? ???????????? ????? ??????? ????????? ? ?????????????? ???????????,
|
||||
/// ?????????????? ??????????? XSLT-?????????????? ????????????? ??????, ? ????? ????????? ?????????? ???????? ???????.
|
||||
/// </remarks>
|
||||
[Ignore("TODO: Нужно произвести диагностику с подключением логирования")]
|
||||
[TestFixture(Description = "Подпись и проверка подписи XML-документа с предварительным XSLT-преобразованием подписываемых данных")]
|
||||
public sealed class SignedXmlTransformTest
|
||||
{
|
||||
[Test]
|
||||
[TestCaseSource(typeof(TestConfig), nameof(TestConfig.Gost_R3410_Certificates))]
|
||||
public void ShouldSignXml(TestCertificateInfo testCase)
|
||||
{
|
||||
// Given
|
||||
var certificate = testCase.Certificate;
|
||||
var xmlDocument = CreateXmlDocument();
|
||||
|
||||
// When
|
||||
var signedXmlDocument = SignXmlDocument(xmlDocument, certificate);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(VerifyXmlDocumentSignature(signedXmlDocument));
|
||||
}
|
||||
|
||||
private static XmlDocument CreateXmlDocument()
|
||||
{
|
||||
var document = new XmlDocument();
|
||||
document.LoadXml(Resources.SignedXmlExample);
|
||||
return document;
|
||||
}
|
||||
|
||||
private static XmlDocument SignXmlDocument(XmlDocument xmlDocument, X509Certificate2 certificate)
|
||||
{
|
||||
// Создание подписчика XML-документа
|
||||
var signedXml = new GostSignedXml(xmlDocument);
|
||||
|
||||
// Установка ключа для создания подписи
|
||||
signedXml.SetSigningCertificate(certificate);
|
||||
|
||||
// Ссылка на узел, который нужно подписать, с указанием алгоритма хэширования
|
||||
var dataReference = new Reference { Uri = "#Id1", DigestMethod = GetDigestMethod(certificate) };
|
||||
|
||||
// Метод преобразования, применяемый к данным перед их подписью
|
||||
var dataTransform = CreateDataTransform();
|
||||
dataReference.AddTransform(dataTransform);
|
||||
|
||||
// Установка ссылки на узел
|
||||
signedXml.AddReference(dataReference);
|
||||
|
||||
// Установка информации о сертификате, который использовался для создания подписи
|
||||
var keyInfo = new KeyInfo();
|
||||
keyInfo.AddClause(new KeyInfoX509Data(certificate));
|
||||
signedXml.KeyInfo = keyInfo;
|
||||
|
||||
// Вычисление подписи
|
||||
signedXml.ComputeSignature();
|
||||
|
||||
// Получение XML-представления подписи
|
||||
var signatureXml = signedXml.GetXml();
|
||||
|
||||
// Добавление подписи в исходный документ
|
||||
xmlDocument.DocumentElement.AppendChild(xmlDocument.ImportNode(signatureXml, true));
|
||||
|
||||
return xmlDocument;
|
||||
}
|
||||
|
||||
private static XmlDsigXsltTransform CreateDataTransform()
|
||||
{
|
||||
var dataTransformDocument = new XmlDocument();
|
||||
|
||||
dataTransformDocument.LoadXml(@"
|
||||
<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns:ds='http://www.w3.org/2000/09/xmldsig#'>
|
||||
<xsl:template match='/'>
|
||||
<xsl:apply-templates />
|
||||
</xsl:template>
|
||||
<xsl:template match='*'>
|
||||
<xsl:copy>
|
||||
<xsl:copy-of select='@*' />
|
||||
<xsl:apply-templates />
|
||||
</xsl:copy>
|
||||
</xsl:template>
|
||||
<xsl:template match='ds:Signature' />
|
||||
</xsl:stylesheet>");
|
||||
|
||||
var dataTransform = new XmlDsigXsltTransform();
|
||||
dataTransform.LoadInnerXml(dataTransformDocument.ChildNodes);
|
||||
|
||||
return dataTransform;
|
||||
}
|
||||
|
||||
private static bool VerifyXmlDocumentSignature(XmlDocument signedXmlDocument)
|
||||
{
|
||||
// Создание подписчика XML-документа
|
||||
var signedXml = new GostSignedXml(signedXmlDocument);
|
||||
|
||||
// Поиск узла с подписью
|
||||
var nodeList = signedXmlDocument.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl);
|
||||
|
||||
// Загрузка найденной подписи
|
||||
signedXml.LoadXml((XmlElement)nodeList[0]);
|
||||
|
||||
// Проверка подписи
|
||||
return signedXml.CheckSignature();
|
||||
}
|
||||
|
||||
private static string GetDigestMethod(X509Certificate2 certificate)
|
||||
{
|
||||
// Имя алгоритма вычисляем динамически, чтобы сделать код теста универсальным
|
||||
|
||||
using (var publicKey = (GostAsymmetricAlgorithm)certificate.GetPublicKeyAlgorithm())
|
||||
using (var hashAlgorithm = publicKey.CreateHashAlgorithm())
|
||||
{
|
||||
return hashAlgorithm.AlgorithmName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
[Ignore("TODO: ????? ?????????? ??????????? ? ???????????? ???????????")]
|
||||
|
||||
@@ -34,6 +34,7 @@ public sealed class AIPlugin : IDoctorPlugin
|
||||
new LlmProviderConfigurationCheck(),
|
||||
new ClaudeProviderCheck(),
|
||||
new OpenAiProviderCheck(),
|
||||
new GeminiProviderCheck(),
|
||||
new OllamaProviderCheck(),
|
||||
new LocalInferenceCheck()
|
||||
];
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
using System.Net.Http.Headers;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.AI.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates Google Gemini API connectivity.
|
||||
/// </summary>
|
||||
public sealed class GeminiProviderCheck : IDoctorCheck
|
||||
{
|
||||
private const string DefaultModel = "gemini-1.5-flash";
|
||||
private const string DefaultEndpoint = "https://generativelanguage.googleapis.com/v1beta";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.ai.provider.gemini";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Gemini Provider";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates Google Gemini API connectivity and authentication";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["ai", "llm", "gemini", "google", "advisoryai"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
var apiKey = context.Configuration.GetValue<string>("AdvisoryAI:LlmProviders:Gemini:ApiKey")
|
||||
?? Environment.GetEnvironmentVariable("GEMINI_API_KEY")
|
||||
?? Environment.GetEnvironmentVariable("GOOGLE_API_KEY");
|
||||
|
||||
return !string.IsNullOrWhiteSpace(apiKey);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.ai", DoctorCategory.AI.ToString());
|
||||
|
||||
var apiKey = context.Configuration.GetValue<string>("AdvisoryAI:LlmProviders:Gemini:ApiKey")
|
||||
?? Environment.GetEnvironmentVariable("GEMINI_API_KEY")
|
||||
?? Environment.GetEnvironmentVariable("GOOGLE_API_KEY");
|
||||
|
||||
var endpoint = context.Configuration.GetValue<string>("AdvisoryAI:LlmProviders:Gemini:Endpoint")
|
||||
?? DefaultEndpoint;
|
||||
|
||||
var model = context.Configuration.GetValue<string>("AdvisoryAI:LlmProviders:Gemini:Model")
|
||||
?? DefaultModel;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
return result
|
||||
.Skip("Gemini API key not configured")
|
||||
.WithEvidence("Gemini provider", e =>
|
||||
{
|
||||
e.Add("Endpoint", endpoint);
|
||||
e.Add("ApiKeyConfigured", "false");
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
var httpClientFactory = context.Services.GetService<IHttpClientFactory>();
|
||||
if (httpClientFactory == null)
|
||||
{
|
||||
return result
|
||||
.Skip("HttpClientFactory not available")
|
||||
.WithEvidence("Gemini provider", e =>
|
||||
{
|
||||
e.Add("Endpoint", endpoint);
|
||||
e.Add("Error", "IHttpClientFactory not registered");
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var client = httpClientFactory.CreateClient();
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
// List models to validate API key (lightweight call)
|
||||
using var response = await client.GetAsync($"{endpoint}/models?key={apiKey}", ct);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return result
|
||||
.Pass("Gemini API is accessible")
|
||||
.WithEvidence("Gemini provider", e =>
|
||||
{
|
||||
e.Add("Endpoint", endpoint);
|
||||
e.Add("Model", model);
|
||||
e.Add("ApiKeyConfigured", "true (masked)");
|
||||
e.Add("StatusCode", ((int)response.StatusCode).ToString());
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
var errorBody = await response.Content.ReadAsStringAsync(ct);
|
||||
var statusCode = (int)response.StatusCode;
|
||||
|
||||
var issues = new List<string>();
|
||||
if (statusCode == 400)
|
||||
{
|
||||
issues.Add("Invalid request - check API key format");
|
||||
}
|
||||
else if (statusCode == 401)
|
||||
{
|
||||
issues.Add("Invalid API key");
|
||||
}
|
||||
else if (statusCode == 403)
|
||||
{
|
||||
issues.Add("Access forbidden - check API key permissions or enable Generative Language API");
|
||||
}
|
||||
else if (statusCode == 429)
|
||||
{
|
||||
issues.Add("Rate limited - too many requests");
|
||||
}
|
||||
else
|
||||
{
|
||||
issues.Add($"API returned status {statusCode}");
|
||||
}
|
||||
|
||||
return result
|
||||
.Warn($"Gemini API issue: {response.StatusCode}")
|
||||
.WithEvidence("Gemini provider", e =>
|
||||
{
|
||||
e.Add("Endpoint", endpoint);
|
||||
e.Add("Model", model);
|
||||
e.Add("StatusCode", statusCode.ToString());
|
||||
e.Add("Error", TruncateError(errorBody));
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Verify API key", "Check GEMINI_API_KEY or GOOGLE_API_KEY is valid")
|
||||
.AddManualStep(2, "Enable API", "Ensure Generative Language API is enabled in Google Cloud Console")
|
||||
.AddManualStep(3, "Check quotas", "Verify API usage limits in Google Cloud Console"))
|
||||
.WithVerification("stella doctor --check check.ai.provider.gemini")
|
||||
.Build();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return result
|
||||
.Fail($"Cannot connect to Gemini API: {ex.Message}")
|
||||
.WithEvidence("Gemini provider", e =>
|
||||
{
|
||||
e.Add("Endpoint", endpoint);
|
||||
e.Add("Error", ex.Message);
|
||||
})
|
||||
.WithCauses("Network connectivity issue or invalid endpoint")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Check network", "Verify network connectivity to generativelanguage.googleapis.com")
|
||||
.AddManualStep(2, "Check proxy", "Ensure proxy settings are configured if required"))
|
||||
.WithVerification("stella doctor --check check.ai.provider.gemini")
|
||||
.Build();
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
return result
|
||||
.Fail($"Gemini API error: {ex.Message}")
|
||||
.WithEvidence("Gemini provider", e =>
|
||||
{
|
||||
e.Add("Endpoint", endpoint);
|
||||
e.Add("Error", ex.GetType().Name);
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
||||
private static string TruncateError(string error, int maxLength = 200)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(error))
|
||||
{
|
||||
return "(empty)";
|
||||
}
|
||||
|
||||
if (error.Length <= maxLength)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
return error[..maxLength] + "...";
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,15 @@ public sealed class LlmProviderConfigurationCheck : IDoctorCheck
|
||||
configuredProviders.Add("OpenAI");
|
||||
}
|
||||
|
||||
// Check Gemini configuration
|
||||
var geminiApiKey = context.Configuration.GetValue<string>("AdvisoryAI:LlmProviders:Gemini:ApiKey")
|
||||
?? Environment.GetEnvironmentVariable("GEMINI_API_KEY")
|
||||
?? Environment.GetEnvironmentVariable("GOOGLE_API_KEY");
|
||||
if (!string.IsNullOrWhiteSpace(geminiApiKey))
|
||||
{
|
||||
configuredProviders.Add("Gemini");
|
||||
}
|
||||
|
||||
// Check Ollama configuration
|
||||
var ollamaEndpoint = context.Configuration.GetValue<string>("AdvisoryAI:LlmProviders:Ollama:Endpoint")
|
||||
?? "http://localhost:11434";
|
||||
@@ -96,6 +105,7 @@ public sealed class LlmProviderConfigurationCheck : IDoctorCheck
|
||||
{
|
||||
"claude" => configuredProviders.Contains("Claude"),
|
||||
"openai" => configuredProviders.Contains("OpenAI"),
|
||||
"gemini" => configuredProviders.Contains("Gemini"),
|
||||
"ollama" => configuredProviders.Contains("Ollama"),
|
||||
"llamacpp" or "llama" => configuredProviders.Contains("Llama.cpp"),
|
||||
_ => false
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Authority.Checks;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Authority;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin providing authority diagnostic checks including authentication plugins,
|
||||
/// user management, and bootstrap user validation.
|
||||
/// </summary>
|
||||
public sealed class AuthorityPlugin : IDoctorPlugin
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string PluginId => "stellaops.doctor.authority";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName => "Authority";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorCategory Category => DoctorCategory.Authority;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version Version => new(1, 0, 0);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version MinEngineVersion => new(1, 0, 0);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAvailable(IServiceProvider services) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<IDoctorCheck> GetChecks(DoctorPluginContext context) =>
|
||||
[
|
||||
new AuthorityPluginConfigurationCheck(),
|
||||
new AuthorityPluginConnectivityCheck(),
|
||||
new BootstrapUserExistsCheck(),
|
||||
new SuperUserExistsCheck(),
|
||||
new UserPasswordPolicyCheck()
|
||||
];
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(DoctorPluginContext context, CancellationToken ct) => Task.CompletedTask;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Authority.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates that at least one authority plugin (Standard or LDAP) is configured.
|
||||
/// </summary>
|
||||
public sealed class AuthorityPluginConfigurationCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.authority.plugin.configured";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Authority Plugin Configuration";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates that at least one authentication plugin is configured";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["authority", "authentication", "configuration", "security"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(50);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.authority", DoctorCategory.Authority.ToString());
|
||||
|
||||
var configuredPlugins = new List<string>();
|
||||
var issues = new List<string>();
|
||||
|
||||
// Check Standard plugin configuration
|
||||
var standardEnabled = context.Configuration.GetValue<bool?>("Authority:Plugins:Standard:Enabled");
|
||||
var standardSection = context.Configuration.GetSection("Authority:Plugins:Standard");
|
||||
if (standardEnabled == true || standardSection.Exists())
|
||||
{
|
||||
configuredPlugins.Add("Standard");
|
||||
}
|
||||
|
||||
// Check LDAP plugin configuration
|
||||
var ldapEnabled = context.Configuration.GetValue<bool?>("Authority:Plugins:Ldap:Enabled");
|
||||
var ldapSection = context.Configuration.GetSection("Authority:Plugins:Ldap");
|
||||
if (ldapEnabled == true || ldapSection.Exists())
|
||||
{
|
||||
var ldapServer = context.Configuration.GetValue<string>("Authority:Plugins:Ldap:Server");
|
||||
if (string.IsNullOrWhiteSpace(ldapServer))
|
||||
{
|
||||
issues.Add("LDAP plugin enabled but server not configured");
|
||||
}
|
||||
else
|
||||
{
|
||||
configuredPlugins.Add("LDAP");
|
||||
}
|
||||
}
|
||||
|
||||
// Check OIDC plugin configuration
|
||||
var oidcEnabled = context.Configuration.GetValue<bool?>("Authority:Plugins:Oidc:Enabled");
|
||||
var oidcSection = context.Configuration.GetSection("Authority:Plugins:Oidc");
|
||||
if (oidcEnabled == true || oidcSection.Exists())
|
||||
{
|
||||
var oidcAuthority = context.Configuration.GetValue<string>("Authority:Plugins:Oidc:Authority");
|
||||
if (string.IsNullOrWhiteSpace(oidcAuthority))
|
||||
{
|
||||
issues.Add("OIDC plugin enabled but authority not configured");
|
||||
}
|
||||
else
|
||||
{
|
||||
configuredPlugins.Add("OIDC");
|
||||
}
|
||||
}
|
||||
|
||||
// Check SAML plugin configuration
|
||||
var samlEnabled = context.Configuration.GetValue<bool?>("Authority:Plugins:Saml:Enabled");
|
||||
if (samlEnabled == true)
|
||||
{
|
||||
configuredPlugins.Add("SAML");
|
||||
}
|
||||
|
||||
if (configuredPlugins.Count == 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Fail("No authentication plugins configured")
|
||||
.WithEvidence("Authority configuration", e =>
|
||||
{
|
||||
e.Add("ConfiguredPlugins", "(none)");
|
||||
e.Add("StandardEnabled", standardEnabled?.ToString() ?? "(not set)");
|
||||
e.Add("LdapEnabled", ldapEnabled?.ToString() ?? "(not set)");
|
||||
e.Add("OidcEnabled", oidcEnabled?.ToString() ?? "(not set)");
|
||||
})
|
||||
.WithCauses(
|
||||
"No authentication plugin is enabled in configuration",
|
||||
"Authority:Plugins section is missing or empty",
|
||||
"Users cannot authenticate without at least one plugin")
|
||||
.WithRemediation(r => r
|
||||
.AddStep(1, "Enable Standard authentication",
|
||||
"# Add to appsettings.json:\n" +
|
||||
"\"Authority\": {\n" +
|
||||
" \"Plugins\": {\n" +
|
||||
" \"Standard\": { \"Enabled\": true }\n" +
|
||||
" }\n" +
|
||||
"}",
|
||||
CommandType.FileEdit)
|
||||
.AddStep(2, "Or configure LDAP authentication",
|
||||
"# Add to appsettings.json:\n" +
|
||||
"\"Authority\": {\n" +
|
||||
" \"Plugins\": {\n" +
|
||||
" \"Ldap\": {\n" +
|
||||
" \"Enabled\": true,\n" +
|
||||
" \"Server\": \"ldap://your-server\"\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
"}",
|
||||
CommandType.FileEdit)
|
||||
.AddStep(3, "Run setup wizard to configure",
|
||||
"stella setup --step authority",
|
||||
CommandType.Shell))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"{issues.Count} configuration issue(s) found")
|
||||
.WithEvidence("Authority configuration", e =>
|
||||
{
|
||||
e.Add("ConfiguredPlugins", string.Join(", ", configuredPlugins));
|
||||
e.Add("Issues", string.Join("; ", issues));
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Review configuration", "Check Authority:Plugins section for missing values")
|
||||
.AddStep(2, "Run setup wizard", "stella setup --step authority", CommandType.Shell))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass($"{configuredPlugins.Count} authentication plugin(s) configured")
|
||||
.WithEvidence("Authority configuration", e =>
|
||||
{
|
||||
e.Add("ConfiguredPlugins", string.Join(", ", configuredPlugins));
|
||||
e.Add("PrimaryPlugin", configuredPlugins[0]);
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Authority.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates connectivity to configured authentication backends (DB for Standard, LDAP server for LDAP).
|
||||
/// </summary>
|
||||
public sealed class AuthorityPluginConnectivityCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.authority.plugin.connectivity";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Authority Backend Connectivity";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Tests connectivity to authentication backends (database or LDAP server)";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["authority", "connectivity", "ldap", "database"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
// Check if any plugin is configured
|
||||
var standardSection = context.Configuration.GetSection("Authority:Plugins:Standard");
|
||||
var ldapSection = context.Configuration.GetSection("Authority:Plugins:Ldap");
|
||||
return standardSection.Exists() || ldapSection.Exists();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.authority", DoctorCategory.Authority.ToString());
|
||||
|
||||
var connectivityResults = new List<(string Backend, bool Connected, string? Error)>();
|
||||
|
||||
// Check Standard plugin (database) connectivity
|
||||
var standardEnabled = context.Configuration.GetValue<bool?>("Authority:Plugins:Standard:Enabled");
|
||||
if (standardEnabled == true)
|
||||
{
|
||||
var dbConnected = await TestDatabaseConnectivityAsync(context, ct);
|
||||
connectivityResults.Add(("Database (Standard)", dbConnected.Success, dbConnected.Error));
|
||||
}
|
||||
|
||||
// Check LDAP plugin connectivity
|
||||
var ldapEnabled = context.Configuration.GetValue<bool?>("Authority:Plugins:Ldap:Enabled");
|
||||
if (ldapEnabled == true)
|
||||
{
|
||||
var ldapServer = context.Configuration.GetValue<string>("Authority:Plugins:Ldap:Server");
|
||||
if (!string.IsNullOrWhiteSpace(ldapServer))
|
||||
{
|
||||
var ldapConnected = await TestLdapConnectivityAsync(ldapServer, context, ct);
|
||||
connectivityResults.Add(("LDAP Server", ldapConnected.Success, ldapConnected.Error));
|
||||
}
|
||||
}
|
||||
|
||||
if (connectivityResults.Count == 0)
|
||||
{
|
||||
return result
|
||||
.Skip("No authentication backends configured to test")
|
||||
.Build();
|
||||
}
|
||||
|
||||
var failedBackends = connectivityResults.Where(r => !r.Connected).ToList();
|
||||
|
||||
if (failedBackends.Count > 0)
|
||||
{
|
||||
var evidenceBuilder = result.Fail($"{failedBackends.Count} backend(s) unreachable");
|
||||
|
||||
return evidenceBuilder
|
||||
.WithEvidence("Connectivity results", e =>
|
||||
{
|
||||
foreach (var (backend, connected, error) in connectivityResults)
|
||||
{
|
||||
e.Add(backend, connected ? "Connected" : $"Failed: {error}");
|
||||
}
|
||||
})
|
||||
.WithCauses(failedBackends.Select(f => $"{f.Backend}: {f.Error}").ToArray())
|
||||
.WithRemediation(r =>
|
||||
{
|
||||
if (failedBackends.Any(f => f.Backend.Contains("Database")))
|
||||
{
|
||||
r.AddManualStep(1, "Check database", "Verify PostgreSQL is running and accessible");
|
||||
r.AddStep(2, "Test database connection", "stella doctor --check check.database.connectivity", CommandType.Shell);
|
||||
}
|
||||
if (failedBackends.Any(f => f.Backend.Contains("LDAP")))
|
||||
{
|
||||
r.AddManualStep(3, "Check LDAP server", "Verify LDAP server is accessible from this network");
|
||||
r.AddManualStep(4, "Verify LDAP credentials", "Check Authority:Plugins:Ldap:BindDn and BindPassword");
|
||||
}
|
||||
})
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build();
|
||||
}
|
||||
|
||||
return result
|
||||
.Pass($"All {connectivityResults.Count} backend(s) reachable")
|
||||
.WithEvidence("Connectivity results", e =>
|
||||
{
|
||||
foreach (var (backend, connected, _) in connectivityResults)
|
||||
{
|
||||
e.Add(backend, "Connected");
|
||||
}
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static Task<(bool Success, string? Error)> TestDatabaseConnectivityAsync(
|
||||
DoctorPluginContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// In a real implementation, this would test the database connection.
|
||||
// For now, we assume success if the connection string is configured.
|
||||
var connectionString = context.Configuration.GetConnectionString("Authority")
|
||||
?? context.Configuration.GetConnectionString("Default");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
{
|
||||
return Task.FromResult<(bool Success, string? Error)>((false, "No connection string configured"));
|
||||
}
|
||||
|
||||
// TODO: Actually test the connection when integrated with Authority services
|
||||
return Task.FromResult((true, (string?)null));
|
||||
}
|
||||
|
||||
private static async Task<(bool Success, string? Error)> TestLdapConnectivityAsync(
|
||||
string ldapServer,
|
||||
DoctorPluginContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse LDAP URI to get host and port
|
||||
var uri = new Uri(ldapServer);
|
||||
var host = uri.Host;
|
||||
var port = uri.Port > 0 ? uri.Port : (uri.Scheme == "ldaps" ? 636 : 389);
|
||||
|
||||
using var client = new System.Net.Sockets.TcpClient();
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
|
||||
await client.ConnectAsync(host, port, cts.Token);
|
||||
return (true, null);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return (false, "Connection timed out");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Authority.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a bootstrap/super user exists in the system.
|
||||
/// </summary>
|
||||
public sealed class BootstrapUserExistsCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.authority.bootstrap.exists";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Bootstrap User Exists";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Verifies that at least one bootstrap/admin user exists";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["authority", "user", "bootstrap", "admin", "security"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(500);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
// Only run if Standard plugin is enabled (LDAP users are managed externally)
|
||||
var standardEnabled = context.Configuration.GetValue<bool?>("Authority:Plugins:Standard:Enabled");
|
||||
return standardEnabled == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.authority", DoctorCategory.Authority.ToString());
|
||||
|
||||
// Check if bootstrap user configuration exists
|
||||
var bootstrapUsername = context.Configuration.GetValue<string>("Authority:Bootstrap:Username")
|
||||
?? context.Configuration.GetValue<string>("Authority:Plugins:Standard:Bootstrap:Username");
|
||||
var bootstrapEmail = context.Configuration.GetValue<string>("Authority:Bootstrap:Email")
|
||||
?? context.Configuration.GetValue<string>("Authority:Plugins:Standard:Bootstrap:Email");
|
||||
|
||||
// Check if auto-bootstrap is configured
|
||||
var autoBootstrap = context.Configuration.GetValue<bool?>("Authority:Bootstrap:Enabled")
|
||||
?? context.Configuration.GetValue<bool?>("Authority:Plugins:Standard:Bootstrap:Enabled")
|
||||
?? true;
|
||||
|
||||
var hasBootstrapConfig = !string.IsNullOrWhiteSpace(bootstrapUsername)
|
||||
|| !string.IsNullOrWhiteSpace(bootstrapEmail);
|
||||
|
||||
if (!autoBootstrap && !hasBootstrapConfig)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Fail("No bootstrap user configured and auto-bootstrap is disabled")
|
||||
.WithEvidence("Bootstrap configuration", e =>
|
||||
{
|
||||
e.Add("AutoBootstrap", "false");
|
||||
e.Add("BootstrapUsername", "(not set)");
|
||||
e.Add("BootstrapEmail", "(not set)");
|
||||
})
|
||||
.WithCauses(
|
||||
"Authority:Bootstrap:Enabled is false",
|
||||
"No bootstrap user credentials configured",
|
||||
"System cannot create initial admin user")
|
||||
.WithRemediation(r => r
|
||||
.AddStep(1, "Enable auto-bootstrap",
|
||||
"# Add to appsettings.json:\n" +
|
||||
"\"Authority\": {\n" +
|
||||
" \"Bootstrap\": {\n" +
|
||||
" \"Enabled\": true,\n" +
|
||||
" \"Username\": \"admin\",\n" +
|
||||
" \"Email\": \"admin@example.com\"\n" +
|
||||
" }\n" +
|
||||
"}",
|
||||
CommandType.FileEdit)
|
||||
.AddStep(2, "Or run setup wizard to create user",
|
||||
"stella setup --step users",
|
||||
CommandType.Shell))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (!hasBootstrapConfig)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Info("Bootstrap user will be auto-created on first startup")
|
||||
.WithEvidence("Bootstrap configuration", e =>
|
||||
{
|
||||
e.Add("AutoBootstrap", "true");
|
||||
e.Add("Status", "Will be created on startup");
|
||||
e.Add("Note", "Default admin user will be created if no users exist");
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
|
||||
// Validate bootstrap configuration completeness
|
||||
var issues = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(bootstrapUsername))
|
||||
{
|
||||
issues.Add("Bootstrap username not set");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(bootstrapEmail))
|
||||
{
|
||||
issues.Add("Bootstrap email not set");
|
||||
}
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn("Bootstrap user configuration is incomplete")
|
||||
.WithEvidence("Bootstrap configuration", e =>
|
||||
{
|
||||
e.Add("Username", bootstrapUsername ?? "(not set)");
|
||||
e.Add("Email", bootstrapEmail ?? "(not set)");
|
||||
e.Add("AutoBootstrap", autoBootstrap.ToString());
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Complete configuration", "Set missing bootstrap user fields")
|
||||
.AddStep(2, "Run setup wizard", "stella setup --step users", CommandType.Shell))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass("Bootstrap user is properly configured")
|
||||
.WithEvidence("Bootstrap configuration", e =>
|
||||
{
|
||||
e.Add("Username", bootstrapUsername!);
|
||||
e.Add("Email", bootstrapEmail!);
|
||||
e.Add("AutoBootstrap", autoBootstrap.ToString());
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Authority.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates that at least one super user (administrator) exists in the system.
|
||||
/// </summary>
|
||||
public sealed class SuperUserExistsCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.users.superuser.exists";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Super User Exists";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Verifies that at least one administrator user exists";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["authority", "user", "admin", "superuser", "security"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(1);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
// Only run if Standard plugin is enabled
|
||||
var standardEnabled = context.Configuration.GetValue<bool?>("Authority:Plugins:Standard:Enabled");
|
||||
return standardEnabled == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.authority", DoctorCategory.Authority.ToString());
|
||||
|
||||
// Check for configured super users
|
||||
var superUsersSection = context.Configuration.GetSection("Authority:Users:Administrators");
|
||||
var bootstrapUsername = context.Configuration.GetValue<string>("Authority:Bootstrap:Username");
|
||||
|
||||
var configuredAdmins = new List<string>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(bootstrapUsername))
|
||||
{
|
||||
configuredAdmins.Add(bootstrapUsername);
|
||||
}
|
||||
|
||||
// Check if there are explicitly configured administrators
|
||||
if (superUsersSection.Exists())
|
||||
{
|
||||
var admins = superUsersSection.GetChildren().Select(c => c.Value).Where(v => !string.IsNullOrWhiteSpace(v)).ToList();
|
||||
configuredAdmins.AddRange(admins!);
|
||||
}
|
||||
|
||||
// Check for admin role assignments
|
||||
var adminRoles = context.Configuration.GetSection("Authority:Roles:Administrators");
|
||||
if (adminRoles.Exists())
|
||||
{
|
||||
var roleMembers = adminRoles.GetChildren().Select(c => c.Value).Where(v => !string.IsNullOrWhiteSpace(v)).ToList();
|
||||
configuredAdmins.AddRange(roleMembers!);
|
||||
}
|
||||
|
||||
configuredAdmins = configuredAdmins.Distinct().ToList();
|
||||
|
||||
if (configuredAdmins.Count == 0)
|
||||
{
|
||||
// Check if auto-bootstrap will create one
|
||||
var autoBootstrap = context.Configuration.GetValue<bool?>("Authority:Bootstrap:Enabled") ?? true;
|
||||
|
||||
if (autoBootstrap)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Info("No administrators configured but auto-bootstrap is enabled")
|
||||
.WithEvidence("Administrator status", e =>
|
||||
{
|
||||
e.Add("ConfiguredAdmins", "(none)");
|
||||
e.Add("AutoBootstrap", "true");
|
||||
e.Add("Note", "Bootstrap user will be created as administrator on first startup");
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Fail("No administrator users configured")
|
||||
.WithEvidence("Administrator status", e =>
|
||||
{
|
||||
e.Add("ConfiguredAdmins", "(none)");
|
||||
e.Add("AutoBootstrap", "false");
|
||||
})
|
||||
.WithCauses(
|
||||
"No users assigned to administrator role",
|
||||
"Bootstrap user not configured",
|
||||
"Auto-bootstrap is disabled")
|
||||
.WithRemediation(r => r
|
||||
.AddStep(1, "Create super user via setup wizard",
|
||||
"stella setup --step users",
|
||||
CommandType.Shell)
|
||||
.AddStep(2, "Or enable auto-bootstrap",
|
||||
"# Add to appsettings.json:\n" +
|
||||
"\"Authority\": {\n" +
|
||||
" \"Bootstrap\": { \"Enabled\": true }\n" +
|
||||
"}",
|
||||
CommandType.FileEdit))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass($"{configuredAdmins.Count} administrator(s) configured")
|
||||
.WithEvidence("Administrator status", e =>
|
||||
{
|
||||
e.Add("ConfiguredAdmins", string.Join(", ", configuredAdmins.Take(5)));
|
||||
if (configuredAdmins.Count > 5)
|
||||
{
|
||||
e.Add("AdditionalAdmins", $"+{configuredAdmins.Count - 5} more");
|
||||
}
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Authority.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates that password policy settings meet security requirements.
|
||||
/// </summary>
|
||||
public sealed class UserPasswordPolicyCheck : IDoctorCheck
|
||||
{
|
||||
private const int RecommendedMinLength = 12;
|
||||
private const int MinimumMinLength = 8;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.users.password.policy";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Password Policy";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates password policy configuration meets security requirements";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["authority", "password", "policy", "security"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(50);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
// Only run if Standard plugin is enabled (LDAP manages passwords externally)
|
||||
var standardEnabled = context.Configuration.GetValue<bool?>("Authority:Plugins:Standard:Enabled");
|
||||
return standardEnabled == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.authority", DoctorCategory.Authority.ToString());
|
||||
|
||||
// Get password policy settings with defaults
|
||||
var policySection = context.Configuration.GetSection("Authority:PasswordPolicy");
|
||||
|
||||
var minLength = context.Configuration.GetValue<int?>("Authority:PasswordPolicy:MinLength") ?? 8;
|
||||
var requireUppercase = context.Configuration.GetValue<bool?>("Authority:PasswordPolicy:RequireUppercase") ?? true;
|
||||
var requireLowercase = context.Configuration.GetValue<bool?>("Authority:PasswordPolicy:RequireLowercase") ?? true;
|
||||
var requireDigit = context.Configuration.GetValue<bool?>("Authority:PasswordPolicy:RequireDigit") ?? true;
|
||||
var requireSpecialChar = context.Configuration.GetValue<bool?>("Authority:PasswordPolicy:RequireSpecialCharacter") ?? true;
|
||||
var maxAge = context.Configuration.GetValue<int?>("Authority:PasswordPolicy:MaxAgeDays");
|
||||
var preventReuse = context.Configuration.GetValue<int?>("Authority:PasswordPolicy:PreventReuseCount") ?? 5;
|
||||
|
||||
var issues = new List<string>();
|
||||
var recommendations = new List<string>();
|
||||
|
||||
// Check minimum length
|
||||
if (minLength < MinimumMinLength)
|
||||
{
|
||||
issues.Add($"Minimum password length ({minLength}) is below absolute minimum ({MinimumMinLength})");
|
||||
}
|
||||
else if (minLength < RecommendedMinLength)
|
||||
{
|
||||
recommendations.Add($"Consider increasing minimum length from {minLength} to {RecommendedMinLength}");
|
||||
}
|
||||
|
||||
// Check complexity requirements
|
||||
var enabledRequirements = new List<string>();
|
||||
if (requireUppercase) enabledRequirements.Add("Uppercase");
|
||||
if (requireLowercase) enabledRequirements.Add("Lowercase");
|
||||
if (requireDigit) enabledRequirements.Add("Digit");
|
||||
if (requireSpecialChar) enabledRequirements.Add("Special character");
|
||||
|
||||
if (enabledRequirements.Count < 3)
|
||||
{
|
||||
recommendations.Add($"Only {enabledRequirements.Count} complexity requirements enabled (recommend 3+)");
|
||||
}
|
||||
|
||||
// Check password age policy
|
||||
if (maxAge.HasValue && maxAge.Value < 30)
|
||||
{
|
||||
recommendations.Add($"Password max age ({maxAge} days) is very short - may frustrate users");
|
||||
}
|
||||
|
||||
// Check reuse prevention
|
||||
if (preventReuse < 3)
|
||||
{
|
||||
recommendations.Add($"Password reuse prevention ({preventReuse}) is low (recommend 5+)");
|
||||
}
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Fail($"{issues.Count} password policy violation(s)")
|
||||
.WithEvidence("Password policy", e =>
|
||||
{
|
||||
e.Add("MinLength", minLength.ToString());
|
||||
e.Add("Requirements", string.Join(", ", enabledRequirements));
|
||||
e.Add("MaxAgeDays", maxAge?.ToString() ?? "(not set)");
|
||||
e.Add("PreventReuseCount", preventReuse.ToString());
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddStep(1, "Update password policy",
|
||||
"# Add to appsettings.json:\n" +
|
||||
"\"Authority\": {\n" +
|
||||
" \"PasswordPolicy\": {\n" +
|
||||
" \"MinLength\": 12,\n" +
|
||||
" \"RequireUppercase\": true,\n" +
|
||||
" \"RequireLowercase\": true,\n" +
|
||||
" \"RequireDigit\": true,\n" +
|
||||
" \"RequireSpecialCharacter\": true\n" +
|
||||
" }\n" +
|
||||
"}",
|
||||
CommandType.FileEdit))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (recommendations.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"{recommendations.Count} password policy recommendation(s)")
|
||||
.WithEvidence("Password policy", e =>
|
||||
{
|
||||
e.Add("MinLength", minLength.ToString());
|
||||
e.Add("Requirements", string.Join(", ", enabledRequirements));
|
||||
e.Add("MaxAgeDays", maxAge?.ToString() ?? "(not set)");
|
||||
e.Add("PreventReuseCount", preventReuse.ToString());
|
||||
})
|
||||
.WithCauses(recommendations.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Review recommendations", "Consider strengthening password policy")
|
||||
.AddStep(2, "Run setup wizard", "stella setup --step authority", CommandType.Shell))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass("Password policy meets security requirements")
|
||||
.WithEvidence("Password policy", e =>
|
||||
{
|
||||
e.Add("MinLength", minLength.ToString());
|
||||
e.Add("Requirements", string.Join(", ", enabledRequirements));
|
||||
e.Add("MaxAgeDays", maxAge?.ToString() ?? "(not enforced)");
|
||||
e.Add("PreventReuseCount", preventReuse.ToString());
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Authority.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering the Authority Doctor plugin.
|
||||
/// </summary>
|
||||
public static class AuthorityPluginExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the Authority Doctor plugin to the service collection.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddAuthorityDoctorPlugin(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IDoctorPlugin, AuthorityPlugin>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Doctor\StellaOps.Doctor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,178 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Notify.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates that at least one notification channel is configured.
|
||||
/// </summary>
|
||||
public sealed class NotifyChannelConfigurationCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.notify.channel.configured";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Notification Channel Configuration";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates that at least one notification channel is configured";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Info;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["notify", "channel", "configuration"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(50);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.notify", DoctorCategory.Notify.ToString());
|
||||
|
||||
var configuredChannels = new List<string>();
|
||||
var issues = new List<string>();
|
||||
|
||||
// Check Email channel configuration
|
||||
var emailEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Email:Enabled");
|
||||
var emailSection = context.Configuration.GetSection("Notify:Channels:Email");
|
||||
if (emailEnabled == true || emailSection.Exists())
|
||||
{
|
||||
var smtpHost = context.Configuration.GetValue<string>("Notify:Channels:Email:SmtpHost");
|
||||
if (string.IsNullOrWhiteSpace(smtpHost))
|
||||
{
|
||||
issues.Add("Email channel enabled but SMTP host not configured");
|
||||
}
|
||||
else
|
||||
{
|
||||
configuredChannels.Add("Email");
|
||||
}
|
||||
}
|
||||
|
||||
// Check Slack channel configuration
|
||||
var slackEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Slack:Enabled");
|
||||
var slackSection = context.Configuration.GetSection("Notify:Channels:Slack");
|
||||
if (slackEnabled == true || slackSection.Exists())
|
||||
{
|
||||
var webhookUrl = context.Configuration.GetValue<string>("Notify:Channels:Slack:WebhookUrl");
|
||||
var token = context.Configuration.GetValue<string>("Notify:Channels:Slack:Token");
|
||||
if (string.IsNullOrWhiteSpace(webhookUrl) && string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
issues.Add("Slack channel enabled but no webhook URL or token configured");
|
||||
}
|
||||
else
|
||||
{
|
||||
configuredChannels.Add("Slack");
|
||||
}
|
||||
}
|
||||
|
||||
// Check Teams channel configuration
|
||||
var teamsEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Teams:Enabled");
|
||||
var teamsSection = context.Configuration.GetSection("Notify:Channels:Teams");
|
||||
if (teamsEnabled == true || teamsSection.Exists())
|
||||
{
|
||||
var webhookUrl = context.Configuration.GetValue<string>("Notify:Channels:Teams:WebhookUrl");
|
||||
if (string.IsNullOrWhiteSpace(webhookUrl))
|
||||
{
|
||||
issues.Add("Teams channel enabled but webhook URL not configured");
|
||||
}
|
||||
else
|
||||
{
|
||||
configuredChannels.Add("Teams");
|
||||
}
|
||||
}
|
||||
|
||||
// Check Webhook channel configuration
|
||||
var webhookEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Webhook:Enabled");
|
||||
var webhookSection = context.Configuration.GetSection("Notify:Channels:Webhook");
|
||||
if (webhookEnabled == true || webhookSection.Exists())
|
||||
{
|
||||
var endpoint = context.Configuration.GetValue<string>("Notify:Channels:Webhook:Endpoint");
|
||||
if (string.IsNullOrWhiteSpace(endpoint))
|
||||
{
|
||||
issues.Add("Webhook channel enabled but endpoint not configured");
|
||||
}
|
||||
else
|
||||
{
|
||||
configuredChannels.Add("Webhook");
|
||||
}
|
||||
}
|
||||
|
||||
if (configuredChannels.Count == 0 && issues.Count == 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Info("No notification channels configured")
|
||||
.WithEvidence("Notify configuration", e =>
|
||||
{
|
||||
e.Add("ConfiguredChannels", "(none)");
|
||||
e.Add("Note", "Notifications are optional - configure channels to receive alerts");
|
||||
})
|
||||
.WithRemediation(r => r
|
||||
.AddStep(1, "Configure Email notifications",
|
||||
"# Add to appsettings.json:\n" +
|
||||
"\"Notify\": {\n" +
|
||||
" \"Channels\": {\n" +
|
||||
" \"Email\": {\n" +
|
||||
" \"Enabled\": true,\n" +
|
||||
" \"SmtpHost\": \"smtp.example.com\",\n" +
|
||||
" \"SmtpPort\": 587,\n" +
|
||||
" \"FromAddress\": \"alerts@example.com\"\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
"}",
|
||||
CommandType.FileEdit)
|
||||
.AddStep(2, "Or run setup wizard",
|
||||
"stella setup --step notify",
|
||||
CommandType.Shell))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (issues.Count > 0 && configuredChannels.Count == 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"{issues.Count} channel configuration issue(s)")
|
||||
.WithEvidence("Notify configuration", e =>
|
||||
{
|
||||
e.Add("ConfiguredChannels", "(none - all have issues)");
|
||||
e.Add("Issues", string.Join("; ", issues));
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Review configuration", "Check Notify:Channels section for missing values")
|
||||
.AddStep(2, "Run setup wizard", "stella setup --step notify", CommandType.Shell))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"{configuredChannels.Count} channel(s) configured, {issues.Count} issue(s)")
|
||||
.WithEvidence("Notify configuration", e =>
|
||||
{
|
||||
e.Add("ConfiguredChannels", string.Join(", ", configuredChannels));
|
||||
e.Add("Issues", string.Join("; ", issues));
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Review configuration", "Check Notify:Channels section for missing values"))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass($"{configuredChannels.Count} notification channel(s) configured")
|
||||
.WithEvidence("Notify configuration", e =>
|
||||
{
|
||||
e.Add("ConfiguredChannels", string.Join(", ", configuredChannels));
|
||||
e.Add("PrimaryChannel", configuredChannels[0]);
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Notify.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates connectivity to configured notification channels.
|
||||
/// </summary>
|
||||
public sealed class NotifyChannelConnectivityCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.notify.channel.connectivity";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Notification Channel Connectivity";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Tests connectivity to configured notification channel endpoints";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["notify", "channel", "connectivity", "network"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
// Only run if at least one channel is configured
|
||||
var emailSection = context.Configuration.GetSection("Notify:Channels:Email");
|
||||
var slackSection = context.Configuration.GetSection("Notify:Channels:Slack");
|
||||
var teamsSection = context.Configuration.GetSection("Notify:Channels:Teams");
|
||||
var webhookSection = context.Configuration.GetSection("Notify:Channels:Webhook");
|
||||
|
||||
return emailSection.Exists() || slackSection.Exists() || teamsSection.Exists() || webhookSection.Exists();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.notify", DoctorCategory.Notify.ToString());
|
||||
|
||||
var connectivityResults = new List<(string Channel, bool Connected, string? Error)>();
|
||||
|
||||
// Test Email connectivity (SMTP)
|
||||
var emailEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Email:Enabled") ?? true;
|
||||
var smtpHost = context.Configuration.GetValue<string>("Notify:Channels:Email:SmtpHost");
|
||||
if (emailEnabled && !string.IsNullOrWhiteSpace(smtpHost))
|
||||
{
|
||||
var smtpPort = context.Configuration.GetValue<int?>("Notify:Channels:Email:SmtpPort") ?? 587;
|
||||
var emailResult = await TestSmtpConnectivityAsync(smtpHost, smtpPort, ct);
|
||||
connectivityResults.Add(("Email (SMTP)", emailResult.Success, emailResult.Error));
|
||||
}
|
||||
|
||||
// Test Slack connectivity
|
||||
var slackEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Slack:Enabled") ?? true;
|
||||
var slackWebhook = context.Configuration.GetValue<string>("Notify:Channels:Slack:WebhookUrl");
|
||||
if (slackEnabled && !string.IsNullOrWhiteSpace(slackWebhook))
|
||||
{
|
||||
var slackResult = await TestHttpEndpointAsync(slackWebhook, "Slack", ct);
|
||||
connectivityResults.Add(("Slack", slackResult.Success, slackResult.Error));
|
||||
}
|
||||
|
||||
// Test Teams connectivity
|
||||
var teamsEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Teams:Enabled") ?? true;
|
||||
var teamsWebhook = context.Configuration.GetValue<string>("Notify:Channels:Teams:WebhookUrl");
|
||||
if (teamsEnabled && !string.IsNullOrWhiteSpace(teamsWebhook))
|
||||
{
|
||||
var teamsResult = await TestHttpEndpointAsync(teamsWebhook, "Teams", ct);
|
||||
connectivityResults.Add(("Teams", teamsResult.Success, teamsResult.Error));
|
||||
}
|
||||
|
||||
// Test Webhook connectivity
|
||||
var webhookEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Webhook:Enabled") ?? true;
|
||||
var webhookEndpoint = context.Configuration.GetValue<string>("Notify:Channels:Webhook:Endpoint");
|
||||
if (webhookEnabled && !string.IsNullOrWhiteSpace(webhookEndpoint))
|
||||
{
|
||||
var webhookResult = await TestHttpEndpointAsync(webhookEndpoint, "Webhook", ct);
|
||||
connectivityResults.Add(("Webhook", webhookResult.Success, webhookResult.Error));
|
||||
}
|
||||
|
||||
if (connectivityResults.Count == 0)
|
||||
{
|
||||
return result
|
||||
.Skip("No notification channels configured to test")
|
||||
.Build();
|
||||
}
|
||||
|
||||
var failedChannels = connectivityResults.Where(r => !r.Connected).ToList();
|
||||
|
||||
if (failedChannels.Count > 0)
|
||||
{
|
||||
return result
|
||||
.Warn($"{failedChannels.Count} of {connectivityResults.Count} channel(s) unreachable")
|
||||
.WithEvidence("Connectivity results", e =>
|
||||
{
|
||||
foreach (var (channel, connected, error) in connectivityResults)
|
||||
{
|
||||
e.Add(channel, connected ? "Connected" : $"Failed: {error}");
|
||||
}
|
||||
})
|
||||
.WithCauses(failedChannels.Select(f => $"{f.Channel}: {f.Error}").ToArray())
|
||||
.WithRemediation(r =>
|
||||
{
|
||||
if (failedChannels.Any(f => f.Channel.Contains("SMTP")))
|
||||
{
|
||||
r.AddManualStep(1, "Check SMTP server", "Verify SMTP server is accessible and credentials are correct");
|
||||
}
|
||||
if (failedChannels.Any(f => f.Channel.Contains("Slack")))
|
||||
{
|
||||
r.AddManualStep(2, "Check Slack webhook", "Verify Slack webhook URL is valid and not expired");
|
||||
}
|
||||
if (failedChannels.Any(f => f.Channel.Contains("Teams")))
|
||||
{
|
||||
r.AddManualStep(3, "Check Teams webhook", "Verify Teams webhook URL is valid");
|
||||
}
|
||||
if (failedChannels.Any(f => f.Channel.Contains("Webhook")))
|
||||
{
|
||||
r.AddManualStep(4, "Check webhook endpoint", "Verify webhook endpoint is accessible from this network");
|
||||
}
|
||||
})
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build();
|
||||
}
|
||||
|
||||
return result
|
||||
.Pass($"All {connectivityResults.Count} channel(s) reachable")
|
||||
.WithEvidence("Connectivity results", e =>
|
||||
{
|
||||
foreach (var (channel, _, _) in connectivityResults)
|
||||
{
|
||||
e.Add(channel, "Connected");
|
||||
}
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static async Task<(bool Success, string? Error)> TestSmtpConnectivityAsync(
|
||||
string host,
|
||||
int port,
|
||||
CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new System.Net.Sockets.TcpClient();
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
|
||||
await client.ConnectAsync(host, port, cts.Token);
|
||||
return (true, null);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return (false, "Connection timed out");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<(bool Success, string? Error)> TestHttpEndpointAsync(
|
||||
string url,
|
||||
string channelType,
|
||||
CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
{
|
||||
return (false, "Invalid URL format");
|
||||
}
|
||||
|
||||
// Just test TCP connectivity to the host/port, don't send actual requests
|
||||
var port = uri.Port > 0 ? uri.Port : (uri.Scheme == "https" ? 443 : 80);
|
||||
|
||||
using var client = new System.Net.Sockets.TcpClient();
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
|
||||
await client.ConnectAsync(uri.Host, port, cts.Token);
|
||||
return (true, null);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return (false, "Connection timed out");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Notify.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates notification delivery capability by checking queue health and delivery configuration.
|
||||
/// </summary>
|
||||
public sealed class NotifyDeliveryTestCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.notify.delivery.test";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Notification Delivery Health";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates notification delivery queue and configuration health";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["notify", "delivery", "queue", "health"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
// Only run if notification system is configured
|
||||
var notifySection = context.Configuration.GetSection("Notify");
|
||||
return notifySection.Exists();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.notify", DoctorCategory.Notify.ToString());
|
||||
|
||||
var issues = new List<string>();
|
||||
var warnings = new List<string>();
|
||||
|
||||
// Check queue transport configuration
|
||||
var queueTransport = context.Configuration.GetValue<string>("Notify:Queue:Transport");
|
||||
var hasQueueConfig = !string.IsNullOrWhiteSpace(queueTransport);
|
||||
|
||||
if (!hasQueueConfig)
|
||||
{
|
||||
// Check for Redis or NATS configuration as fallback
|
||||
var redisConnection = context.Configuration.GetValue<string>("Notify:Queue:Redis:ConnectionString")
|
||||
?? context.Configuration.GetConnectionString("Redis");
|
||||
var natsConnection = context.Configuration.GetValue<string>("Notify:Queue:Nats:Url")
|
||||
?? context.Configuration.GetValue<string>("Nats:Url");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(redisConnection) && string.IsNullOrWhiteSpace(natsConnection))
|
||||
{
|
||||
warnings.Add("No queue transport configured - notifications may be processed in-memory only");
|
||||
}
|
||||
else
|
||||
{
|
||||
queueTransport = !string.IsNullOrWhiteSpace(redisConnection) ? "Redis" : "NATS";
|
||||
}
|
||||
}
|
||||
|
||||
// Check delivery retry configuration
|
||||
var maxRetries = context.Configuration.GetValue<int?>("Notify:Delivery:MaxRetries");
|
||||
if (maxRetries.HasValue && maxRetries.Value < 1)
|
||||
{
|
||||
issues.Add("MaxRetries is set to 0 or negative - failed deliveries will not be retried");
|
||||
}
|
||||
else if (maxRetries.HasValue && maxRetries.Value > 10)
|
||||
{
|
||||
warnings.Add($"MaxRetries ({maxRetries}) is high - consider reducing to avoid delivery delays");
|
||||
}
|
||||
|
||||
// Check throttle configuration
|
||||
var throttleEnabled = context.Configuration.GetValue<bool?>("Notify:Throttle:Enabled");
|
||||
var throttleWindow = context.Configuration.GetValue<string>("Notify:Throttle:Window");
|
||||
var throttleLimit = context.Configuration.GetValue<int?>("Notify:Throttle:Limit");
|
||||
|
||||
if (throttleEnabled == true)
|
||||
{
|
||||
if (throttleLimit.HasValue && throttleLimit.Value < 10)
|
||||
{
|
||||
warnings.Add($"Throttle limit ({throttleLimit}) is very low - may cause notification delays");
|
||||
}
|
||||
}
|
||||
|
||||
// Check digest configuration
|
||||
var digestEnabled = context.Configuration.GetValue<bool?>("Notify:Digest:Enabled");
|
||||
var digestInterval = context.Configuration.GetValue<string>("Notify:Digest:Interval");
|
||||
|
||||
// Check for default channel configuration
|
||||
var defaultChannel = context.Configuration.GetValue<string>("Notify:DefaultChannel");
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Fail($"{issues.Count} delivery configuration issue(s)")
|
||||
.WithEvidence("Delivery configuration", e =>
|
||||
{
|
||||
e.Add("QueueTransport", queueTransport ?? "(not configured)");
|
||||
e.Add("MaxRetries", maxRetries?.ToString() ?? "(default)");
|
||||
e.Add("ThrottleEnabled", throttleEnabled?.ToString() ?? "(default)");
|
||||
e.Add("DigestEnabled", digestEnabled?.ToString() ?? "(default)");
|
||||
e.Add("DefaultChannel", defaultChannel ?? "(not set)");
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Review delivery settings", "Check Notify:Delivery section for invalid values")
|
||||
.AddStep(2, "Run setup wizard", "stella setup --step notify", CommandType.Shell))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (warnings.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"{warnings.Count} delivery configuration recommendation(s)")
|
||||
.WithEvidence("Delivery configuration", e =>
|
||||
{
|
||||
e.Add("QueueTransport", queueTransport ?? "(in-memory)");
|
||||
e.Add("MaxRetries", maxRetries?.ToString() ?? "(default: 3)");
|
||||
e.Add("ThrottleEnabled", throttleEnabled?.ToString() ?? "(default: false)");
|
||||
e.Add("DigestEnabled", digestEnabled?.ToString() ?? "(default: false)");
|
||||
e.Add("DefaultChannel", defaultChannel ?? "(not set)");
|
||||
})
|
||||
.WithCauses(warnings.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Review recommendations", "Consider adjusting notification delivery settings")
|
||||
.AddStep(2, "Configure queue transport",
|
||||
"# Add to appsettings.json for Redis:\n" +
|
||||
"\"Notify\": {\n" +
|
||||
" \"Queue\": {\n" +
|
||||
" \"Transport\": \"Redis\",\n" +
|
||||
" \"Redis\": { \"ConnectionString\": \"localhost:6379\" }\n" +
|
||||
" }\n" +
|
||||
"}",
|
||||
CommandType.FileEdit))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass("Notification delivery configuration is healthy")
|
||||
.WithEvidence("Delivery configuration", e =>
|
||||
{
|
||||
e.Add("QueueTransport", queueTransport ?? "(in-memory)");
|
||||
e.Add("MaxRetries", maxRetries?.ToString() ?? "(default: 3)");
|
||||
e.Add("ThrottleEnabled", throttleEnabled?.ToString() ?? "(default: false)");
|
||||
e.Add("DigestEnabled", digestEnabled?.ToString() ?? "(default: false)");
|
||||
if (!string.IsNullOrWhiteSpace(defaultChannel))
|
||||
{
|
||||
e.Add("DefaultChannel", defaultChannel);
|
||||
}
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Notify.Checks;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Notify;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin providing notification channel diagnostic checks including
|
||||
/// Email, Slack, Teams, and Webhook connectivity validation.
|
||||
/// </summary>
|
||||
public sealed class NotifyPlugin : IDoctorPlugin
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string PluginId => "stellaops.doctor.notify";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName => "Notifications";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorCategory Category => DoctorCategory.Notify;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version Version => new(1, 0, 0);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version MinEngineVersion => new(1, 0, 0);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAvailable(IServiceProvider services) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<IDoctorCheck> GetChecks(DoctorPluginContext context) =>
|
||||
[
|
||||
new NotifyChannelConfigurationCheck(),
|
||||
new NotifyChannelConnectivityCheck(),
|
||||
new NotifyDeliveryTestCheck()
|
||||
];
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(DoctorPluginContext context, CancellationToken ct) => Task.CompletedTask;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<RootNamespace>StellaOps.Doctor.Plugins.Notify</RootNamespace>
|
||||
<Description>Doctor plugin for Notify channel diagnostics</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Doctor\StellaOps.Doctor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,172 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// MirrorServerAuthCheck.cs
|
||||
// Sprint: SPRINT_20260114_SOURCES_SETUP
|
||||
// Task: 12.1 - Sources Doctor Plugin
|
||||
// Description: Check for mirror server authentication configuration
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Concelier.Core.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Sources.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies mirror server authentication is properly configured.
|
||||
/// </summary>
|
||||
public sealed class MirrorServerAuthCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.sources.mirror.auth";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Mirror Server Authentication";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Verifies mirror server authentication configuration when OAuth is enabled";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["security", "sources", "mirror", "authentication"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
var config = context.Configuration
|
||||
.GetSection("sources:mirrorServer")
|
||||
.Get<MirrorServerConfig>();
|
||||
|
||||
// Only run if mirror server is enabled
|
||||
return config?.Enabled == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.sources", DoctorCategory.Data.ToString());
|
||||
|
||||
var config = context.Configuration
|
||||
.GetSection("sources:mirrorServer")
|
||||
.Get<MirrorServerConfig>();
|
||||
|
||||
if (config is null || !config.Enabled)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Skip("Mirror server not enabled")
|
||||
.WithEvidence("Mirror server", e => e.Add("Enabled", "false"))
|
||||
.Build());
|
||||
}
|
||||
|
||||
// Check authentication mode
|
||||
if (config.Authentication == MirrorAuthMode.Anonymous)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Info("Mirror server configured for anonymous access")
|
||||
.WithEvidence("Mirror server authentication", e =>
|
||||
{
|
||||
e.Add("AuthMode", "Anonymous");
|
||||
e.Add("ExportRoot", config.ExportRoot);
|
||||
})
|
||||
.WithVerification("stella doctor --check check.sources.mirror.auth")
|
||||
.Build());
|
||||
}
|
||||
|
||||
// OAuth mode - validate configuration
|
||||
if (config.Authentication == MirrorAuthMode.OAuth)
|
||||
{
|
||||
if (config.OAuth is null)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Fail("OAuth authentication enabled but OAuth configuration missing")
|
||||
.WithEvidence("Mirror server authentication", e =>
|
||||
{
|
||||
e.Add("AuthMode", "OAuth");
|
||||
e.Add("OAuthConfigured", "false");
|
||||
})
|
||||
.WithCauses(
|
||||
"OAuth section not configured in mirror server settings",
|
||||
"Missing required OAuth issuer configuration")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Configure OAuth settings", "Add 'sources:mirrorServer:oauth' section with issuer URL")
|
||||
.AddShellStep(2, "Run setup wizard", "stella setup --step sources"))
|
||||
.WithVerification("stella doctor --check check.sources.mirror.auth")
|
||||
.Build());
|
||||
}
|
||||
|
||||
// Validate OAuth configuration - Issuer is required
|
||||
if (string.IsNullOrWhiteSpace(config.OAuth.Issuer))
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Fail("OAuth configuration incomplete: missing Issuer")
|
||||
.WithEvidence("Mirror server OAuth", e =>
|
||||
{
|
||||
e.Add("AuthMode", "OAuth");
|
||||
e.Add("IssuerConfigured", "false");
|
||||
})
|
||||
.WithCauses("OAuth Issuer URL not configured")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Configure OAuth issuer", "Set 'sources:mirrorServer:oauth:issuer' to your OIDC provider URL")
|
||||
.AddShellStep(2, "Verify issuer metadata", "curl -s {issuer}/.well-known/openid-configuration"))
|
||||
.WithVerification("stella doctor --check check.sources.mirror.auth")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass("Mirror server OAuth authentication configured")
|
||||
.WithEvidence("Mirror server OAuth", e =>
|
||||
{
|
||||
e.Add("AuthMode", "OAuth");
|
||||
e.Add("Issuer", config.OAuth.Issuer);
|
||||
e.Add("Audience", config.OAuth.Audience ?? "(not set)");
|
||||
e.Add("RequiredScopesCount", config.OAuth.RequiredScopes.Length.ToString());
|
||||
e.Add("RequireHttpsMetadata", config.OAuth.RequireHttpsMetadata.ToString());
|
||||
})
|
||||
.WithVerification("stella doctor --check check.sources.mirror.auth")
|
||||
.Build());
|
||||
}
|
||||
|
||||
// ApiKey mode
|
||||
if (config.Authentication == MirrorAuthMode.ApiKey)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Pass("Mirror server configured with API key authentication")
|
||||
.WithEvidence("Mirror server authentication", e =>
|
||||
{
|
||||
e.Add("AuthMode", "ApiKey");
|
||||
e.Add("ExportRoot", config.ExportRoot);
|
||||
})
|
||||
.WithVerification("stella doctor --check check.sources.mirror.auth")
|
||||
.Build());
|
||||
}
|
||||
|
||||
// mTLS mode
|
||||
if (config.Authentication == MirrorAuthMode.Mtls)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Pass("Mirror server configured with mTLS authentication")
|
||||
.WithEvidence("Mirror server authentication", e =>
|
||||
{
|
||||
e.Add("AuthMode", "Mtls");
|
||||
e.Add("ExportRoot", config.ExportRoot);
|
||||
})
|
||||
.WithVerification("stella doctor --check check.sources.mirror.auth")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass($"Mirror server authentication mode: {config.Authentication}")
|
||||
.WithEvidence("Mirror server authentication", e =>
|
||||
{
|
||||
e.Add("AuthMode", config.Authentication.ToString());
|
||||
e.Add("ExportRoot", config.ExportRoot);
|
||||
})
|
||||
.WithVerification("stella doctor --check check.sources.mirror.auth")
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// MirrorServerRateLimitCheck.cs
|
||||
// Sprint: SPRINT_20260114_SOURCES_SETUP
|
||||
// Task: 12.1 - Sources Doctor Plugin
|
||||
// Description: Check for mirror server rate limiting configuration
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Concelier.Core.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Sources.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies mirror server rate limiting is properly configured.
|
||||
/// </summary>
|
||||
public sealed class MirrorServerRateLimitCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.sources.mirror.ratelimit";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Mirror Server Rate Limiting";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Verifies mirror server rate limiting configuration for Router integration";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Info;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["configuration", "sources", "mirror", "ratelimit"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
var config = context.Configuration
|
||||
.GetSection("sources:mirrorServer")
|
||||
.Get<MirrorServerConfig>();
|
||||
|
||||
// Only run if mirror server is enabled
|
||||
return config?.Enabled == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.sources", DoctorCategory.Data.ToString());
|
||||
|
||||
var config = context.Configuration
|
||||
.GetSection("sources:mirrorServer")
|
||||
.Get<MirrorServerConfig>();
|
||||
|
||||
if (config is null || !config.Enabled)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Skip("Mirror server not enabled")
|
||||
.WithEvidence("Mirror server", e => e.Add("Enabled", "false"))
|
||||
.Build());
|
||||
}
|
||||
|
||||
var rateLimits = config.RateLimits;
|
||||
|
||||
if (!rateLimits.IsEnabled)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Info("Rate limiting not enabled for mirror server")
|
||||
.WithEvidence("Rate limiting", e =>
|
||||
{
|
||||
e.Add("Enabled", "false");
|
||||
e.Add("Recommendation", "Consider enabling rate limiting to protect against abuse");
|
||||
})
|
||||
.WithVerification("stella doctor --check check.sources.mirror.ratelimit")
|
||||
.Build());
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
var warnings = new List<string>();
|
||||
|
||||
// Check instance-level limits
|
||||
if (rateLimits.ForInstance is null)
|
||||
{
|
||||
warnings.Add("Instance-level rate limiting not configured");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rateLimits.ForInstance.MaxRequests <= 0)
|
||||
warnings.Add("Instance MaxRequests should be positive");
|
||||
if (rateLimits.ForInstance.PerSeconds <= 0)
|
||||
warnings.Add("Instance PerSeconds should be positive");
|
||||
}
|
||||
|
||||
// Check environment-level limits
|
||||
if (rateLimits.ForEnvironment is not null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rateLimits.ForEnvironment.ValkeyConnection))
|
||||
{
|
||||
warnings.Add("Environment rate limiting configured but Valkey connection missing");
|
||||
}
|
||||
|
||||
if (rateLimits.ForEnvironment.MaxRequests <= 0)
|
||||
warnings.Add("Environment MaxRequests should be positive");
|
||||
if (rateLimits.ForEnvironment.PerSeconds <= 0)
|
||||
warnings.Add("Environment PerSeconds should be positive");
|
||||
}
|
||||
|
||||
if (warnings.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"Rate limiting has configuration issues: {warnings.Count} warning(s)")
|
||||
.WithEvidence("Rate limiting configuration", e =>
|
||||
{
|
||||
e.Add("Enabled", "true");
|
||||
e.Add("Warnings", string.Join("; ", warnings));
|
||||
if (rateLimits.ForInstance is not null)
|
||||
{
|
||||
e.Add("Instance.MaxRequests", rateLimits.ForInstance.MaxRequests.ToString(CultureInfo.InvariantCulture));
|
||||
e.Add("Instance.PerSeconds", rateLimits.ForInstance.PerSeconds.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
if (rateLimits.ForEnvironment is not null)
|
||||
{
|
||||
e.Add("Environment.MaxRequests", rateLimits.ForEnvironment.MaxRequests.ToString(CultureInfo.InvariantCulture));
|
||||
e.Add("Environment.PerSeconds", rateLimits.ForEnvironment.PerSeconds.ToString(CultureInfo.InvariantCulture));
|
||||
e.Add("Environment.ValkeyConfigured", (!string.IsNullOrWhiteSpace(rateLimits.ForEnvironment.ValkeyConnection)).ToString());
|
||||
}
|
||||
})
|
||||
.WithCauses(warnings.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Review rate limit configuration", "Check sources:mirrorServer:rateLimits in configuration")
|
||||
.AddManualStep(2, "Set appropriate limits", "Configure MaxRequests and PerSeconds for your expected traffic"))
|
||||
.WithVerification("stella doctor --check check.sources.mirror.ratelimit")
|
||||
.Build());
|
||||
}
|
||||
|
||||
// Build route summary
|
||||
var routeCount = rateLimits.ForEnvironment?.Routes.Count ?? 0;
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass($"Rate limiting properly configured with {routeCount} route-specific rule(s)")
|
||||
.WithEvidence("Rate limiting configuration", e =>
|
||||
{
|
||||
e.Add("Enabled", "true");
|
||||
e.Add("ActivationThresholdPer5Min", rateLimits.ActivationThresholdPer5Min.ToString(CultureInfo.InvariantCulture));
|
||||
if (rateLimits.ForInstance is not null)
|
||||
{
|
||||
e.Add("Instance.MaxRequests", rateLimits.ForInstance.MaxRequests.ToString(CultureInfo.InvariantCulture));
|
||||
e.Add("Instance.PerSeconds", rateLimits.ForInstance.PerSeconds.ToString(CultureInfo.InvariantCulture));
|
||||
if (rateLimits.ForInstance.AllowBurstForSeconds.HasValue)
|
||||
{
|
||||
e.Add("Instance.BurstSeconds", rateLimits.ForInstance.AllowBurstForSeconds.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
if (rateLimits.ForEnvironment is not null)
|
||||
{
|
||||
e.Add("Environment.MaxRequests", rateLimits.ForEnvironment.MaxRequests.ToString(CultureInfo.InvariantCulture));
|
||||
e.Add("Environment.PerSeconds", rateLimits.ForEnvironment.PerSeconds.ToString(CultureInfo.InvariantCulture));
|
||||
e.Add("Environment.ValkeyBucket", rateLimits.ForEnvironment.ValkeyBucket);
|
||||
e.Add("Environment.RouteCount", routeCount.ToString(CultureInfo.InvariantCulture));
|
||||
if (rateLimits.ForEnvironment.CircuitBreaker is not null)
|
||||
{
|
||||
e.Add("CircuitBreaker.Enabled", "true");
|
||||
e.Add("CircuitBreaker.FailureThreshold", rateLimits.ForEnvironment.CircuitBreaker.FailureThreshold.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
})
|
||||
.WithVerification("stella doctor --check check.sources.mirror.ratelimit")
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SourceConnectivityCheck.cs
|
||||
// Sprint: SPRINT_20260114_SOURCES_SETUP
|
||||
// Task: 12.1 - Sources Doctor Plugin
|
||||
// Description: Individual source connectivity check with detailed remediation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Concelier.Core.Sources;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Builders;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Sources.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Connectivity check for a single advisory data source.
|
||||
/// Provides detailed error messages and remediation steps when connectivity fails.
|
||||
/// </summary>
|
||||
public sealed class SourceConnectivityCheck : IDoctorCheck
|
||||
{
|
||||
private readonly string _sourceId;
|
||||
private readonly string _displayName;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new source connectivity check for the specified source.
|
||||
/// </summary>
|
||||
/// <param name="sourceId">Source identifier.</param>
|
||||
/// <param name="displayName">Human-readable source name.</param>
|
||||
public SourceConnectivityCheck(string sourceId, string displayName)
|
||||
{
|
||||
_sourceId = sourceId;
|
||||
_displayName = displayName;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CheckId => $"check.sources.{_sourceId.ToLowerInvariant()}.connectivity";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => $"{_displayName} Connectivity";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => $"Verifies connectivity to {_displayName} advisory data source";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["connectivity", "sources", _sourceId.ToLowerInvariant()];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(15);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
var registry = context.Services.GetService<ISourceRegistry>();
|
||||
return registry?.GetSource(_sourceId) is not null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.sources", DoctorCategory.Data.ToString());
|
||||
|
||||
var registry = context.Services.GetService<ISourceRegistry>();
|
||||
if (registry is null)
|
||||
{
|
||||
return result
|
||||
.Skip("ISourceRegistry not available")
|
||||
.Build();
|
||||
}
|
||||
|
||||
var source = registry.GetSource(_sourceId);
|
||||
if (source is null)
|
||||
{
|
||||
return result
|
||||
.Skip($"Source {_sourceId} not found in registry")
|
||||
.WithEvidence("Source lookup", e => e.Add("SourceId", _sourceId))
|
||||
.Build();
|
||||
}
|
||||
|
||||
// Perform connectivity check
|
||||
var checkResult = await registry.CheckConnectivityAsync(_sourceId, ct);
|
||||
|
||||
if (checkResult.IsHealthy)
|
||||
{
|
||||
return result
|
||||
.Pass($"{_displayName} is reachable (latency: {checkResult.Latency?.TotalMilliseconds:F0}ms)")
|
||||
.WithEvidence("Connectivity check", e =>
|
||||
{
|
||||
e.Add("SourceId", _sourceId);
|
||||
e.Add("DisplayName", _displayName);
|
||||
e.Add("Status", checkResult.Status.ToString());
|
||||
e.Add("LatencyMs", checkResult.Latency?.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) ?? "N/A");
|
||||
e.Add("CheckedAt", checkResult.CheckedAt.ToString("O", CultureInfo.InvariantCulture));
|
||||
e.Add("Category", source.Category.ToString());
|
||||
e.Add("HealthCheckEndpoint", source.HealthCheckEndpoint);
|
||||
})
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build();
|
||||
}
|
||||
|
||||
if (checkResult.Status == SourceConnectivityStatus.Degraded)
|
||||
{
|
||||
return result
|
||||
.Warn($"{_displayName} is degraded: {checkResult.ErrorMessage}")
|
||||
.WithEvidence("Connectivity check", e =>
|
||||
{
|
||||
e.Add("SourceId", _sourceId);
|
||||
e.Add("DisplayName", _displayName);
|
||||
e.Add("Status", checkResult.Status.ToString());
|
||||
e.Add("ErrorCode", checkResult.ErrorCode ?? "UNKNOWN");
|
||||
e.Add("ErrorMessage", checkResult.ErrorMessage ?? "No details available");
|
||||
e.Add("CheckedAt", checkResult.CheckedAt.ToString("O", CultureInfo.InvariantCulture));
|
||||
if (checkResult.HttpStatusCode.HasValue)
|
||||
{
|
||||
e.Add("HttpStatusCode", checkResult.HttpStatusCode.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
})
|
||||
.WithCauses(checkResult.PossibleReasons.ToArray())
|
||||
.WithRemediation(r => BuildRemediation(r, checkResult))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build();
|
||||
}
|
||||
|
||||
// Failed status
|
||||
return result
|
||||
.Fail($"{_displayName} connectivity failed: {checkResult.ErrorMessage}")
|
||||
.WithEvidence("Connectivity check", e =>
|
||||
{
|
||||
e.Add("SourceId", _sourceId);
|
||||
e.Add("DisplayName", _displayName);
|
||||
e.Add("Status", checkResult.Status.ToString());
|
||||
e.Add("ErrorCode", checkResult.ErrorCode ?? "UNKNOWN");
|
||||
e.Add("ErrorMessage", checkResult.ErrorMessage ?? "No details available");
|
||||
e.Add("CheckedAt", checkResult.CheckedAt.ToString("O", CultureInfo.InvariantCulture));
|
||||
e.Add("HealthCheckEndpoint", source.HealthCheckEndpoint);
|
||||
if (checkResult.HttpStatusCode.HasValue)
|
||||
{
|
||||
e.Add("HttpStatusCode", checkResult.HttpStatusCode.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
if (checkResult.Latency.HasValue)
|
||||
{
|
||||
e.Add("LatencyMs", checkResult.Latency.Value.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture));
|
||||
}
|
||||
})
|
||||
.WithCauses(checkResult.PossibleReasons.ToArray())
|
||||
.WithRemediation(r => BuildRemediation(r, checkResult))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static void BuildRemediation(RemediationBuilder builder, SourceConnectivityResult checkResult)
|
||||
{
|
||||
foreach (var step in checkResult.RemediationSteps)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(step.Command))
|
||||
{
|
||||
var commandType = MapCommandType(step.CommandType);
|
||||
builder.AddStep(step.Order, step.Description, step.Command, commandType);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AddManualStep(step.Order, step.Description, step.Description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps Concelier CommandType to Doctor CommandType.
|
||||
/// </summary>
|
||||
private static Doctor.Models.CommandType MapCommandType(Concelier.Core.Sources.CommandType sourceType)
|
||||
{
|
||||
return sourceType switch
|
||||
{
|
||||
Concelier.Core.Sources.CommandType.Bash => Doctor.Models.CommandType.Shell,
|
||||
Concelier.Core.Sources.CommandType.PowerShell => Doctor.Models.CommandType.Shell,
|
||||
Concelier.Core.Sources.CommandType.StellaCli => Doctor.Models.CommandType.Shell,
|
||||
Concelier.Core.Sources.CommandType.Url => Doctor.Models.CommandType.Api,
|
||||
Concelier.Core.Sources.CommandType.EnvVar => Doctor.Models.CommandType.Manual,
|
||||
_ => Doctor.Models.CommandType.Shell
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SourceModeConfiguredCheck.cs
|
||||
// Sprint: SPRINT_20260114_SOURCES_SETUP
|
||||
// Task: 12.1 - Sources Doctor Plugin
|
||||
// Description: Check that source mode is properly configured
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Concelier.Core.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Sources.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the advisory source mode is properly configured.
|
||||
/// </summary>
|
||||
public sealed class SourceModeConfiguredCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.sources.mode.configured";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Source Mode Configuration";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Verifies that the advisory source mode (upstream/mirror/hybrid) is properly configured";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["configuration", "sources"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.sources", DoctorCategory.Data.ToString());
|
||||
|
||||
var sourcesConfig = context.Configuration
|
||||
.GetSection("sources")
|
||||
.Get<SourcesConfiguration>();
|
||||
|
||||
if (sourcesConfig is null)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn("Sources configuration section not found")
|
||||
.WithEvidence("Configuration", e =>
|
||||
{
|
||||
e.Add("ConfigSection", "sources");
|
||||
e.Add("Status", "Missing");
|
||||
})
|
||||
.WithCauses(
|
||||
"Configuration file missing 'sources' section",
|
||||
"Configuration not loaded properly")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Add sources section to configuration", "Add 'sources:' section to appsettings.json or environment-specific config")
|
||||
.AddShellStep(2, "Run setup wizard", "stella setup --step sources"))
|
||||
.WithVerification("stella doctor --check check.sources.mode.configured")
|
||||
.Build());
|
||||
}
|
||||
|
||||
// Check mode is valid
|
||||
var modeStr = sourcesConfig.Mode.ToString();
|
||||
|
||||
// Check if at least one source type is configured
|
||||
var hasUpstream = sourcesConfig.Mode is SourceMode.Direct or SourceMode.Hybrid;
|
||||
var hasMirror = sourcesConfig.Mode is SourceMode.Mirror or SourceMode.Hybrid;
|
||||
|
||||
if (sourcesConfig.Mode == SourceMode.Mirror && sourcesConfig.MirrorServer is null)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn("Mirror mode configured but mirror server settings missing")
|
||||
.WithEvidence("Configuration", e =>
|
||||
{
|
||||
e.Add("Mode", modeStr);
|
||||
e.Add("MirrorServerConfigured", "false");
|
||||
})
|
||||
.WithCauses(
|
||||
"Mirror server configuration section missing",
|
||||
"Mirror server URL not specified")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Configure mirror server", "Add 'sources:mirrorServer' section with URL and authentication settings")
|
||||
.AddShellStep(2, "Run setup wizard", "stella setup --step sources"))
|
||||
.WithVerification("stella doctor --check check.sources.mode.configured")
|
||||
.Build());
|
||||
}
|
||||
|
||||
// Count enabled sources
|
||||
var enabledCount = sourcesConfig.Sources?.Count(s => s.Value.Enabled) ?? 0;
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass($"Source mode '{modeStr}' configured with {enabledCount} enabled source(s)")
|
||||
.WithEvidence("Source configuration", e =>
|
||||
{
|
||||
e.Add("Mode", modeStr);
|
||||
e.Add("EnabledSources", enabledCount.ToString());
|
||||
e.Add("HasUpstreamSources", hasUpstream.ToString());
|
||||
e.Add("HasMirrorSources", hasMirror.ToString());
|
||||
e.Add("AutoEnableHealthy", sourcesConfig.AutoEnableHealthySources.ToString());
|
||||
if (sourcesConfig.MirrorServer is not null)
|
||||
{
|
||||
e.Add("MirrorServerEnabled", sourcesConfig.MirrorServer.Enabled.ToString());
|
||||
}
|
||||
})
|
||||
.WithVerification("stella doctor --check check.sources.mode.configured")
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SourcesPluginExtensions.cs
|
||||
// Sprint: SPRINT_20260114_SOURCES_SETUP
|
||||
// Task: 12.1 - Sources Doctor Plugin
|
||||
// Description: DI extension for registering Sources Doctor Plugin
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Sources.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering the Sources Doctor plugin.
|
||||
/// </summary>
|
||||
public static class SourcesPluginExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the Doctor Sources plugin to the service collection.
|
||||
/// This plugin provides connectivity checks for advisory data sources.
|
||||
/// </summary>
|
||||
/// <param name="services">Service collection.</param>
|
||||
/// <returns>Service collection for chaining.</returns>
|
||||
public static IServiceCollection AddDoctorSourcesPlugin(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDoctorPlugin, SourcesPlugin>());
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SourcesPlugin.cs
|
||||
// Sprint: SPRINT_20260114_SOURCES_SETUP
|
||||
// Task: 12.1 - Sources Doctor Plugin
|
||||
// Description: Doctor plugin providing advisory source connectivity diagnostics
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Concelier.Core.Sources;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Sources.Checks;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Sources;
|
||||
|
||||
/// <summary>
|
||||
/// Doctor plugin for advisory data source diagnostics.
|
||||
/// Provides connectivity checks for all configured CVE/advisory data sources.
|
||||
/// </summary>
|
||||
public sealed class SourcesPlugin : IDoctorPlugin
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string PluginId => "stellaops.doctor.sources";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName => "Advisory Sources";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorCategory Category => DoctorCategory.Data;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version Version => new(1, 0, 0);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version MinEngineVersion => new(1, 0, 0);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAvailable(IServiceProvider services)
|
||||
{
|
||||
// Plugin is available if ISourceRegistry is registered
|
||||
return services.GetService<ISourceRegistry>() is not null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<IDoctorCheck> GetChecks(DoctorPluginContext context)
|
||||
{
|
||||
var registry = context.Services.GetService<ISourceRegistry>();
|
||||
if (registry is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var checks = new List<IDoctorCheck>
|
||||
{
|
||||
// Overall source mode configuration check
|
||||
new SourceModeConfiguredCheck(),
|
||||
|
||||
// Mirror server checks
|
||||
new MirrorServerAuthCheck(),
|
||||
new MirrorServerRateLimitCheck()
|
||||
};
|
||||
|
||||
// Generate dynamic checks for each registered source
|
||||
foreach (var source in registry.GetAllSources())
|
||||
{
|
||||
checks.Add(new SourceConnectivityCheck(source.Id, source.DisplayName));
|
||||
}
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
// No initialization required
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<RootNamespace>StellaOps.Doctor.Plugins.Sources</RootNamespace>
|
||||
<Description>Doctor plugin for advisory data source connectivity diagnostics</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Doctor\StellaOps.Doctor.csproj" />
|
||||
<ProjectReference Include="..\..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -8,3 +8,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0091-M | DONE | Revalidated 2026-01-08; maintainability audit for StellaOps.Interop. |
|
||||
| AUDIT-0091-T | DONE | Revalidated 2026-01-08; test coverage audit for StellaOps.Interop. |
|
||||
| AUDIT-0091-A | TODO | Pending approval (revalidated 2026-01-08). |
|
||||
| AUDIT-TESTGAP-CORELIB-INTEROP-0001 | DONE | Added interop ToolManager unit tests + test wiring (2026-01-13). |
|
||||
|
||||
@@ -500,6 +500,23 @@ internal sealed class MockReachGraphAdapter : IReachGraphAdapter
|
||||
});
|
||||
}
|
||||
|
||||
public Task<bool> HasGraphAsync(string artifactDigest, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task<ReachGraphMetadata?> GetMetadataAsync(string artifactDigest, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return Task.FromResult<ReachGraphMetadata?>(new ReachGraphMetadata
|
||||
{
|
||||
ArtifactDigest = artifactDigest,
|
||||
GeneratedAt = _timeProvider.GetUtcNow(),
|
||||
SymbolCount = 100
|
||||
});
|
||||
}
|
||||
|
||||
private static string MakeKey(SymbolRef symbol, string artifactDigest)
|
||||
=> $"{symbol.Purl}:{symbol.Symbol}:{artifactDigest}";
|
||||
}
|
||||
@@ -569,6 +586,24 @@ internal sealed class MockSignalsAdapter : ISignalsAdapter
|
||||
});
|
||||
}
|
||||
|
||||
public Task<bool> HasFactsAsync(string artifactDigest, string? tenantId, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task<SignalsMetadata?> GetMetadataAsync(string artifactDigest, string? tenantId, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return Task.FromResult<SignalsMetadata?>(new SignalsMetadata
|
||||
{
|
||||
ArtifactDigest = artifactDigest,
|
||||
FirstObservation = _timeProvider.GetUtcNow().AddDays(-30),
|
||||
LastObservation = _timeProvider.GetUtcNow(),
|
||||
TotalObservations = 1000
|
||||
});
|
||||
}
|
||||
|
||||
private static string MakeKey(SymbolRef symbol, string artifactDigest)
|
||||
=> $"{symbol.Purl}:{symbol.Symbol}:{artifactDigest}";
|
||||
}
|
||||
|
||||
41
src/__Libraries/StellaOps.Reachability.Core/AGENTS.md
Normal file
41
src/__Libraries/StellaOps.Reachability.Core/AGENTS.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# StellaOps.Reachability.Core
|
||||
|
||||
## Module Charter
|
||||
The Reachability.Core library defines canonical identifiers and deterministic hashing helpers used by reachability analysis and evidence pipelines.
|
||||
|
||||
### Scope
|
||||
- Symbol normalization and canonical IDs
|
||||
- Node hash and path hash helpers
|
||||
- Deterministic serialization support
|
||||
|
||||
### Out of Scope
|
||||
- Call graph extraction (Scanner)
|
||||
- Runtime signal collection (Signals)
|
||||
- DSSE signing and transparency submission (Attestor)
|
||||
- Policy evaluation and gates (Policy)
|
||||
|
||||
## Roles
|
||||
- Backend developer (.NET)
|
||||
- QA automation engineer
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/reach-graph/architecture.md
|
||||
- docs/modules/scanner/architecture.md
|
||||
- docs/modules/signals/architecture.md
|
||||
- docs/contracts/witness-v1.md
|
||||
- docs/architecture/EVIDENCE_PIPELINE_ARCHITECTURE.md
|
||||
|
||||
## Working Rules
|
||||
- Determinism first: invariant culture, stable ordering, UTC timestamps.
|
||||
- No direct `DateTime.UtcNow`, `Guid.NewGuid()`, or `Random.Shared` in production code.
|
||||
- Hashing and canonical JSON must use shared helpers; do not reimplement DSSE PAE.
|
||||
- ASCII-only output in logs and comments.
|
||||
- Tests live in `src/__Libraries/__Tests/StellaOps.Reachability.Core.Tests` and must cover determinism and edge cases.
|
||||
|
||||
## Allowed Shared Dependencies
|
||||
- `StellaOps.Cryptography`
|
||||
- `StellaOps.Attestation`
|
||||
|
||||
@@ -91,6 +91,11 @@ public sealed class Spdx3Document
|
||||
/// </summary>
|
||||
public ImmutableHashSet<Spdx3ProfileIdentifier> Profiles { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all creation info objects in the document.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<Spdx3CreationInfo> CreationInfos => _creationInfoById.Values;
|
||||
|
||||
/// <summary>
|
||||
/// Gets an element by its SPDX ID.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
using StellaOps.DistroIntel;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.DistroIntel.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for DerivativeConfidence enum.
|
||||
/// </summary>
|
||||
public sealed class DerivativeConfidenceTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(DerivativeConfidence.High)]
|
||||
[InlineData(DerivativeConfidence.Medium)]
|
||||
public void DerivativeConfidence_AllValues_AreDefined(DerivativeConfidence confidence)
|
||||
{
|
||||
Assert.True(Enum.IsDefined(confidence));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DerivativeConfidence_AllValues_AreCounted()
|
||||
{
|
||||
var values = Enum.GetValues<DerivativeConfidence>();
|
||||
Assert.Equal(2, values.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for DistroDerivative record.
|
||||
/// </summary>
|
||||
public sealed class DistroDerivativeTests
|
||||
{
|
||||
[Fact]
|
||||
public void DistroDerivative_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var derivative = new DistroDerivative("rhel", "almalinux", 9, DerivativeConfidence.High);
|
||||
|
||||
Assert.Equal("rhel", derivative.CanonicalDistro);
|
||||
Assert.Equal("almalinux", derivative.DerivativeDistro);
|
||||
Assert.Equal(9, derivative.MajorRelease);
|
||||
Assert.Equal(DerivativeConfidence.High, derivative.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DistroDerivative_RecordEquality_WorksCorrectly()
|
||||
{
|
||||
var d1 = new DistroDerivative("rhel", "rocky", 9, DerivativeConfidence.High);
|
||||
var d2 = new DistroDerivative("rhel", "rocky", 9, DerivativeConfidence.High);
|
||||
|
||||
Assert.Equal(d1, d2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DistroDerivative_DifferentReleases_AreNotEqual()
|
||||
{
|
||||
var d1 = new DistroDerivative("rhel", "rocky", 8, DerivativeConfidence.High);
|
||||
var d2 = new DistroDerivative("rhel", "rocky", 9, DerivativeConfidence.High);
|
||||
|
||||
Assert.NotEqual(d1, d2);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for DistroMappings static class.
|
||||
/// </summary>
|
||||
public sealed class DistroMappingsTests
|
||||
{
|
||||
[Fact]
|
||||
public void DistroMappings_Derivatives_ContainsKnownMappings()
|
||||
{
|
||||
Assert.NotEmpty(DistroMappings.Derivatives);
|
||||
Assert.True(DistroMappings.Derivatives.Length >= 10);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("rhel", 8)]
|
||||
[InlineData("rhel", 9)]
|
||||
[InlineData("debian", 11)]
|
||||
[InlineData("debian", 12)]
|
||||
[InlineData("ubuntu", 20)]
|
||||
[InlineData("ubuntu", 22)]
|
||||
public void FindDerivativesFor_KnownCanonical_ReturnsDerivatives(string canonical, int release)
|
||||
{
|
||||
var derivatives = DistroMappings.FindDerivativesFor(canonical, release).ToList();
|
||||
|
||||
Assert.NotEmpty(derivatives);
|
||||
Assert.All(derivatives, d => Assert.Equal(canonical, d.CanonicalDistro));
|
||||
Assert.All(derivatives, d => Assert.Equal(release, d.MajorRelease));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindDerivativesFor_UnknownDistro_ReturnsEmpty()
|
||||
{
|
||||
var derivatives = DistroMappings.FindDerivativesFor("unknowndistro", 1).ToList();
|
||||
|
||||
Assert.Empty(derivatives);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindDerivativesFor_ResultsOrderedByConfidence()
|
||||
{
|
||||
var derivatives = DistroMappings.FindDerivativesFor("rhel", 9).ToList();
|
||||
|
||||
Assert.NotEmpty(derivatives);
|
||||
// All RHEL derivatives should be High confidence
|
||||
Assert.All(derivatives, d => Assert.Equal(DerivativeConfidence.High, d.Confidence));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("almalinux", 9, "rhel")]
|
||||
[InlineData("rocky", 9, "rhel")]
|
||||
[InlineData("centos", 7, "rhel")]
|
||||
[InlineData("oracle", 8, "rhel")]
|
||||
public void FindCanonicalFor_KnownDerivative_ReturnsCanonical(string derivative, int release, string expectedCanonical)
|
||||
{
|
||||
var canonical = DistroMappings.FindCanonicalFor(derivative, release);
|
||||
|
||||
Assert.NotNull(canonical);
|
||||
Assert.Equal(expectedCanonical, canonical.CanonicalDistro);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindCanonicalFor_UnknownDerivative_ReturnsNull()
|
||||
{
|
||||
var canonical = DistroMappings.FindCanonicalFor("unknowndistro", 1);
|
||||
|
||||
Assert.Null(canonical);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(DerivativeConfidence.High, 0.95)]
|
||||
[InlineData(DerivativeConfidence.Medium, 0.80)]
|
||||
public void GetConfidenceMultiplier_ReturnsExpectedValues(DerivativeConfidence confidence, decimal expected)
|
||||
{
|
||||
var multiplier = DistroMappings.GetConfidenceMultiplier(confidence);
|
||||
|
||||
Assert.Equal(expected, multiplier);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("rhel", true)]
|
||||
[InlineData("debian", true)]
|
||||
[InlineData("ubuntu", true)]
|
||||
[InlineData("sles", true)]
|
||||
[InlineData("alpine", true)]
|
||||
[InlineData("almalinux", false)]
|
||||
[InlineData("rocky", false)]
|
||||
[InlineData("linuxmint", false)]
|
||||
public void IsCanonicalDistro_ReturnsCorrectResult(string distro, bool expected)
|
||||
{
|
||||
var result = DistroMappings.IsCanonicalDistro(distro);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("redhat", "rhel")]
|
||||
[InlineData("red hat", "rhel")]
|
||||
[InlineData("red-hat", "rhel")]
|
||||
[InlineData("RHEL", "rhel")]
|
||||
[InlineData("alma", "almalinux")]
|
||||
[InlineData("almalinux-os", "almalinux")]
|
||||
[InlineData("rockylinux", "rocky")]
|
||||
[InlineData("rocky-linux", "rocky")]
|
||||
[InlineData("oracle linux", "oracle")]
|
||||
[InlineData("oraclelinux", "oracle")]
|
||||
[InlineData("opensuse", "opensuse-leap")]
|
||||
[InlineData("mint", "linuxmint")]
|
||||
[InlineData("popos", "pop")]
|
||||
[InlineData("pop_os", "pop")]
|
||||
[InlineData("debian", "debian")]
|
||||
[InlineData("ubuntu", "ubuntu")]
|
||||
public void NormalizeDistroName_ReturnsCanonicalForm(string input, string expected)
|
||||
{
|
||||
var result = DistroMappings.NormalizeDistroName(input);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindDerivativesFor_CaseInsensitive()
|
||||
{
|
||||
var lower = DistroMappings.FindDerivativesFor("rhel", 9).ToList();
|
||||
var upper = DistroMappings.FindDerivativesFor("RHEL", 9).ToList();
|
||||
var mixed = DistroMappings.FindDerivativesFor("RhEl", 9).ToList();
|
||||
|
||||
Assert.Equal(lower.Count, upper.Count);
|
||||
Assert.Equal(lower.Count, mixed.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindCanonicalFor_CaseInsensitive()
|
||||
{
|
||||
var lower = DistroMappings.FindCanonicalFor("almalinux", 9);
|
||||
var upper = DistroMappings.FindCanonicalFor("ALMALINUX", 9);
|
||||
var mixed = DistroMappings.FindCanonicalFor("AlmaLinux", 9);
|
||||
|
||||
Assert.NotNull(lower);
|
||||
Assert.NotNull(upper);
|
||||
Assert.NotNull(mixed);
|
||||
Assert.Equal(lower, upper);
|
||||
Assert.Equal(lower, mixed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<UseXunitV3>true</UseXunitV3>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.DistroIntel\StellaOps.DistroIntel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||
"diagnosticMessages": true,
|
||||
"parallelizeAssembly": true,
|
||||
"parallelizeTestCollections": true,
|
||||
"maxParallelThreads": -1
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Authority.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AuthorityPluginTests
|
||||
{
|
||||
private readonly AuthorityPlugin _plugin = new();
|
||||
|
||||
[Fact]
|
||||
public void PluginId_ReturnsExpectedValue()
|
||||
{
|
||||
_plugin.PluginId.Should().Be("stellaops.doctor.authority");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayName_ReturnsExpectedValue()
|
||||
{
|
||||
_plugin.DisplayName.Should().Be("Authority");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Category_ReturnsAuthority()
|
||||
{
|
||||
_plugin.Category.Should().Be(DoctorCategory.Authority);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChecks_ReturnsAllExpectedChecks()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext();
|
||||
|
||||
// Act
|
||||
var checks = _plugin.GetChecks(context);
|
||||
|
||||
// Assert
|
||||
checks.Should().HaveCount(5);
|
||||
checks.Select(c => c.CheckId).Should().BeEquivalentTo(new[]
|
||||
{
|
||||
"check.authority.plugin.configured",
|
||||
"check.authority.plugin.connectivity",
|
||||
"check.authority.bootstrap.exists",
|
||||
"check.users.superuser.exists",
|
||||
"check.users.password.policy"
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChecks_AllChecksHaveUniqueIds()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext();
|
||||
|
||||
// Act
|
||||
var checks = _plugin.GetChecks(context);
|
||||
|
||||
// Assert
|
||||
var checkIds = checks.Select(c => c.CheckId);
|
||||
checkIds.Should().OnlyHaveUniqueItems();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChecks_AllChecksHaveDescriptions()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext();
|
||||
|
||||
// Act
|
||||
var checks = _plugin.GetChecks(context);
|
||||
|
||||
// Assert
|
||||
foreach (var check in checks)
|
||||
{
|
||||
check.Description.Should().NotBeNullOrWhiteSpace(
|
||||
$"Check {check.CheckId} should have a description");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChecks_AllChecksHaveTags()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext();
|
||||
|
||||
// Act
|
||||
var checks = _plugin.GetChecks(context);
|
||||
|
||||
// Assert
|
||||
foreach (var check in checks)
|
||||
{
|
||||
check.Tags.Should().NotBeEmpty(
|
||||
$"Check {check.CheckId} should have at least one tag");
|
||||
}
|
||||
}
|
||||
|
||||
private static DoctorPluginContext CreateContext()
|
||||
{
|
||||
var config = new ConfigurationBuilder().Build();
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = new ServiceCollection().BuildServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Authority.Checks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Authority.Tests.Checks;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AuthorityPluginConfigurationCheckTests
|
||||
{
|
||||
private readonly AuthorityPluginConfigurationCheck _check = new();
|
||||
|
||||
[Fact]
|
||||
public void CheckId_ReturnsExpectedValue()
|
||||
{
|
||||
_check.CheckId.Should().Be("check.authority.plugin.configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_ReturnsExpectedValue()
|
||||
{
|
||||
_check.Name.Should().Be("Authority Plugin Configuration");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultSeverity_IsCritical()
|
||||
{
|
||||
_check.DefaultSeverity.Should().Be(DoctorSeverity.Fail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tags_ContainsExpectedValues()
|
||||
{
|
||||
_check.Tags.Should().Contain("authority");
|
||||
_check.Tags.Should().Contain("authentication");
|
||||
_check.Tags.Should().Contain("security");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsTrue()
|
||||
{
|
||||
var context = CreateContext(new Dictionary<string, string?>());
|
||||
_check.CanRun(context).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Fails_WhenNoPluginsConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>());
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Fail);
|
||||
result.Diagnosis.Should().Contain("No authentication plugins configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenStandardPluginEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("1 authentication plugin(s) configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenStandardSectionExists()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:PasswordPolicy:MinLength"] = "12"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenLdapPluginConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Ldap:Enabled"] = "true",
|
||||
["Authority:Plugins:Ldap:Server"] = "ldap://ldap.example.com"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("1 authentication plugin(s) configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Warns_WhenLdapEnabledButServerMissing()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Ldap:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Fail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenOidcPluginConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Oidc:Enabled"] = "true",
|
||||
["Authority:Plugins:Oidc:Authority"] = "https://login.example.com"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Warns_WhenOidcEnabledButAuthorityMissing()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Oidc:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Fail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenSamlPluginEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Saml:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ReportsMultiplePlugins()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:Plugins:Ldap:Enabled"] = "true",
|
||||
["Authority:Plugins:Ldap:Server"] = "ldap://ldap.example.com"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("2 authentication plugin(s) configured");
|
||||
}
|
||||
|
||||
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configValues)
|
||||
.Build();
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = new ServiceCollection().BuildServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Authority.Checks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Authority.Tests.Checks;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AuthorityPluginConnectivityCheckTests
|
||||
{
|
||||
private readonly AuthorityPluginConnectivityCheck _check = new();
|
||||
|
||||
[Fact]
|
||||
public void CheckId_ReturnsExpectedValue()
|
||||
{
|
||||
_check.CheckId.Should().Be("check.authority.plugin.connectivity");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_ReturnsExpectedValue()
|
||||
{
|
||||
_check.Name.Should().Be("Authority Backend Connectivity");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultSeverity_IsCritical()
|
||||
{
|
||||
_check.DefaultSeverity.Should().Be(DoctorSeverity.Fail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tags_ContainsExpectedValues()
|
||||
{
|
||||
_check.Tags.Should().Contain("authority");
|
||||
_check.Tags.Should().Contain("connectivity");
|
||||
_check.Tags.Should().Contain("ldap");
|
||||
_check.Tags.Should().Contain("database");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsFalse_WhenNoPluginsConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>());
|
||||
|
||||
// Act & Assert
|
||||
_check.CanRun(context).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsTrue_WhenStandardPluginSectionExists()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
_check.CanRun(context).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsTrue_WhenLdapPluginSectionExists()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Ldap:Server"] = "ldap://ldap.example.com"
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
_check.CanRun(context).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Skips_WhenNoBackendsToTest()
|
||||
{
|
||||
// Arrange - Sections exist but no plugins enabled
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:SomeSetting"] = "value",
|
||||
["Authority:Plugins:Standard:Enabled"] = "false"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Skip);
|
||||
result.Diagnosis.Should().Contain("No authentication backends configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Fails_WhenDatabaseConnectionStringMissing()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Fail);
|
||||
result.Diagnosis.Should().Contain("unreachable");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenDatabaseConnectionStringConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["ConnectionStrings:Authority"] = "Host=localhost;Database=authority"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("reachable");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_UsesDefaultConnectionString_WhenAuthorityConnectionMissing()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["ConnectionStrings:Default"] = "Host=localhost;Database=stellaops"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_TestsBothBackends_WhenBothEnabled()
|
||||
{
|
||||
// Arrange - Standard with DB and LDAP both enabled
|
||||
// Note: LDAP will fail in test because we can't actually connect
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["ConnectionStrings:Authority"] = "Host=localhost;Database=authority",
|
||||
["Authority:Plugins:Ldap:Enabled"] = "true",
|
||||
["Authority:Plugins:Ldap:Server"] = "ldap://127.0.0.1:3899"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert - LDAP will fail (can't connect in test), so overall should fail
|
||||
result.Severity.Should().Be(DoctorSeverity.Fail);
|
||||
result.Diagnosis.Should().Contain("unreachable");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_SkipsLdapTest_WhenLdapEnabledButNoServer()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["ConnectionStrings:Authority"] = "Host=localhost;Database=authority",
|
||||
["Authority:Plugins:Ldap:Enabled"] = "true"
|
||||
// No Ldap:Server configured
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert - Should only test Standard (DB), which passes
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("1 backend(s) reachable");
|
||||
}
|
||||
|
||||
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configValues)
|
||||
.Build();
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = new ServiceCollection().BuildServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Authority.Checks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Authority.Tests.Checks;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class BootstrapUserExistsCheckTests
|
||||
{
|
||||
private readonly BootstrapUserExistsCheck _check = new();
|
||||
|
||||
[Fact]
|
||||
public void CheckId_ReturnsExpectedValue()
|
||||
{
|
||||
_check.CheckId.Should().Be("check.authority.bootstrap.exists");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_ReturnsExpectedValue()
|
||||
{
|
||||
_check.Name.Should().Be("Bootstrap User Exists");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultSeverity_IsCritical()
|
||||
{
|
||||
_check.DefaultSeverity.Should().Be(DoctorSeverity.Fail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tags_ContainsExpectedValues()
|
||||
{
|
||||
_check.Tags.Should().Contain("authority");
|
||||
_check.Tags.Should().Contain("user");
|
||||
_check.Tags.Should().Contain("bootstrap");
|
||||
_check.Tags.Should().Contain("admin");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsFalse_WhenStandardPluginNotEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>());
|
||||
|
||||
// Act & Assert
|
||||
_check.CanRun(context).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsTrue_WhenStandardPluginEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
_check.CanRun(context).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Fails_WhenAutoBootstrapDisabledAndNoConfig()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:Bootstrap:Enabled"] = "false"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Fail);
|
||||
result.Diagnosis.Should().Contain("No bootstrap user configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Info_WhenAutoBootstrapEnabledButNoConfig()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:Bootstrap:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Info);
|
||||
result.Diagnosis.Should().Contain("auto-created");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Warns_WhenUsernameMissing()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:Bootstrap:Email"] = "admin@example.com"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
result.Diagnosis.Should().Contain("incomplete");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Warns_WhenEmailMissing()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:Bootstrap:Username"] = "admin"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
result.Diagnosis.Should().Contain("incomplete");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenFullyConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:Bootstrap:Username"] = "admin",
|
||||
["Authority:Bootstrap:Email"] = "admin@example.com"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("properly configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ReadsFromAlternativeConfigPath()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:Plugins:Standard:Bootstrap:Username"] = "admin",
|
||||
["Authority:Plugins:Standard:Bootstrap:Email"] = "admin@example.com"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
}
|
||||
|
||||
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configValues)
|
||||
.Build();
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = new ServiceCollection().BuildServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Authority.Checks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Authority.Tests.Checks;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class SuperUserExistsCheckTests
|
||||
{
|
||||
private readonly SuperUserExistsCheck _check = new();
|
||||
|
||||
[Fact]
|
||||
public void CheckId_ReturnsExpectedValue()
|
||||
{
|
||||
_check.CheckId.Should().Be("check.users.superuser.exists");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_ReturnsExpectedValue()
|
||||
{
|
||||
_check.Name.Should().Be("Super User Exists");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultSeverity_IsCritical()
|
||||
{
|
||||
_check.DefaultSeverity.Should().Be(DoctorSeverity.Fail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tags_ContainsExpectedValues()
|
||||
{
|
||||
_check.Tags.Should().Contain("authority");
|
||||
_check.Tags.Should().Contain("user");
|
||||
_check.Tags.Should().Contain("admin");
|
||||
_check.Tags.Should().Contain("superuser");
|
||||
_check.Tags.Should().Contain("security");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsFalse_WhenStandardPluginNotEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>());
|
||||
|
||||
// Act & Assert
|
||||
_check.CanRun(context).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsTrue_WhenStandardPluginEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
_check.CanRun(context).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Fails_WhenNoAdminsAndAutoBootstrapDisabled()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:Bootstrap:Enabled"] = "false"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Fail);
|
||||
result.Diagnosis.Should().Contain("No administrator users configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Info_WhenNoAdminsButAutoBootstrapEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:Bootstrap:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Info);
|
||||
result.Diagnosis.Should().Contain("auto-bootstrap");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenBootstrapUsernameConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:Bootstrap:Username"] = "admin"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("1 administrator(s) configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenAdministratorsConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:Users:Administrators:0"] = "admin1",
|
||||
["Authority:Users:Administrators:1"] = "admin2"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("2 administrator(s) configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenAdminRolesConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:Roles:Administrators:0"] = "alice",
|
||||
["Authority:Roles:Administrators:1"] = "bob"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("2 administrator(s) configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_DeduplicatesAdminUsers()
|
||||
{
|
||||
// Arrange - Same user in both bootstrap and admin list
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:Bootstrap:Username"] = "admin",
|
||||
["Authority:Users:Administrators:0"] = "admin"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("1 administrator(s) configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_DefaultsAutoBootstrapToTrue()
|
||||
{
|
||||
// Arrange - No explicit Bootstrap:Enabled setting
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
// Should return Info because auto-bootstrap defaults to true
|
||||
result.Severity.Should().Be(DoctorSeverity.Info);
|
||||
}
|
||||
|
||||
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configValues)
|
||||
.Build();
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = new ServiceCollection().BuildServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Authority.Checks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Authority.Tests.Checks;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class UserPasswordPolicyCheckTests
|
||||
{
|
||||
private readonly UserPasswordPolicyCheck _check = new();
|
||||
|
||||
[Fact]
|
||||
public void CheckId_ReturnsExpectedValue()
|
||||
{
|
||||
_check.CheckId.Should().Be("check.users.password.policy");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_ReturnsExpectedValue()
|
||||
{
|
||||
_check.Name.Should().Be("Password Policy");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultSeverity_IsWarn()
|
||||
{
|
||||
_check.DefaultSeverity.Should().Be(DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tags_ContainsExpectedValues()
|
||||
{
|
||||
_check.Tags.Should().Contain("authority");
|
||||
_check.Tags.Should().Contain("password");
|
||||
_check.Tags.Should().Contain("policy");
|
||||
_check.Tags.Should().Contain("security");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsFalse_WhenStandardPluginNotEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>());
|
||||
|
||||
// Act & Assert
|
||||
_check.CanRun(context).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsTrue_WhenStandardPluginEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
_check.CanRun(context).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Fails_WhenMinLengthBelowAbsoluteMinimum()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:PasswordPolicy:MinLength"] = "6"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Fail);
|
||||
result.Diagnosis.Should().Contain("violation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Warns_WhenMinLengthBelowRecommended()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:PasswordPolicy:MinLength"] = "10"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
result.Diagnosis.Should().Contain("recommendation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Warns_WhenInsufficientComplexityRequirements()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:PasswordPolicy:MinLength"] = "12",
|
||||
["Authority:PasswordPolicy:RequireUppercase"] = "true",
|
||||
["Authority:PasswordPolicy:RequireLowercase"] = "true",
|
||||
["Authority:PasswordPolicy:RequireDigit"] = "false",
|
||||
["Authority:PasswordPolicy:RequireSpecialCharacter"] = "false"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
result.Diagnosis.Should().Contain("recommendation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Warns_WhenMaxAgeVeryShort()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:PasswordPolicy:MinLength"] = "12",
|
||||
["Authority:PasswordPolicy:MaxAgeDays"] = "14"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
result.Diagnosis.Should().Contain("recommendation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Warns_WhenPreventReuseCountLow()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:PasswordPolicy:MinLength"] = "12",
|
||||
["Authority:PasswordPolicy:PreventReuseCount"] = "2"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
result.Diagnosis.Should().Contain("recommendation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenPolicyMeetsRequirements()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true",
|
||||
["Authority:PasswordPolicy:MinLength"] = "14",
|
||||
["Authority:PasswordPolicy:RequireUppercase"] = "true",
|
||||
["Authority:PasswordPolicy:RequireLowercase"] = "true",
|
||||
["Authority:PasswordPolicy:RequireDigit"] = "true",
|
||||
["Authority:PasswordPolicy:RequireSpecialCharacter"] = "true",
|
||||
["Authority:PasswordPolicy:PreventReuseCount"] = "5"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("meets security requirements");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_UsesDefaults_WhenNoPolicyConfigured()
|
||||
{
|
||||
// Arrange - Only Standard enabled, no password policy explicitly set
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:Plugins:Standard:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
// Default MinLength is 8, which is below recommended 12, so should warn
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configValues)
|
||||
.Build();
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = new ServiceCollection().BuildServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Doctor\StellaOps.Doctor.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Doctor.Plugins.Authority\StellaOps.Doctor.Plugins.Authority.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,296 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Notify.Checks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Notify.Tests.Checks;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class NotifyChannelConfigurationCheckTests
|
||||
{
|
||||
private readonly NotifyChannelConfigurationCheck _check = new();
|
||||
|
||||
[Fact]
|
||||
public void CheckId_ReturnsExpectedValue()
|
||||
{
|
||||
_check.CheckId.Should().Be("check.notify.channel.configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_ReturnsExpectedValue()
|
||||
{
|
||||
_check.Name.Should().Be("Notification Channel Configuration");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultSeverity_IsInfo()
|
||||
{
|
||||
_check.DefaultSeverity.Should().Be(DoctorSeverity.Info);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tags_ContainsExpectedValues()
|
||||
{
|
||||
_check.Tags.Should().Contain("notify");
|
||||
_check.Tags.Should().Contain("channel");
|
||||
_check.Tags.Should().Contain("configuration");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EstimatedDuration_IsShort()
|
||||
{
|
||||
_check.EstimatedDuration.Should().BeLessThanOrEqualTo(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsTrue()
|
||||
{
|
||||
var context = CreateContext(new Dictionary<string, string?>());
|
||||
_check.CanRun(context).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ReturnsInfo_WhenNoChannelsConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>());
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Info);
|
||||
result.Diagnosis.Should().Contain("No notification channels configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenEmailChannelConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Channels:Email:SmtpHost"] = "smtp.example.com"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("1 notification channel(s) configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Warns_WhenEmailEnabledButSmtpHostMissing()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenSlackChannelConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Slack:Enabled"] = "true",
|
||||
["Notify:Channels:Slack:WebhookUrl"] = "https://hooks.slack.com/services/..."
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("1 notification channel(s) configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenSlackTokenConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Slack:Enabled"] = "true",
|
||||
["Notify:Channels:Slack:Token"] = "xoxb-123456789"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Warns_WhenSlackEnabledButNoWebhookOrToken()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Slack:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenTeamsChannelConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Teams:Enabled"] = "true",
|
||||
["Notify:Channels:Teams:WebhookUrl"] = "https://outlook.office.com/webhook/..."
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("1 notification channel(s) configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Warns_WhenTeamsEnabledButNoWebhook()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Teams:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenWebhookChannelConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Webhook:Enabled"] = "true",
|
||||
["Notify:Channels:Webhook:Endpoint"] = "https://api.example.com/webhook"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("1 notification channel(s) configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Warns_WhenWebhookEnabledButNoEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Webhook:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ReportsMultipleChannels()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Channels:Email:SmtpHost"] = "smtp.example.com",
|
||||
["Notify:Channels:Slack:Enabled"] = "true",
|
||||
["Notify:Channels:Slack:WebhookUrl"] = "https://hooks.slack.com/services/..."
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
result.Diagnosis.Should().Contain("2 notification channel(s) configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_IncludesIssues_WhenSomeChannelsMisconfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Channels:Email:SmtpHost"] = "smtp.example.com",
|
||||
["Notify:Channels:Slack:Enabled"] = "true" // Missing webhook URL
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
result.Diagnosis.Should().Contain("1 channel(s) configured");
|
||||
result.Diagnosis.Should().Contain("1 issue(s)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_DetectsChannelFromSection_WhenEnabledNotExplicit()
|
||||
{
|
||||
// Arrange - Section exists but Enabled not set (defaults to true for existing sections)
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:SmtpHost"] = "smtp.example.com",
|
||||
["Notify:Channels:Email:SmtpPort"] = "587"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Pass);
|
||||
}
|
||||
|
||||
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configValues)
|
||||
.Build();
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = new ServiceCollection().BuildServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Notify.Checks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Notify.Tests.Checks;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class NotifyChannelConnectivityCheckTests
|
||||
{
|
||||
private readonly NotifyChannelConnectivityCheck _check = new();
|
||||
|
||||
[Fact]
|
||||
public void CheckId_ReturnsExpectedValue()
|
||||
{
|
||||
_check.CheckId.Should().Be("check.notify.channel.connectivity");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_ReturnsExpectedValue()
|
||||
{
|
||||
_check.Name.Should().Be("Notification Channel Connectivity");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Description_ReturnsExpectedValue()
|
||||
{
|
||||
_check.Description.Should().Contain("connectivity");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultSeverity_IsWarn()
|
||||
{
|
||||
_check.DefaultSeverity.Should().Be(DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tags_ContainsExpectedValues()
|
||||
{
|
||||
_check.Tags.Should().Contain("notify");
|
||||
_check.Tags.Should().Contain("channel");
|
||||
_check.Tags.Should().Contain("connectivity");
|
||||
_check.Tags.Should().Contain("network");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EstimatedDuration_AllowsForNetworkTimeout()
|
||||
{
|
||||
_check.EstimatedDuration.Should().BeGreaterThanOrEqualTo(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsFalse_WhenNoChannelsConfigured()
|
||||
{
|
||||
var context = CreateContext(new Dictionary<string, string?>());
|
||||
_check.CanRun(context).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsTrue_WhenEmailChannelConfigured()
|
||||
{
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:SmtpHost"] = "smtp.example.com"
|
||||
});
|
||||
|
||||
_check.CanRun(context).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsTrue_WhenSlackChannelConfigured()
|
||||
{
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Slack:WebhookUrl"] = "https://hooks.slack.com/services/..."
|
||||
});
|
||||
|
||||
_check.CanRun(context).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsTrue_WhenTeamsChannelConfigured()
|
||||
{
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Teams:WebhookUrl"] = "https://outlook.office.com/webhook/..."
|
||||
});
|
||||
|
||||
_check.CanRun(context).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsTrue_WhenWebhookChannelConfigured()
|
||||
{
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Webhook:Endpoint"] = "https://api.example.com/webhook"
|
||||
});
|
||||
|
||||
_check.CanRun(context).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Skips_WhenNoChannelsConfiguredToTest()
|
||||
{
|
||||
// Arrange - Channels exist but are disabled
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "false"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Skip);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_TestsEmailSmtpConnectivity()
|
||||
{
|
||||
// Arrange - Use localhost which should fail quickly
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Channels:Email:SmtpHost"] = "localhost",
|
||||
["Notify:Channels:Email:SmtpPort"] = "9999" // Unlikely to be open
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert - Should warn about connectivity failure
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
result.Diagnosis.Should().Contain("unreachable");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_UsesDefaultSmtpPort_WhenNotSpecified()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Channels:Email:SmtpHost"] = "localhost"
|
||||
// No port specified - should use 587
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert - Should attempt connection (will likely fail but uses correct port)
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ReportsInvalidWebhookUrl()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Slack:Enabled"] = "true",
|
||||
["Notify:Channels:Slack:WebhookUrl"] = "not-a-valid-url"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_IncludesRemediationForFailedChannels()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Channels:Email:SmtpHost"] = "nonexistent.example.com"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Remediation.Should().NotBeNull();
|
||||
result.Remediation!.Steps.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ReportsMultipleChannelResults()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Channels:Email:SmtpHost"] = "localhost",
|
||||
["Notify:Channels:Slack:Enabled"] = "true",
|
||||
["Notify:Channels:Slack:WebhookUrl"] = "https://hooks.slack.com/test"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Evidence.Should().NotBeNull();
|
||||
result.Evidence.Description.Should().Contain("Connectivity");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_SkipsDisabledChannels()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "false",
|
||||
["Notify:Channels:Email:SmtpHost"] = "smtp.example.com",
|
||||
["Notify:Channels:Slack:Enabled"] = "true",
|
||||
["Notify:Channels:Slack:WebhookUrl"] = "https://hooks.slack.com/test"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert - Email should not be tested since it's disabled
|
||||
// But Slack should be tested
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_IncludesVerificationCommand()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Channels:Email:SmtpHost"] = "localhost"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.VerificationCommand.Should().Contain("stella doctor --check check.notify.channel.connectivity");
|
||||
}
|
||||
|
||||
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configValues)
|
||||
.Build();
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = new ServiceCollection().BuildServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Notify.Checks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Notify.Tests.Checks;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class NotifyDeliveryTestCheckTests
|
||||
{
|
||||
private readonly NotifyDeliveryTestCheck _check = new();
|
||||
|
||||
[Fact]
|
||||
public void CheckId_ReturnsExpectedValue()
|
||||
{
|
||||
_check.CheckId.Should().Be("check.notify.delivery.test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_ReturnsExpectedValue()
|
||||
{
|
||||
_check.Name.Should().Be("Notification Delivery Health");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Description_ReturnsExpectedValue()
|
||||
{
|
||||
_check.Description.Should().Contain("delivery");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultSeverity_IsWarn()
|
||||
{
|
||||
_check.DefaultSeverity.Should().Be(DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tags_ContainsExpectedValues()
|
||||
{
|
||||
_check.Tags.Should().Contain("notify");
|
||||
_check.Tags.Should().Contain("delivery");
|
||||
_check.Tags.Should().Contain("queue");
|
||||
_check.Tags.Should().Contain("health");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EstimatedDuration_IsReasonable()
|
||||
{
|
||||
_check.EstimatedDuration.Should().BeLessThanOrEqualTo(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsFalse_WhenNotifyNotConfigured()
|
||||
{
|
||||
var context = CreateContext(new Dictionary<string, string?>());
|
||||
_check.CanRun(context).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRun_ReturnsTrue_WhenNotifySectionExists()
|
||||
{
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true"
|
||||
});
|
||||
|
||||
_check.CanRun(context).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WithDefaultConfiguration()
|
||||
{
|
||||
// Arrange - Notify section exists but no specific delivery settings
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert - Should pass with defaults
|
||||
result.Severity.Should().BeOneOf(DoctorSeverity.Pass, DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Warns_WhenNoQueueTransportConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true"
|
||||
// No queue transport configured
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
if (result.Severity == DoctorSeverity.Warn && result.LikelyCauses is not null)
|
||||
{
|
||||
result.LikelyCauses.Should().Contain(c => c.Contains("queue") || c.Contains("in-memory"));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenRedisQueueConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Queue:Transport"] = "Redis",
|
||||
["Notify:Queue:Redis:ConnectionString"] = "localhost:6379"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().BeOneOf(DoctorSeverity.Pass, DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenNatsQueueConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Queue:Transport"] = "NATS",
|
||||
["Notify:Queue:Nats:Url"] = "nats://localhost:4222"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().BeOneOf(DoctorSeverity.Pass, DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Fails_WhenMaxRetriesIsZero()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Delivery:MaxRetries"] = "0"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Fail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Fails_WhenMaxRetriesIsNegative()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Delivery:MaxRetries"] = "-1"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Fail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Warns_WhenMaxRetriesIsVeryHigh()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Delivery:MaxRetries"] = "15"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Warns_WhenThrottleLimitIsVeryLow()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Throttle:Enabled"] = "true",
|
||||
["Notify:Throttle:Limit"] = "5"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().Be(DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_Passes_WhenThrottleConfiguredProperly()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Throttle:Enabled"] = "true",
|
||||
["Notify:Throttle:Limit"] = "100",
|
||||
["Notify:Throttle:Window"] = "1m"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Severity.Should().BeOneOf(DoctorSeverity.Pass, DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_IncludesEvidenceForConfiguration()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Queue:Transport"] = "Redis",
|
||||
["Notify:Delivery:MaxRetries"] = "5"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Evidence.Should().NotBeNull();
|
||||
result.Evidence.Description.Should().Contain("Delivery");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_IncludesDefaultChannel_WhenConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:DefaultChannel"] = "email"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Evidence.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_IncludesRemediation_OnFailure()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Delivery:MaxRetries"] = "0"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Remediation.Should().NotBeNull();
|
||||
result.Remediation!.Steps.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_IncludesVerificationCommand()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
if (result.Severity != DoctorSeverity.Pass)
|
||||
{
|
||||
result.VerificationCommand.Should().Contain("stella doctor --check check.notify.delivery.test");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_FallsBackToRedisConnectionString_WhenTransportNotSet()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["ConnectionStrings:Redis"] = "localhost:6379"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert - Should detect Redis from connection string
|
||||
result.Severity.Should().BeOneOf(DoctorSeverity.Pass, DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_FallsBackToNatsUrl_WhenTransportNotSet()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Nats:Url"] = "nats://localhost:4222"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert - Should detect NATS from global config
|
||||
result.Severity.Should().BeOneOf(DoctorSeverity.Pass, DoctorSeverity.Warn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ReportsDigestConfiguration()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Notify:Channels:Email:Enabled"] = "true",
|
||||
["Notify:Digest:Enabled"] = "true",
|
||||
["Notify:Digest:Interval"] = "1h"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Evidence.Should().NotBeNull();
|
||||
result.Evidence.Data.Should().ContainKey("DigestEnabled");
|
||||
}
|
||||
|
||||
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configValues)
|
||||
.Build();
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = new ServiceCollection().BuildServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Notify.Checks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Notify.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class NotifyPluginTests
|
||||
{
|
||||
private readonly NotifyPlugin _plugin = new();
|
||||
|
||||
[Fact]
|
||||
public void PluginId_ReturnsExpectedValue()
|
||||
{
|
||||
_plugin.PluginId.Should().Be("stellaops.doctor.notify");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayName_ReturnsExpectedValue()
|
||||
{
|
||||
_plugin.DisplayName.Should().Be("Notifications");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Category_ReturnsNotify()
|
||||
{
|
||||
_plugin.Category.Should().Be(DoctorCategory.Notify);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Version_ReturnsVersion1()
|
||||
{
|
||||
_plugin.Version.Should().Be(new Version(1, 0, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinEngineVersion_ReturnsVersion1()
|
||||
{
|
||||
_plugin.MinEngineVersion.Should().Be(new Version(1, 0, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsAvailable_ReturnsTrue()
|
||||
{
|
||||
var services = new ServiceCollection().BuildServiceProvider();
|
||||
_plugin.IsAvailable(services).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChecks_ReturnsThreeChecks()
|
||||
{
|
||||
var context = CreateContext(new Dictionary<string, string?>());
|
||||
var checks = _plugin.GetChecks(context);
|
||||
|
||||
checks.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChecks_ContainsChannelConfigurationCheck()
|
||||
{
|
||||
var context = CreateContext(new Dictionary<string, string?>());
|
||||
var checks = _plugin.GetChecks(context);
|
||||
|
||||
checks.Should().ContainSingle(c => c is NotifyChannelConfigurationCheck);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChecks_ContainsChannelConnectivityCheck()
|
||||
{
|
||||
var context = CreateContext(new Dictionary<string, string?>());
|
||||
var checks = _plugin.GetChecks(context);
|
||||
|
||||
checks.Should().ContainSingle(c => c is NotifyChannelConnectivityCheck);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChecks_ContainsDeliveryTestCheck()
|
||||
{
|
||||
var context = CreateContext(new Dictionary<string, string?>());
|
||||
var checks = _plugin.GetChecks(context);
|
||||
|
||||
checks.Should().ContainSingle(c => c is NotifyDeliveryTestCheck);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InitializeAsync_CompletesSuccessfully()
|
||||
{
|
||||
var context = CreateContext(new Dictionary<string, string?>());
|
||||
var act = () => _plugin.InitializeAsync(context, CancellationToken.None);
|
||||
|
||||
await act.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configValues)
|
||||
.Build();
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = new ServiceCollection().BuildServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Doctor\StellaOps.Doctor.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Doctor.Plugins.Notify\StellaOps.Doctor.Plugins.Notify.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,271 @@
|
||||
using StellaOps.Orchestrator.Schemas;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Orchestrator.Schemas.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for OrchestratorEnvelope generic record.
|
||||
/// </summary>
|
||||
public sealed class OrchestratorEnvelopeTests
|
||||
{
|
||||
[Fact]
|
||||
public void OrchestratorEnvelope_RequiredProperties_HaveDefaults()
|
||||
{
|
||||
var envelope = new OrchestratorEnvelope<string>
|
||||
{
|
||||
Payload = "test"
|
||||
};
|
||||
|
||||
Assert.Equal(Guid.Empty, envelope.EventId);
|
||||
Assert.Equal(string.Empty, envelope.Kind);
|
||||
Assert.Equal(0, envelope.Version);
|
||||
Assert.Equal(string.Empty, envelope.Tenant);
|
||||
Assert.Equal(string.Empty, envelope.Source);
|
||||
Assert.Equal(string.Empty, envelope.IdempotencyKey);
|
||||
Assert.Equal("test", envelope.Payload);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrchestratorEnvelope_WithAllProperties_ContainsValues()
|
||||
{
|
||||
var eventId = Guid.NewGuid();
|
||||
var occurredAt = DateTimeOffset.UtcNow;
|
||||
var recordedAt = occurredAt.AddSeconds(1);
|
||||
|
||||
var envelope = new OrchestratorEnvelope<string>
|
||||
{
|
||||
EventId = eventId,
|
||||
Kind = OrchestratorEventKinds.ScannerReportReady,
|
||||
Version = 1,
|
||||
Tenant = "tenant-001",
|
||||
OccurredAt = occurredAt,
|
||||
RecordedAt = recordedAt,
|
||||
Source = "scanner-service",
|
||||
IdempotencyKey = "idem-key-123",
|
||||
CorrelationId = "corr-456",
|
||||
TraceId = "trace-789",
|
||||
SpanId = "span-abc",
|
||||
Payload = "report-data"
|
||||
};
|
||||
|
||||
Assert.Equal(eventId, envelope.EventId);
|
||||
Assert.Equal(OrchestratorEventKinds.ScannerReportReady, envelope.Kind);
|
||||
Assert.Equal(1, envelope.Version);
|
||||
Assert.Equal("tenant-001", envelope.Tenant);
|
||||
Assert.Equal(occurredAt, envelope.OccurredAt);
|
||||
Assert.Equal(recordedAt, envelope.RecordedAt);
|
||||
Assert.Equal("scanner-service", envelope.Source);
|
||||
Assert.Equal("idem-key-123", envelope.IdempotencyKey);
|
||||
Assert.Equal("corr-456", envelope.CorrelationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrchestratorEnvelope_WithScope_ContainsScope()
|
||||
{
|
||||
var scope = new OrchestratorScope
|
||||
{
|
||||
Namespace = "production",
|
||||
Repo = "myapp",
|
||||
Digest = "sha256:abc123",
|
||||
Component = "api",
|
||||
Image = "myapp:v1.0.0"
|
||||
};
|
||||
|
||||
var envelope = new OrchestratorEnvelope<string>
|
||||
{
|
||||
Scope = scope,
|
||||
Payload = "test"
|
||||
};
|
||||
|
||||
Assert.NotNull(envelope.Scope);
|
||||
Assert.Equal("production", envelope.Scope.Namespace);
|
||||
Assert.Equal("myapp", envelope.Scope.Repo);
|
||||
Assert.Equal("sha256:abc123", envelope.Scope.Digest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrchestratorEnvelope_WithAttributes_ContainsAttributes()
|
||||
{
|
||||
var attributes = new Dictionary<string, string>
|
||||
{
|
||||
["region"] = "us-east-1",
|
||||
["environment"] = "staging"
|
||||
};
|
||||
|
||||
var envelope = new OrchestratorEnvelope<string>
|
||||
{
|
||||
Attributes = attributes,
|
||||
Payload = "test"
|
||||
};
|
||||
|
||||
Assert.NotNull(envelope.Attributes);
|
||||
Assert.Equal(2, envelope.Attributes.Count);
|
||||
Assert.Equal("us-east-1", envelope.Attributes["region"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrchestratorEnvelope_OptionalProperties_AreNullByDefault()
|
||||
{
|
||||
var envelope = new OrchestratorEnvelope<string>
|
||||
{
|
||||
Payload = "test"
|
||||
};
|
||||
|
||||
Assert.Null(envelope.RecordedAt);
|
||||
Assert.Null(envelope.CorrelationId);
|
||||
Assert.Null(envelope.TraceId);
|
||||
Assert.Null(envelope.SpanId);
|
||||
Assert.Null(envelope.Scope);
|
||||
Assert.Null(envelope.Attributes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrchestratorEnvelope_GenericPayload_WorksWithComplexTypes()
|
||||
{
|
||||
var payload = new ScannerReportReadyPayload
|
||||
{
|
||||
ReportId = "report-001",
|
||||
Verdict = "PASS"
|
||||
};
|
||||
|
||||
var envelope = new OrchestratorEnvelope<ScannerReportReadyPayload>
|
||||
{
|
||||
Kind = OrchestratorEventKinds.ScannerReportReady,
|
||||
Payload = payload
|
||||
};
|
||||
|
||||
Assert.Equal("report-001", envelope.Payload.ReportId);
|
||||
Assert.Equal("PASS", envelope.Payload.Verdict);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for OrchestratorScope record.
|
||||
/// </summary>
|
||||
public sealed class OrchestratorScopeTests
|
||||
{
|
||||
[Fact]
|
||||
public void OrchestratorScope_RequiredProperties_HaveDefaults()
|
||||
{
|
||||
var scope = new OrchestratorScope();
|
||||
|
||||
Assert.Null(scope.Namespace);
|
||||
Assert.Equal(string.Empty, scope.Repo);
|
||||
Assert.Equal(string.Empty, scope.Digest);
|
||||
Assert.Null(scope.Component);
|
||||
Assert.Null(scope.Image);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrchestratorScope_WithAllProperties_ContainsValues()
|
||||
{
|
||||
var scope = new OrchestratorScope
|
||||
{
|
||||
Namespace = "default",
|
||||
Repo = "registry.example.com/myapp",
|
||||
Digest = "sha256:1234567890abcdef",
|
||||
Component = "backend",
|
||||
Image = "myapp:latest"
|
||||
};
|
||||
|
||||
Assert.Equal("default", scope.Namespace);
|
||||
Assert.Equal("registry.example.com/myapp", scope.Repo);
|
||||
Assert.Equal("sha256:1234567890abcdef", scope.Digest);
|
||||
Assert.Equal("backend", scope.Component);
|
||||
Assert.Equal("myapp:latest", scope.Image);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrchestratorScope_RecordEquality_WorksCorrectly()
|
||||
{
|
||||
var scope1 = new OrchestratorScope
|
||||
{
|
||||
Repo = "myapp",
|
||||
Digest = "sha256:abc"
|
||||
};
|
||||
|
||||
var scope2 = new OrchestratorScope
|
||||
{
|
||||
Repo = "myapp",
|
||||
Digest = "sha256:abc"
|
||||
};
|
||||
|
||||
Assert.Equal(scope1, scope2);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for OrchestratorEventKinds constants.
|
||||
/// </summary>
|
||||
public sealed class OrchestratorEventKindsTests
|
||||
{
|
||||
[Fact]
|
||||
public void OrchestratorEventKinds_ScannerReportReady_HasCorrectValue()
|
||||
{
|
||||
Assert.Equal("scanner.event.report.ready", OrchestratorEventKinds.ScannerReportReady);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrchestratorEventKinds_ScannerScanCompleted_HasCorrectValue()
|
||||
{
|
||||
Assert.Equal("scanner.event.scan.completed", OrchestratorEventKinds.ScannerScanCompleted);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for ScannerReportReadyPayload record.
|
||||
/// </summary>
|
||||
public sealed class ScannerReportReadyPayloadTests
|
||||
{
|
||||
[Fact]
|
||||
public void ScannerReportReadyPayload_RequiredProperties_HaveDefaults()
|
||||
{
|
||||
var payload = new ScannerReportReadyPayload();
|
||||
|
||||
Assert.Equal(string.Empty, payload.ReportId);
|
||||
Assert.Null(payload.ScanId);
|
||||
Assert.Null(payload.ImageDigest);
|
||||
Assert.Equal(string.Empty, payload.Verdict);
|
||||
Assert.NotNull(payload.Summary);
|
||||
Assert.NotNull(payload.Links);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScannerReportReadyPayload_WithAllProperties_ContainsValues()
|
||||
{
|
||||
var generatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
var payload = new ScannerReportReadyPayload
|
||||
{
|
||||
ReportId = "rpt-001",
|
||||
ScanId = "scan-001",
|
||||
ImageDigest = "sha256:xyz789",
|
||||
GeneratedAt = generatedAt,
|
||||
Verdict = "FAIL",
|
||||
QuietedFindingCount = 5
|
||||
};
|
||||
|
||||
Assert.Equal("rpt-001", payload.ReportId);
|
||||
Assert.Equal("scan-001", payload.ScanId);
|
||||
Assert.Equal("sha256:xyz789", payload.ImageDigest);
|
||||
Assert.Equal(generatedAt, payload.GeneratedAt);
|
||||
Assert.Equal("FAIL", payload.Verdict);
|
||||
Assert.Equal(5, payload.QuietedFindingCount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("PASS")]
|
||||
[InlineData("FAIL")]
|
||||
[InlineData("WARN")]
|
||||
[InlineData("UNKNOWN")]
|
||||
public void ScannerReportReadyPayload_Verdict_SupportedValues(string verdict)
|
||||
{
|
||||
var payload = new ScannerReportReadyPayload
|
||||
{
|
||||
Verdict = verdict
|
||||
};
|
||||
|
||||
Assert.Equal(verdict, payload.Verdict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<UseXunitV3>true</UseXunitV3>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Orchestrator.Schemas\StellaOps.Orchestrator.Schemas.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||
"diagnosticMessages": true,
|
||||
"parallelizeAssembly": true,
|
||||
"parallelizeTestCollections": true,
|
||||
"maxParallelThreads": -1
|
||||
}
|
||||
@@ -324,7 +324,7 @@ internal static class LatticeArbs
|
||||
|
||||
public static Arbitrary<List<EvidenceType>> EvidenceSequence(int minLength, int maxLength) =>
|
||||
(from length in Gen.Choose(minLength, maxLength)
|
||||
from sequence in Gen.ListOf(length, Gen.Elements(AllEvidenceTypes))
|
||||
from sequence in Gen.ListOf<EvidenceType>(Gen.Elements(AllEvidenceTypes), length)
|
||||
select sequence.ToList()).ToArbitrary();
|
||||
|
||||
public static Arbitrary<(LatticeState, EvidenceType)> ReinforcingEvidencePair()
|
||||
@@ -345,7 +345,8 @@ internal static class LatticeArbs
|
||||
{
|
||||
Purl = "pkg:npm/test@1.0.0",
|
||||
Namespace = "test",
|
||||
SymbolName = "testFunc"
|
||||
Type = "_",
|
||||
Method = "testFunc"
|
||||
};
|
||||
|
||||
var reachableResult = new StaticReachabilityResult
|
||||
@@ -381,7 +382,8 @@ internal static class LatticeArbs
|
||||
{
|
||||
Purl = "pkg:npm/test@1.0.0",
|
||||
Namespace = "test",
|
||||
SymbolName = "testFunc"
|
||||
Type = "_",
|
||||
Method = "testFunc"
|
||||
};
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
using StellaOps.Signals.Contracts;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Contracts.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for SignalEnvelope model.
|
||||
/// </summary>
|
||||
public sealed class SignalEnvelopeTests
|
||||
{
|
||||
[Fact]
|
||||
public void SignalEnvelope_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var envelope = new SignalEnvelope
|
||||
{
|
||||
SignalKey = "pkg:npm/lodash@4.17.21:reachability",
|
||||
SignalType = SignalType.Reachability,
|
||||
Value = new { reachable = true, confidence = 0.95 },
|
||||
ComputedAt = DateTimeOffset.UtcNow,
|
||||
SourceService = "reachability-analyzer"
|
||||
};
|
||||
|
||||
Assert.Equal("pkg:npm/lodash@4.17.21:reachability", envelope.SignalKey);
|
||||
Assert.Equal(SignalType.Reachability, envelope.SignalType);
|
||||
Assert.Equal("reachability-analyzer", envelope.SourceService);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SignalType.Reachability)]
|
||||
[InlineData(SignalType.Entropy)]
|
||||
[InlineData(SignalType.Exploitability)]
|
||||
[InlineData(SignalType.Trust)]
|
||||
[InlineData(SignalType.UnknownSymbol)]
|
||||
[InlineData(SignalType.Custom)]
|
||||
public void SignalEnvelope_SignalType_AllValues_AreValid(SignalType type)
|
||||
{
|
||||
var envelope = new SignalEnvelope
|
||||
{
|
||||
SignalKey = $"test:{type.ToString().ToLowerInvariant()}",
|
||||
SignalType = type,
|
||||
Value = new { test = true },
|
||||
ComputedAt = DateTimeOffset.UtcNow,
|
||||
SourceService = "test-service"
|
||||
};
|
||||
|
||||
Assert.Equal(type, envelope.SignalType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SignalEnvelope_DefaultSchemaVersion_IsOne()
|
||||
{
|
||||
var envelope = new SignalEnvelope
|
||||
{
|
||||
SignalKey = "test:schema",
|
||||
SignalType = SignalType.Custom,
|
||||
Value = new { },
|
||||
ComputedAt = DateTimeOffset.UtcNow,
|
||||
SourceService = "test"
|
||||
};
|
||||
|
||||
Assert.Equal("1.0", envelope.SchemaVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SignalEnvelope_OptionalProperties_AreNullByDefault()
|
||||
{
|
||||
var envelope = new SignalEnvelope
|
||||
{
|
||||
SignalKey = "test:optional",
|
||||
SignalType = SignalType.Reachability,
|
||||
Value = new { },
|
||||
ComputedAt = DateTimeOffset.UtcNow,
|
||||
SourceService = "test"
|
||||
};
|
||||
|
||||
Assert.Null(envelope.TenantId);
|
||||
Assert.Null(envelope.CorrelationId);
|
||||
Assert.Null(envelope.ProvenanceDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SignalEnvelope_WithAllOptionalProperties_ContainsValues()
|
||||
{
|
||||
var envelope = new SignalEnvelope
|
||||
{
|
||||
SignalKey = "pkg:pypi/django@4.2.0:trust",
|
||||
SignalType = SignalType.Trust,
|
||||
Value = new { score = 0.85 },
|
||||
ComputedAt = DateTimeOffset.UtcNow,
|
||||
SourceService = "trust-engine",
|
||||
TenantId = "tenant-001",
|
||||
CorrelationId = "corr-abc123",
|
||||
ProvenanceDigest = "sha256:xyz789",
|
||||
SchemaVersion = "2.0"
|
||||
};
|
||||
|
||||
Assert.Equal("tenant-001", envelope.TenantId);
|
||||
Assert.Equal("corr-abc123", envelope.CorrelationId);
|
||||
Assert.Equal("sha256:xyz789", envelope.ProvenanceDigest);
|
||||
Assert.Equal("2.0", envelope.SchemaVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SignalEnvelope_Value_CanBeAnyObject()
|
||||
{
|
||||
var reachabilityValue = new
|
||||
{
|
||||
reachable = true,
|
||||
paths = new[] { "main->helper->vulnerable" },
|
||||
confidence = 0.92
|
||||
};
|
||||
|
||||
var envelope = new SignalEnvelope
|
||||
{
|
||||
SignalKey = "test:value",
|
||||
SignalType = SignalType.Reachability,
|
||||
Value = reachabilityValue,
|
||||
ComputedAt = DateTimeOffset.UtcNow,
|
||||
SourceService = "analyzer"
|
||||
};
|
||||
|
||||
Assert.NotNull(envelope.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SignalEnvelope_RecordEquality_WorksCorrectly()
|
||||
{
|
||||
var computedAt = DateTimeOffset.UtcNow;
|
||||
var value = new { test = 123 };
|
||||
|
||||
var envelope1 = new SignalEnvelope
|
||||
{
|
||||
SignalKey = "test:eq",
|
||||
SignalType = SignalType.Entropy,
|
||||
Value = value,
|
||||
ComputedAt = computedAt,
|
||||
SourceService = "test"
|
||||
};
|
||||
|
||||
var envelope2 = new SignalEnvelope
|
||||
{
|
||||
SignalKey = "test:eq",
|
||||
SignalType = SignalType.Entropy,
|
||||
Value = value,
|
||||
ComputedAt = computedAt,
|
||||
SourceService = "test"
|
||||
};
|
||||
|
||||
Assert.Equal(envelope1, envelope2);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for SignalType enum.
|
||||
/// </summary>
|
||||
public sealed class SignalTypeTests
|
||||
{
|
||||
[Fact]
|
||||
public void SignalType_AllDefinedValues_AreCounted()
|
||||
{
|
||||
var values = Enum.GetValues<SignalType>();
|
||||
|
||||
// Ensure we have expected signal types
|
||||
Assert.Equal(6, values.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SignalType_Reachability_HasValue()
|
||||
{
|
||||
Assert.Equal(0, (int)SignalType.Reachability);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SignalType_Custom_IsLast()
|
||||
{
|
||||
var values = Enum.GetValues<SignalType>();
|
||||
var last = values.Max();
|
||||
|
||||
Assert.Equal(SignalType.Custom, last);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<UseXunitV3>true</UseXunitV3>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Signals.Contracts\StellaOps.Signals.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||
"diagnosticMessages": true,
|
||||
"parallelizeAssembly": true,
|
||||
"parallelizeTestCollections": true,
|
||||
"maxParallelThreads": -1
|
||||
}
|
||||
Reference in New Issue
Block a user