diff --git a/RPCTest/RPCTest.csproj b/RPCTest/RPCTest.csproj new file mode 100644 index 000000000..8638c026d --- /dev/null +++ b/RPCTest/RPCTest.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + latest + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/RPCTest/Registry/StrategyExecutorTests.cs b/RPCTest/Registry/StrategyExecutorTests.cs new file mode 100644 index 000000000..da723f646 --- /dev/null +++ b/RPCTest/Registry/StrategyExecutorTests.cs @@ -0,0 +1,130 @@ +using SharpHoundRPC.Registry; +using Xunit; + +namespace RPCTest.Registry; + +public class StrategyExecutorTests { + private readonly StrategyExecutor _strategyExecutor = new(); + + [Fact] + public async Task CollectAsync_NoStrategies_ReturnsFailure_WithNoAttempts() { + // Act + var result = await _strategyExecutor.CollectAsync("target machine", [], []); + + // Assert + Assert.NotNull(result); + Assert.False(result.WasSuccessful); + Assert.Empty(result.FailureAttempts!); + Assert.Null(result.Results); + Assert.Null(result.SuccessfulStrategy); + } + + [Fact] + public async Task CollectAsync_CanExecuteIsFalse_ReturnsFailure_WithAttempt() { + //Arrange + var strategy = new FakeCollectionStrategy(false, "well now I am not doing it"); + + // Act + var result = await _strategyExecutor.CollectAsync("target machine", [], [strategy]); + + // Assert + Assert.False(result.WasSuccessful); + Assert.Null(result.Results); + Assert.Null(result.SuccessfulStrategy); + + var attempt = result.FailureAttempts?.Single(); + Assert.Equal("well now I am not doing it", attempt?.FailureReason); + Assert.Equal(strategy.GetType(), attempt?.StrategyType); + } + + [Theory] + [MemberData(nameof(StrategyExceptions))] + public async Task CollectAsync_ThrowsException_ReturnsFailure_WithExceptionMessage(Exception strategyException) { + //Arrange + var strategy = new FakeCollectionStrategy( + canExecute: true, + exception: strategyException + ); + + // Act + var result = await _strategyExecutor.CollectAsync("target machine", [], [strategy]); + + // Assert + Assert.False(result.WasSuccessful); + Assert.Null(result.Results); + Assert.Null(result.SuccessfulStrategy); + + var attempt = result.FailureAttempts?.Single(); + Assert.Contains($"Collector failed: {strategyException.Message}.", attempt!.FailureReason!); + + if (strategyException.InnerException is not null) + Assert.Contains($"\nInner Exception: {strategyException.InnerException}", attempt!.FailureReason!); + else + Assert.DoesNotContain("Inner Exception:", attempt!.FailureReason!); + } + + public static IEnumerable StrategyExceptions => + [ + [new Exception("Outer Exception")], + [new Exception("Outer Exception", new Exception("Inner Exception"))] + ]; + + [Fact] + public async Task CollectAsync_FirstStrategySuccessful_ReturnsSuccess_WithNoAttempts() { + //Arrange + var strategyResult = new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0", "NtlmMinClientSec", 1, null, true); + var strategy = new FakeCollectionStrategy( + true, + results: [strategyResult] + ); + + // Act + var result = await _strategyExecutor.CollectAsync("target machine", [], [strategy]); + + // Assert + Assert.True(result.WasSuccessful); + Assert.Empty(result.FailureAttempts!); + Assert.Equal(strategy.GetType(), result.SuccessfulStrategy); + Assert.Equal(strategyResult, result.Results?.Single()); + } + + [Fact] + public async Task CollectAsync_SecondStrategySuccessful_ReturnsSuccess_WithAttempt() { + //Arrange + var failedStrategy = new FakeCollectionStrategy(false, "well now I am not doing it"); + var strategyResult = new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0", "NtlmMinClientSec", 1, null, true); + var successfulStrategy = new FakeCollectionStrategy( + true, + results: [strategyResult] + ); + + // Act + var result = await _strategyExecutor.CollectAsync("target machine", [], [failedStrategy, successfulStrategy]); + + // Assert + Assert.True(result.WasSuccessful); + Assert.Single(result.FailureAttempts!); + Assert.Equal(successfulStrategy.GetType(), result.SuccessfulStrategy); + Assert.Equal(strategyResult, result.Results?.Single()); + } +} + +internal sealed class FakeCollectionStrategy( + bool canExecute, + string failureReason = "", + Exception? exception = null, + IEnumerable? results = null +) : ICollectionStrategy +{ + public Task<(bool, string)> CanExecute(string target) + => Task.FromResult((canExecute, failureReason)); + + public Task> ExecuteAsync( + string target, + IEnumerable queries) { + if (exception is not null) + throw exception; + + return Task.FromResult(results ?? []); + } +} \ No newline at end of file diff --git a/SharpHoundCommon.sln b/SharpHoundCommon.sln index 8d0c35a8c..797661a83 100644 --- a/SharpHoundCommon.sln +++ b/SharpHoundCommon.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docfx", "docfx\Docfx.csproj EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpHoundRPC", "src\SharpHoundRPC\SharpHoundRPC.csproj", "{4F06116D-88A7-4601-AB28-B48F2857D458}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RPCTest", "RPCTest\RPCTest.csproj", "{F1E6714E-72B3-4295-9940-0EAA69695201}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,5 +33,9 @@ Global {4F06116D-88A7-4601-AB28-B48F2857D458}.Debug|Any CPU.Build.0 = Debug|Any CPU {4F06116D-88A7-4601-AB28-B48F2857D458}.Release|Any CPU.ActiveCfg = Release|Any CPU {4F06116D-88A7-4601-AB28-B48F2857D458}.Release|Any CPU.Build.0 = Release|Any CPU + {F1E6714E-72B3-4295-9940-0EAA69695201}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1E6714E-72B3-4295-9940-0EAA69695201}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1E6714E-72B3-4295-9940-0EAA69695201}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1E6714E-72B3-4295-9940-0EAA69695201}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/CommonLib/Processors/ComputerAvailability.cs b/src/CommonLib/Processors/ComputerAvailability.cs index 54bc41049..d0564729b 100644 --- a/src/CommonLib/Processors/ComputerAvailability.cs +++ b/src/CommonLib/Processors/ComputerAvailability.cs @@ -95,11 +95,19 @@ await SendComputerStatus(new CSVComputerStatus { }; } - if (_skipPortScan) + if (_skipPortScan) { + await SendComputerStatus(new CSVComputerStatus { + Status = CSVComputerStatus.StatusSuccess, + Task = "ComputerAvailability", + ComputerName = computerName, + ObjectId = objectId, + }); + return new ComputerStatus { Connectable = true, Error = null }; + } if (!await _scanner.CheckPort(computerName)) { _log.LogTrace("{ComputerName} is not available because port 445 is unavailable", computerName); diff --git a/src/CommonLib/Processors/RegistryProcessor.cs b/src/CommonLib/Processors/RegistryProcessor.cs index ebf381aed..d514eead3 100644 --- a/src/CommonLib/Processors/RegistryProcessor.cs +++ b/src/CommonLib/Processors/RegistryProcessor.cs @@ -6,20 +6,24 @@ using System; using System.Linq; using System.Threading.Tasks; -using static SharpHoundCommonLib.Helpers; namespace SharpHoundCommonLib.Processors; public class RegistryProcessor { + public delegate Task ComputerStatusDelegate(CSVComputerStatus status); + private readonly ILogger _log; private readonly IPortScanner _portScanner; + private readonly IStrategyExecutor _registryCollector; + private readonly AdaptiveTimeout _registryAdaptiveTimeout = new(maxTimeout:TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(ReadRegistrySettings))); private readonly ICollectionStrategy[] _strategies; private readonly RegistryQuery[] _queries; - private readonly AdaptiveTimeout _registryAdaptiveTimeout = new(maxTimeout:TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(ReadRegistrySettings))); - public RegistryProcessor(ILogger log, string domain) { + public RegistryProcessor(ILogger log, IStrategyExecutor registryCollector, string domain) { _log = log ?? Logging.LogProvider.CreateLogger("RegistryProcessor"); _portScanner = new PortScanner(); + _registryCollector = registryCollector; + _strategies = [ // Higher priority at the top of the list new DotNetWmiRegistryStrategy(_portScanner, domain), @@ -50,12 +54,13 @@ public RegistryProcessor(ILogger log, string domain) { ]; } + public event ComputerStatusDelegate ComputerStatusEvent; + public async Task> ReadRegistrySettings(string targetMachine) { var output = new RegistryData(); try { - var registryCollector = new StrategyExecutor(); - var result = await _registryAdaptiveTimeout.ExecuteWithTimeout(async (_) => await registryCollector + var result = await _registryAdaptiveTimeout.ExecuteWithTimeout(async (_) => await _registryCollector .CollectAsync(targetMachine, _queries, _strategies) .ConfigureAwait(false)); @@ -65,6 +70,32 @@ public async Task> ReadRegistrySettings(string targetMac var collectedData = result.Value; + foreach (var attempt in collectedData.FailureAttempts ?? []) { + _log.LogTrace("ReadRegistry failed on {ComputerName} using {Strategy}: {Error}", targetMachine, attempt.StrategyType.Name, attempt.FailureReason); + await SendComputerStatus(new CSVComputerStatus + { + Task = $"{nameof(ReadRegistrySettings)} - {attempt.StrategyType.Name}", + ComputerName = targetMachine, + Status = attempt.FailureReason + }); + } + + if (!collectedData.WasSuccessful) { + var msg = collectedData.FailureAttempts is null + ? "Failed to read registry settings" + : string.Join("\n", + collectedData.FailureAttempts.Select(a => $"{a.StrategyType.Name}: {a.FailureReason ?? ""}")); + + return APIResult.Failure(msg); + } + + await SendComputerStatus(new CSVComputerStatus + { + Task = $"{nameof(ReadRegistrySettings)} - {collectedData.SuccessfulStrategy?.Name ?? ""}", + ComputerName = targetMachine, + Status = CSVComputerStatus.StatusSuccess + }); + foreach (var key in collectedData.Results ?? []) { if (!key.ValueExists) continue; @@ -101,13 +132,6 @@ public async Task> ReadRegistrySettings(string targetMac } } - // If all strategies failed, need to report errors. - if (collectedData.FailureAttempts.Count() == _strategies.Length) { - string msg = string.Join("\n", - collectedData.FailureAttempts.Select(a => $"{a.StrategyType.Name}: {a.FailureReason ?? ""}")); - return APIResult.Failure(msg); - } - return APIResult.Success(output); } catch (Exception ex) { _log.LogError( @@ -118,4 +142,8 @@ public async Task> ReadRegistrySettings(string targetMac return APIResult.Failure(ex.ToString()); } } + + private async Task SendComputerStatus(CSVComputerStatus status) { + if (ComputerStatusEvent is not null) await ComputerStatusEvent.Invoke(status); + } } \ No newline at end of file diff --git a/src/SharpHoundRPC/Registry/DotNetWmiRegistryStrategy.cs b/src/SharpHoundRPC/Registry/DotNetWmiRegistryStrategy.cs index ad281c889..7d48acc86 100644 --- a/src/SharpHoundRPC/Registry/DotNetWmiRegistryStrategy.cs +++ b/src/SharpHoundRPC/Registry/DotNetWmiRegistryStrategy.cs @@ -26,7 +26,6 @@ public class DotNetWmiRegistryStrategy : ICollectionStrategy public bool UseKerberos { get; set; } = true; - /// /// Creates a new WMI registry strategy /// @@ -38,6 +37,9 @@ public DotNetWmiRegistryStrategy(IPortScanner scanner, string domain) { } public async Task<(bool, string)> CanExecute(string targetMachine) { + if (string.IsNullOrEmpty(targetMachine)) { + throw new ArgumentException("Target machine cannot be null or empty", nameof(targetMachine)); + } try { var isOpen = await _portScanner.CheckPort(targetMachine, EpMapperPort, throwError: true); return (isOpen, string.Empty); diff --git a/src/SharpHoundRPC/Registry/NativeUtils.cs b/src/SharpHoundRPC/Registry/NativeUtils.cs index 4b07a28ee..8ac1cf626 100644 --- a/src/SharpHoundRPC/Registry/NativeUtils.cs +++ b/src/SharpHoundRPC/Registry/NativeUtils.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; using static SharpHoundRPC.NetAPINative.NetAPIEnums; namespace SharpHoundRPC.Registry { diff --git a/src/SharpHoundRPC/Registry/RemoteRegistryStrategy.cs b/src/SharpHoundRPC/Registry/RemoteRegistryStrategy.cs index f21d7b688..16185fce8 100644 --- a/src/SharpHoundRPC/Registry/RemoteRegistryStrategy.cs +++ b/src/SharpHoundRPC/Registry/RemoteRegistryStrategy.cs @@ -1,7 +1,7 @@ #nullable enable namespace SharpHoundRPC.Registry { using Microsoft.Win32; - using SharpHoundRPC.PortScanner; + using PortScanner; using System; using System.Collections.Generic; using System.Linq; @@ -38,7 +38,6 @@ public async Task> ExecuteAsync( if (queries == null || !queries.Any()) throw new ArgumentException("Queries cannot be null or empty", nameof(queries)); - return await Task.Run(() => { var results = new List(); diff --git a/src/SharpHoundRPC/Registry/StrategyExecutor.cs b/src/SharpHoundRPC/Registry/StrategyExecutor.cs index fda7da462..a95f5fd89 100644 --- a/src/SharpHoundRPC/Registry/StrategyExecutor.cs +++ b/src/SharpHoundRPC/Registry/StrategyExecutor.cs @@ -4,8 +4,15 @@ namespace SharpHoundRPC.Registry { using System.Collections.Generic; using System.Threading.Tasks; - - public class StrategyExecutor { + public interface IStrategyExecutor + { + Task> CollectAsync( + string targetMachine, + IEnumerable queries, + IEnumerable> strategies); + } + + public class StrategyExecutor : IStrategyExecutor { public async Task> CollectAsync( string targetMachine, IEnumerable queries, @@ -25,24 +32,26 @@ public async Task> CollectAsync( try { var results = await strategy.ExecuteAsync(targetMachine, queries).ConfigureAwait(false); - attempt.WasSuccessful = true; - attempt.Results = results; - return new StrategyExecutorResult { Results = results, FailureAttempts = attempts, - WasSuccessful = true + WasSuccessful = true, + SuccessfulStrategy = strategy.GetType() }; } catch (Exception ex) { - attempt.FailureReason = $"Collector failed: {ex.Message}.\nInner Exception: {ex.InnerException}"; + var innerException = ex.InnerException != null + ? $"\nInner Exception: {ex.InnerException}" + : string.Empty; + + attempt.FailureReason = $"Collector failed: {ex.Message}.{innerException}"; } attempts.Add(attempt); } return new StrategyExecutorResult { - Results = null, - FailureAttempts = attempts + FailureAttempts = attempts, + WasSuccessful = false, }; } } diff --git a/src/SharpHoundRPC/Registry/StrategyExecutorResult.cs b/src/SharpHoundRPC/Registry/StrategyExecutorResult.cs index 0cfc4514f..010311b9d 100644 --- a/src/SharpHoundRPC/Registry/StrategyExecutorResult.cs +++ b/src/SharpHoundRPC/Registry/StrategyExecutorResult.cs @@ -1,12 +1,16 @@ #nullable enable -namespace SharpHoundRPC.Registry { - using System.Collections.Generic; +using System; +using System.Collections.Generic; + +namespace SharpHoundRPC.Registry { public class StrategyExecutorResult { public IEnumerable? Results { get; set; } = null; public IEnumerable>? FailureAttempts { get; set; } = null; public bool WasSuccessful = false; + public Type? SuccessfulStrategy { get; set; } = null; } -#nullable disable -} \ No newline at end of file +} + +#nullable disable \ No newline at end of file diff --git a/test/unit/Facades/MockExtentions.cs b/test/unit/Facades/MockExtentions.cs index 7d8169e30..856bd64f4 100644 --- a/test/unit/Facades/MockExtentions.cs +++ b/test/unit/Facades/MockExtentions.cs @@ -32,4 +32,16 @@ public static void VerifyLog(this Mock> mockLogger, LogLevel logLe It.IsAny>()), Times.Once); } + + public static void VerifyNoLogs(this Mock> mockLogger, LogLevel logLevel) + { + mockLogger.Verify( + x => x.Log( + logLevel, + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>()), + Times.Never); + } } \ No newline at end of file diff --git a/test/unit/RegistryProcessorTests.cs b/test/unit/RegistryProcessorTests.cs new file mode 100644 index 000000000..f86b05fcd --- /dev/null +++ b/test/unit/RegistryProcessorTests.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CommonLibTest.Facades; +using Microsoft.Extensions.Logging; +using Microsoft.Win32; +using Moq; +using SharpHoundCommonLib; +using SharpHoundCommonLib.Processors; +using Xunit; +using SharpHoundRPC.Registry; + +namespace CommonLibTest +{ + public class RegistryProcessorTest + { + private readonly Mock> _mockLogger; + private readonly Mock _mockStrategyExecutor; + private readonly RegistryProcessor _registryProcessor; + + private const string DomainName = "TEST.LOCAL"; + private const string TargetName = "target.test.local"; + + private readonly List _receivedCompStatuses = []; + + public RegistryProcessorTest() { + _mockLogger = new Mock>(); + _mockStrategyExecutor = new Mock(); + _registryProcessor = new RegistryProcessor(_mockLogger.Object, _mockStrategyExecutor.Object, DomainName); + + _registryProcessor.ComputerStatusEvent += status => { + _receivedCompStatuses.Add(status); + return Task.CompletedTask; + }; + } + + [Fact] + public async Task RegistryProcessor_ReadRegistrySettings_CollectionFailed() { + const string failureReason = "No such host is known."; + + var attempts = new List> { + new(typeof(DotNetWmiRegistryStrategy)) { + FailureReason = failureReason + }, + new(typeof(RemoteRegistryStrategy)) { + FailureReason = failureReason + } + }; + var executorResult = new StrategyExecutorResult { + FailureAttempts = attempts, + WasSuccessful = false + }; + + _mockStrategyExecutor.Setup(se => se.CollectAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>>())) + .ReturnsAsync(executorResult); + + var results = await _registryProcessor.ReadRegistrySettings(TargetName); + + //Validate result + Assert.False(results.Collected); + var expectedFailureReason = string.Join("\n", attempts.Select(a => $"{a.StrategyType.Name}: {failureReason}")); + Assert.Equal(expectedFailureReason, results.FailureReason); + + //Validate logs + VerifyFailureLog(TargetName, failureReason); + VerifyFailureLog(TargetName, failureReason); + Assert.Equal(2, _receivedCompStatuses.Count); + foreach (var attempt in attempts) { + VerifyCompStatusLog($"{nameof(_registryProcessor.ReadRegistrySettings)} - {attempt.StrategyType.Name}", TargetName, failureReason); + } + } + + [WindowsOnlyFact] + public async Task RegistryProcessor_ReadRegistrySettings_FirstStrategySuccessful() { + const uint minClientSecValue = 536870912; + + var executorResult = new StrategyExecutorResult { + Results = [ + new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0", "ClientAllowedNTLMServers", null, null, false), + new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0", "NtlmMinClientSec", minClientSecValue, RegistryValueKind.DWord, true) + ], + WasSuccessful = true, + SuccessfulStrategy = typeof(DotNetWmiRegistryStrategy) + }; + + _mockStrategyExecutor.Setup(se => se.CollectAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>>())) + .ReturnsAsync(executorResult); + + var results = await _registryProcessor.ReadRegistrySettings(TargetName); + + //Validate result + Assert.True(results.Collected); + Assert.Null(results.Result.ClientAllowedNTLMServers); + Assert.Equal(minClientSecValue, results.Result.NtlmMinClientSec); + + //Validate logs + _mockLogger.VerifyNoLogs(LogLevel.Trace); + const string task = $"{nameof(_registryProcessor.ReadRegistrySettings)} - {nameof(DotNetWmiRegistryStrategy)}"; + VerifyCompStatusLog(task, TargetName, CSVComputerStatus.StatusSuccess); + } + + [WindowsOnlyFact] + public async Task RegistryProcessor_ReadRegistrySettings_SecondStrategySuccessful() { + const string failureReason = "No such host is known."; + const uint minClientSecValue = 536870912; + + var attempts = new List> { + new(typeof(DotNetWmiRegistryStrategy)) { + FailureReason = failureReason + } + }; + var executorResult = new StrategyExecutorResult { + FailureAttempts = attempts, + Results = [ + new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0", "NtlmMinClientSec", minClientSecValue, RegistryValueKind.DWord, true) + ], + WasSuccessful = true, + SuccessfulStrategy = typeof(RemoteRegistryStrategy) + }; + + _mockStrategyExecutor.Setup(se => se.CollectAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>>())) + .ReturnsAsync(executorResult); + + var results = await _registryProcessor.ReadRegistrySettings(TargetName); + + //Validate result + Assert.True(results.Collected); + Assert.Equal(minClientSecValue, results.Result.NtlmMinClientSec); + + //Validate logs + VerifyFailureLog(TargetName, failureReason); + Assert.Equal(2, _receivedCompStatuses.Count); + VerifyCompStatusLog($"{nameof(_registryProcessor.ReadRegistrySettings)} - {attempts[0].StrategyType.Name}", TargetName, failureReason); + const string task = $"{nameof(_registryProcessor.ReadRegistrySettings)} - {nameof(RemoteRegistryStrategy)}"; + VerifyCompStatusLog(task, TargetName, CSVComputerStatus.StatusSuccess); + } + + [Fact] + public async Task RegistryProcessor_ReadRegistrySettings_HandlesException() { + var exception = new Exception("test exception"); + + _mockStrategyExecutor.Setup(se => se.CollectAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>>())) + .Throws(exception); + + var results = await _registryProcessor.ReadRegistrySettings(TargetName); + + //Validate result + Assert.False(results.Collected); + Assert.Equal(exception.ToString(), results.FailureReason); + + //Validate logs + _mockLogger.VerifyLogContains(LogLevel.Error, $"Unhandled Registry Processor exception {TargetName}: {exception}"); + Assert.Empty(_receivedCompStatuses); + } + + [WindowsOnlyFact] + public async Task RegistryProcessor_ReadRegistrySettings_SetsAllValues() { + var allowedServers = new[] {"server"}; + const uint keyValue = 1; + + var executorResult = new StrategyExecutorResult { + Results = [ + new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0", "ClientAllowedNTLMServers", allowedServers, RegistryValueKind.MultiString, true), + new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0", "NtlmMinClientSec", keyValue, RegistryValueKind.DWord, true), + new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0", "NtlmMinServerSec", keyValue, RegistryValueKind.DWord, true), + new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0", "RestrictSendingNTLMTraffic", keyValue, RegistryValueKind.DWord, true), + new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0", "RestrictReceivingNTLMTraffic", keyValue, RegistryValueKind.DWord, true), + new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Control\Lsa\", "LMCompatibilityLevel", keyValue, RegistryValueKind.DWord, true), + new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Control\Lsa\", "UseMachineId", keyValue, RegistryValueKind.DWord, true), + new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters", "RequireSecuritySignature", keyValue, RegistryValueKind.DWord, true), + new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters", "EnableSecuritySignature", keyValue, RegistryValueKind.DWord, true), + ], + WasSuccessful = true, + SuccessfulStrategy = typeof(DotNetWmiRegistryStrategy) + }; + + _mockStrategyExecutor.Setup(se => se.CollectAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>>())) + .ReturnsAsync(executorResult); + + var results = await _registryProcessor.ReadRegistrySettings(TargetName); + + //Validate result + Assert.True(results.Collected); + Assert.Equal(allowedServers, results.Result.ClientAllowedNTLMServers); + Assert.Equal(keyValue, results.Result.NtlmMinClientSec); + Assert.Equal(keyValue, results.Result.NtlmMinServerSec); + Assert.Equal(keyValue, results.Result.RestrictSendingNtlmTraffic); + Assert.Equal(keyValue, results.Result.RestrictReceivingNtlmTraffic); + Assert.Equal(keyValue, results.Result.LmCompatibilityLevel); + Assert.Equal(keyValue, results.Result.UseMachineId); + Assert.Equal(keyValue, results.Result.RequireSecuritySignature); + Assert.Equal(keyValue, results.Result.EnableSecuritySignature); + + //Validate logs + const string task = $"{nameof(_registryProcessor.ReadRegistrySettings)} - {nameof(DotNetWmiRegistryStrategy)}"; + VerifyCompStatusLog(task, TargetName, CSVComputerStatus.StatusSuccess); + } + + [Fact] + public async Task RegistryProcessor_ReadRegistrySettings_HandlesFailureWithNoAttempts() { + var executorResult = new StrategyExecutorResult { + WasSuccessful = false + }; + + _mockStrategyExecutor.Setup(se => se.CollectAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>>())) + .ReturnsAsync(executorResult); + + var results = await _registryProcessor.ReadRegistrySettings(TargetName); + + //Validate result + Assert.False(results.Collected); + Assert.Equal("Failed to read registry settings", results.FailureReason); + + //Validate logs + _mockLogger.VerifyNoLogs(LogLevel.Trace); + Assert.Empty(_receivedCompStatuses); + } + + private void VerifyFailureLog(string target, string reason) { + var expected = $"ReadRegistry failed on {target} using {typeof(TStrategy).Name}: {reason}"; + _mockLogger.VerifyLogContains(LogLevel.Trace, expected); + } + + private void VerifyCompStatusLog(string task, string computerName, string status) { + Assert.Contains(_receivedCompStatuses, + compStat => compStat.Task == task && + compStat.ComputerName == computerName && + compStat.Status == status ); + } + } +} \ No newline at end of file