audit notes work completed, test fixes work (95% done), new sprints, new data sources setup and configuration

This commit is contained in:
master
2026-01-14 10:48:00 +02:00
parent d7be6ba34b
commit 95d5898650
379 changed files with 40695 additions and 19041 deletions

View File

@@ -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>

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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..."));
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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..."));
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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..."));
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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

View File

@@ -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;
}
}
}
}

View File

@@ -1,4 +1,4 @@
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
}
}
}
}
}

View File

@@ -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
}
}
}
}
}

View File

@@ -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
}
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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: ????? ?????????? ??????????? ? ???????????? ???????????")]

View File

@@ -34,6 +34,7 @@ public sealed class AIPlugin : IDoctorPlugin
new LlmProviderConfigurationCheck(),
new ClaudeProviderCheck(),
new OpenAiProviderCheck(),
new GeminiProviderCheck(),
new OllamaProviderCheck(),
new LocalInferenceCheck()
];

View File

@@ -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] + "...";
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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());
}
}

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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
};
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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). |

View File

@@ -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}";
}

View 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`

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -0,0 +1,7 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"diagnosticMessages": true,
"parallelizeAssembly": true,
"parallelizeTestCollections": true,
"maxParallelThreads": -1
}

View File

@@ -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")
};
}
}

View File

@@ -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")
};
}
}

View File

@@ -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")
};
}
}

View File

@@ -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")
};
}
}

View File

@@ -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")
};
}
}

View File

@@ -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")
};
}
}

View File

@@ -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>

View File

@@ -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")
};
}
}

View File

@@ -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")
};
}
}

View File

@@ -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")
};
}
}

View File

@@ -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")
};
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -0,0 +1,7 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"diagnosticMessages": true,
"parallelizeAssembly": true,
"parallelizeTestCollections": true,
"maxParallelThreads": -1
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -0,0 +1,7 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"diagnosticMessages": true,
"parallelizeAssembly": true,
"parallelizeTestCollections": true,
"maxParallelThreads": -1
}