From e8cb013df8904e794f0413ac5c8cf6bbe6c04fbc Mon Sep 17 00:00:00 2001 From: Lucas Falslev Date: Thu, 12 Feb 2026 17:13:29 -0700 Subject: [PATCH 01/10] add compstatus to registryprocessor --- .../Processors/ComputerAvailability.cs | 10 ++++- src/CommonLib/Processors/RegistryProcessor.cs | 41 +++++++++++++++---- .../Registry/DotNetWmiRegistryStrategy.cs | 3 ++ src/SharpHoundRPC/Registry/NativeUtils.cs | 2 - .../Registry/RemoteRegistryStrategy.cs | 3 +- .../Registry/StrategyExecutor.cs | 4 -- 6 files changed, 46 insertions(+), 17 deletions(-) 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..f96bf9813 100644 --- a/src/CommonLib/Processors/RegistryProcessor.cs +++ b/src/CommonLib/Processors/RegistryProcessor.cs @@ -5,12 +5,14 @@ using SharpHoundRPC.Registry; using System; using System.Linq; +using System.Runtime.Serialization.Json; 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 ICollectionStrategy[] _strategies; @@ -50,6 +52,8 @@ public RegistryProcessor(ILogger log, string domain) { ]; } + public event ComputerStatusDelegate ComputerStatusEvent; + public async Task> ReadRegistrySettings(string targetMachine) { var output = new RegistryData(); @@ -65,6 +69,22 @@ 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, attempt.FailureReason); + await SendComputerStatus(new CSVComputerStatus + { + Status = attempt.FailureReason, + ComputerName = targetMachine, + Task = nameof(ReadRegistrySettings) + }); + } + + if (!collectedData.WasSuccessful) { + string msg = string.Join("\n", + collectedData.FailureAttempts.Select(a => $"{a.StrategyType.Name}: {a.FailureReason ?? ""}")); + return APIResult.Failure(msg); + } + foreach (var key in collectedData.Results ?? []) { if (!key.ValueExists) continue; @@ -100,13 +120,14 @@ public async Task> ReadRegistrySettings(string targetMac break; } } - - // 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); - } + + await SendComputerStatus(new CSVComputerStatus + { + Status = CSVComputerStatus.StatusSuccess, + ComputerName = targetMachine, + Task = nameof(ReadRegistrySettings), + // ObjectId = computerObjectId, //TODO: can we get a compId? + }); return APIResult.Success(output); } catch (Exception ex) { @@ -118,4 +139,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..343261faf 100644 --- a/src/SharpHoundRPC/Registry/DotNetWmiRegistryStrategy.cs +++ b/src/SharpHoundRPC/Registry/DotNetWmiRegistryStrategy.cs @@ -38,6 +38,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..5d230db29 100644 --- a/src/SharpHoundRPC/Registry/StrategyExecutor.cs +++ b/src/SharpHoundRPC/Registry/StrategyExecutor.cs @@ -4,7 +4,6 @@ namespace SharpHoundRPC.Registry { using System.Collections.Generic; using System.Threading.Tasks; - public class StrategyExecutor { public async Task> CollectAsync( string targetMachine, @@ -25,9 +24,6 @@ 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, From 53a2e0fe4a3867a223cd9f60b9c0cc70e13a1da6 Mon Sep 17 00:00:00 2001 From: Lucas Falslev Date: Thu, 19 Feb 2026 09:17:45 -0700 Subject: [PATCH 02/10] add RegistryProcessorTests --- src/CommonLib/Processors/RegistryProcessor.cs | 30 +-- .../Registry/StrategyExecutor.cs | 10 +- test/unit/Facades/MockExtentions.cs | 12 + test/unit/RegistryProcessorTests.cs | 226 ++++++++++++++++++ 4 files changed, 262 insertions(+), 16 deletions(-) create mode 100644 test/unit/RegistryProcessorTests.cs diff --git a/src/CommonLib/Processors/RegistryProcessor.cs b/src/CommonLib/Processors/RegistryProcessor.cs index f96bf9813..21272a27e 100644 --- a/src/CommonLib/Processors/RegistryProcessor.cs +++ b/src/CommonLib/Processors/RegistryProcessor.cs @@ -5,7 +5,6 @@ using SharpHoundRPC.Registry; using System; using System.Linq; -using System.Runtime.Serialization.Json; using System.Threading.Tasks; namespace SharpHoundCommonLib.Processors; @@ -15,13 +14,16 @@ public class RegistryProcessor { 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), @@ -58,8 +60,7 @@ public async Task> ReadRegistrySettings(string targetMac 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)); @@ -73,9 +74,9 @@ public async Task> ReadRegistrySettings(string targetMac _log.LogTrace("ReadRegistry failed on {ComputerName} using {Strategy}: {Error}", targetMachine, attempt.StrategyType, attempt.FailureReason); await SendComputerStatus(new CSVComputerStatus { - Status = attempt.FailureReason, + Task = nameof(ReadRegistrySettings), ComputerName = targetMachine, - Task = nameof(ReadRegistrySettings) + Status = attempt.StrategyType.Name + " Failed: " + attempt.FailureReason }); } @@ -84,6 +85,13 @@ await SendComputerStatus(new CSVComputerStatus collectedData.FailureAttempts.Select(a => $"{a.StrategyType.Name}: {a.FailureReason ?? ""}")); return APIResult.Failure(msg); } + + await SendComputerStatus(new CSVComputerStatus + { + Task = nameof(ReadRegistrySettings), + ComputerName = targetMachine, + Status = CSVComputerStatus.StatusSuccess + }); foreach (var key in collectedData.Results ?? []) { if (!key.ValueExists) @@ -120,14 +128,6 @@ await SendComputerStatus(new CSVComputerStatus break; } } - - await SendComputerStatus(new CSVComputerStatus - { - Status = CSVComputerStatus.StatusSuccess, - ComputerName = targetMachine, - Task = nameof(ReadRegistrySettings), - // ObjectId = computerObjectId, //TODO: can we get a compId? - }); return APIResult.Success(output); } catch (Exception ex) { diff --git a/src/SharpHoundRPC/Registry/StrategyExecutor.cs b/src/SharpHoundRPC/Registry/StrategyExecutor.cs index 5d230db29..3b802fed1 100644 --- a/src/SharpHoundRPC/Registry/StrategyExecutor.cs +++ b/src/SharpHoundRPC/Registry/StrategyExecutor.cs @@ -4,7 +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, 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..a87bf951e --- /dev/null +++ b/test/unit/RegistryProcessorTests.cs @@ -0,0 +1,226 @@ +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_CollectionNotSuccessful() { + const string failureReason = "No such host is known."; + var attempts = new List> + { + new(typeof(DotNetWmiRegistryStrategy)) + { + FailureReason = failureReason + }, + new(typeof(RemoteRegistryStrategy)) + { + FailureReason = failureReason + } + }; + + _mockStrategyExecutor.Setup(se => se.CollectAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>>())) + .ReturnsAsync( + new StrategyExecutorResult { + FailureAttempts = attempts, + WasSuccessful = false + } + ); + + 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), TargetName, $"{attempt.StrategyType.Name} Failed: {failureReason}"); + } + } + + [WindowsOnlyFact] + public async Task RegistryProcessor_ReadRegistrySettings_FirstStrategySuccessful() { + const uint minClientSecValue = 536870912; + + _mockStrategyExecutor.Setup(se => se.CollectAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>>())) + .ReturnsAsync( + 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, + } + ); + + 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); + VerifyCompStatusLog(nameof(_registryProcessor.ReadRegistrySettings), 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 + } + }; + + _mockStrategyExecutor.Setup(se => se.CollectAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>>())) + .ReturnsAsync( + new StrategyExecutorResult { + FailureAttempts = attempts, + Results = [ + new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0", "NtlmMinClientSec", minClientSecValue, RegistryValueKind.DWord, true) + ], + WasSuccessful = true + } + ); + + 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), TargetName, $"{attempts[0].StrategyType.Name} Failed: {failureReason}"); + VerifyCompStatusLog(nameof(_registryProcessor.ReadRegistrySettings), TargetName, CSVComputerStatus.StatusSuccess); + } + + [WindowsOnlyFact] + 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(results.FailureReason, exception.ToString()); + + //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; + + _mockStrategyExecutor.Setup(se => se.CollectAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>>())) + .ReturnsAsync( + 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, + } + ); + + 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 + VerifyCompStatusLog(nameof(_registryProcessor.ReadRegistrySettings), TargetName, CSVComputerStatus.StatusSuccess); + } + + private void VerifyFailureLog(string target, string reason) { + var expected = $"ReadRegistry failed on {target} using {typeof(TStrategy)}: {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 From 9019d99c47946ebaf6f444a09c93170d899a1342 Mon Sep 17 00:00:00 2001 From: Lucas Falslev Date: Thu, 19 Feb 2026 09:55:21 -0700 Subject: [PATCH 03/10] null handling --- src/CommonLib/Processors/RegistryProcessor.cs | 9 +++++--- .../Registry/DotNetWmiRegistryStrategy.cs | 1 - test/unit/RegistryProcessorTests.cs | 23 +++++++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/CommonLib/Processors/RegistryProcessor.cs b/src/CommonLib/Processors/RegistryProcessor.cs index 21272a27e..baa92f6d5 100644 --- a/src/CommonLib/Processors/RegistryProcessor.cs +++ b/src/CommonLib/Processors/RegistryProcessor.cs @@ -79,10 +79,13 @@ await SendComputerStatus(new CSVComputerStatus Status = attempt.StrategyType.Name + " Failed: " + attempt.FailureReason }); } - + if (!collectedData.WasSuccessful) { - string msg = string.Join("\n", - collectedData.FailureAttempts.Select(a => $"{a.StrategyType.Name}: {a.FailureReason ?? ""}")); + 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); } diff --git a/src/SharpHoundRPC/Registry/DotNetWmiRegistryStrategy.cs b/src/SharpHoundRPC/Registry/DotNetWmiRegistryStrategy.cs index 343261faf..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 /// diff --git a/test/unit/RegistryProcessorTests.cs b/test/unit/RegistryProcessorTests.cs index a87bf951e..4d1424044 100644 --- a/test/unit/RegistryProcessorTests.cs +++ b/test/unit/RegistryProcessorTests.cs @@ -210,6 +210,29 @@ public async Task RegistryProcessor_ReadRegistrySettings_SetsAllValues() { //Validate logs VerifyCompStatusLog(nameof(_registryProcessor.ReadRegistrySettings), TargetName, CSVComputerStatus.StatusSuccess); } + + [Fact] + public async Task RegistryProcessor_ReadRegistrySettings_HandlesFailureWithNoAttempts() { + _mockStrategyExecutor.Setup(se => se.CollectAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>>())) + .ReturnsAsync( + new StrategyExecutorResult { + WasSuccessful = false + } + ); + + 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)}: {reason}"; From 221ce52d0824889c7196e40c1b32b3a533649c34 Mon Sep 17 00:00:00 2001 From: Lucas Falslev Date: Thu, 19 Feb 2026 11:11:15 -0700 Subject: [PATCH 04/10] clean up compstatus log --- src/CommonLib/Processors/RegistryProcessor.cs | 4 ++-- test/unit/RegistryProcessorTests.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/CommonLib/Processors/RegistryProcessor.cs b/src/CommonLib/Processors/RegistryProcessor.cs index baa92f6d5..dd93b1303 100644 --- a/src/CommonLib/Processors/RegistryProcessor.cs +++ b/src/CommonLib/Processors/RegistryProcessor.cs @@ -74,9 +74,9 @@ public async Task> ReadRegistrySettings(string targetMac _log.LogTrace("ReadRegistry failed on {ComputerName} using {Strategy}: {Error}", targetMachine, attempt.StrategyType, attempt.FailureReason); await SendComputerStatus(new CSVComputerStatus { - Task = nameof(ReadRegistrySettings), + Task = $"{nameof(ReadRegistrySettings)} - {attempt.StrategyType.Name}", ComputerName = targetMachine, - Status = attempt.StrategyType.Name + " Failed: " + attempt.FailureReason + Status = attempt.FailureReason }); } diff --git a/test/unit/RegistryProcessorTests.cs b/test/unit/RegistryProcessorTests.cs index 4d1424044..38ae7d8d2 100644 --- a/test/unit/RegistryProcessorTests.cs +++ b/test/unit/RegistryProcessorTests.cs @@ -36,7 +36,7 @@ public RegistryProcessorTest() { } [Fact] - public async Task RegistryProcessor_ReadRegistrySettings_CollectionNotSuccessful() { + public async Task RegistryProcessor_ReadRegistrySettings_CollectionFailed() { const string failureReason = "No such host is known."; var attempts = new List> { @@ -73,7 +73,7 @@ public async Task RegistryProcessor_ReadRegistrySettings_CollectionNotSuccessful VerifyFailureLog(TargetName, failureReason); Assert.Equal(2, _receivedCompStatuses.Count); foreach (var attempt in attempts) { - VerifyCompStatusLog(nameof(_registryProcessor.ReadRegistrySettings), TargetName, $"{attempt.StrategyType.Name} Failed: {failureReason}"); + VerifyCompStatusLog($"{nameof(_registryProcessor.ReadRegistrySettings)} - {attempt.StrategyType.Name}", TargetName, failureReason); } } @@ -143,7 +143,7 @@ public async Task RegistryProcessor_ReadRegistrySettings_SecondStrategySuccessfu //Validate logs VerifyFailureLog(TargetName, failureReason); Assert.Equal(2, _receivedCompStatuses.Count); - VerifyCompStatusLog(nameof(_registryProcessor.ReadRegistrySettings), TargetName, $"{attempts[0].StrategyType.Name} Failed: {failureReason}"); + VerifyCompStatusLog($"{nameof(_registryProcessor.ReadRegistrySettings)} - {attempts[0].StrategyType.Name}", TargetName, failureReason); VerifyCompStatusLog(nameof(_registryProcessor.ReadRegistrySettings), TargetName, CSVComputerStatus.StatusSuccess); } From 938303123f2efb38d1cbb00be8e91e3d23002f23 Mon Sep 17 00:00:00 2001 From: Lucas Falslev Date: Thu, 19 Feb 2026 15:28:23 -0700 Subject: [PATCH 05/10] fact attribute --- test/unit/RegistryProcessorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/RegistryProcessorTests.cs b/test/unit/RegistryProcessorTests.cs index 38ae7d8d2..a7095dd94 100644 --- a/test/unit/RegistryProcessorTests.cs +++ b/test/unit/RegistryProcessorTests.cs @@ -147,7 +147,7 @@ public async Task RegistryProcessor_ReadRegistrySettings_SecondStrategySuccessfu VerifyCompStatusLog(nameof(_registryProcessor.ReadRegistrySettings), TargetName, CSVComputerStatus.StatusSuccess); } - [WindowsOnlyFact] + [Fact] public async Task RegistryProcessor_ReadRegistrySettings_HandlesException() { var exception = new Exception("test exception"); _mockStrategyExecutor.Setup(se => se.CollectAsync( From 9350ffe8d1e944dd2128c250449a0f4d0330be01 Mon Sep 17 00:00:00 2001 From: Lucas Falslev Date: Fri, 20 Feb 2026 10:34:39 -0700 Subject: [PATCH 06/10] include registry strategy in successful compstat log --- src/CommonLib/Processors/RegistryProcessor.cs | 2 +- test/unit/RegistryProcessorTests.cs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/CommonLib/Processors/RegistryProcessor.cs b/src/CommonLib/Processors/RegistryProcessor.cs index dd93b1303..a204558ca 100644 --- a/src/CommonLib/Processors/RegistryProcessor.cs +++ b/src/CommonLib/Processors/RegistryProcessor.cs @@ -91,7 +91,7 @@ await SendComputerStatus(new CSVComputerStatus await SendComputerStatus(new CSVComputerStatus { - Task = nameof(ReadRegistrySettings), + Task = $"{nameof(ReadRegistrySettings)} - {_strategies[collectedData.FailureAttempts?.Count() ?? 0].GetType().Name}", ComputerName = targetMachine, Status = CSVComputerStatus.StatusSuccess }); diff --git a/test/unit/RegistryProcessorTests.cs b/test/unit/RegistryProcessorTests.cs index a7095dd94..224f7e617 100644 --- a/test/unit/RegistryProcessorTests.cs +++ b/test/unit/RegistryProcessorTests.cs @@ -104,7 +104,8 @@ public async Task RegistryProcessor_ReadRegistrySettings_FirstStrategySuccessful //Validate logs _mockLogger.VerifyNoLogs(LogLevel.Trace); - VerifyCompStatusLog(nameof(_registryProcessor.ReadRegistrySettings), TargetName, CSVComputerStatus.StatusSuccess); + const string task = $"{nameof(_registryProcessor.ReadRegistrySettings)} - {nameof(DotNetWmiRegistryStrategy)}"; + VerifyCompStatusLog(task, TargetName, CSVComputerStatus.StatusSuccess); } [WindowsOnlyFact] @@ -144,7 +145,8 @@ public async Task RegistryProcessor_ReadRegistrySettings_SecondStrategySuccessfu VerifyFailureLog(TargetName, failureReason); Assert.Equal(2, _receivedCompStatuses.Count); VerifyCompStatusLog($"{nameof(_registryProcessor.ReadRegistrySettings)} - {attempts[0].StrategyType.Name}", TargetName, failureReason); - VerifyCompStatusLog(nameof(_registryProcessor.ReadRegistrySettings), TargetName, CSVComputerStatus.StatusSuccess); + const string task = $"{nameof(_registryProcessor.ReadRegistrySettings)} - {nameof(RemoteRegistryStrategy)}"; + VerifyCompStatusLog(task, TargetName, CSVComputerStatus.StatusSuccess); } [Fact] @@ -208,7 +210,8 @@ public async Task RegistryProcessor_ReadRegistrySettings_SetsAllValues() { Assert.Equal(keyValue, results.Result.EnableSecuritySignature); //Validate logs - VerifyCompStatusLog(nameof(_registryProcessor.ReadRegistrySettings), TargetName, CSVComputerStatus.StatusSuccess); + const string task = $"{nameof(_registryProcessor.ReadRegistrySettings)} - {nameof(DotNetWmiRegistryStrategy)}"; + VerifyCompStatusLog(task, TargetName, CSVComputerStatus.StatusSuccess); } [Fact] From dd88aa05f2b8abbc12e0315a958b4283406fc27f Mon Sep 17 00:00:00 2001 From: Lucas Falslev Date: Fri, 20 Feb 2026 11:35:55 -0700 Subject: [PATCH 07/10] use strategy name in log --- src/CommonLib/Processors/RegistryProcessor.cs | 2 +- test/unit/RegistryProcessorTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommonLib/Processors/RegistryProcessor.cs b/src/CommonLib/Processors/RegistryProcessor.cs index a204558ca..11c2c816b 100644 --- a/src/CommonLib/Processors/RegistryProcessor.cs +++ b/src/CommonLib/Processors/RegistryProcessor.cs @@ -71,7 +71,7 @@ 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, attempt.FailureReason); + _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}", diff --git a/test/unit/RegistryProcessorTests.cs b/test/unit/RegistryProcessorTests.cs index 224f7e617..d0b7408e0 100644 --- a/test/unit/RegistryProcessorTests.cs +++ b/test/unit/RegistryProcessorTests.cs @@ -238,7 +238,7 @@ public async Task RegistryProcessor_ReadRegistrySettings_HandlesFailureWithNoAtt } private void VerifyFailureLog(string target, string reason) { - var expected = $"ReadRegistry failed on {target} using {typeof(TStrategy)}: {reason}"; + var expected = $"ReadRegistry failed on {target} using {typeof(TStrategy).Name}: {reason}"; _mockLogger.VerifyLogContains(LogLevel.Trace, expected); } From b4939bb864175345d8e8d4c22ef984ab1e344a6a Mon Sep 17 00:00:00 2001 From: Lucas Falslev Date: Fri, 20 Feb 2026 15:48:43 -0700 Subject: [PATCH 08/10] include successful strategy in result --- src/CommonLib/Processors/RegistryProcessor.cs | 2 +- .../Registry/StrategyExecutor.cs | 3 +- .../Registry/StrategyExecutorResult.cs | 12 +- test/unit/RegistryProcessorTests.cs | 110 +++++++++--------- 4 files changed, 65 insertions(+), 62 deletions(-) diff --git a/src/CommonLib/Processors/RegistryProcessor.cs b/src/CommonLib/Processors/RegistryProcessor.cs index 11c2c816b..d514eead3 100644 --- a/src/CommonLib/Processors/RegistryProcessor.cs +++ b/src/CommonLib/Processors/RegistryProcessor.cs @@ -91,7 +91,7 @@ await SendComputerStatus(new CSVComputerStatus await SendComputerStatus(new CSVComputerStatus { - Task = $"{nameof(ReadRegistrySettings)} - {_strategies[collectedData.FailureAttempts?.Count() ?? 0].GetType().Name}", + Task = $"{nameof(ReadRegistrySettings)} - {collectedData.SuccessfulStrategy?.Name ?? ""}", ComputerName = targetMachine, Status = CSVComputerStatus.StatusSuccess }); diff --git a/src/SharpHoundRPC/Registry/StrategyExecutor.cs b/src/SharpHoundRPC/Registry/StrategyExecutor.cs index 3b802fed1..55239aba3 100644 --- a/src/SharpHoundRPC/Registry/StrategyExecutor.cs +++ b/src/SharpHoundRPC/Registry/StrategyExecutor.cs @@ -35,7 +35,8 @@ public async Task> CollectAsync( 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}"; 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/RegistryProcessorTests.cs b/test/unit/RegistryProcessorTests.cs index d0b7408e0..f86b05fcd 100644 --- a/test/unit/RegistryProcessorTests.cs +++ b/test/unit/RegistryProcessorTests.cs @@ -38,28 +38,25 @@ public RegistryProcessorTest() { [Fact] public async Task RegistryProcessor_ReadRegistrySettings_CollectionFailed() { const string failureReason = "No such host is known."; - var attempts = new List> - { - new(typeof(DotNetWmiRegistryStrategy)) - { + + var attempts = new List> { + new(typeof(DotNetWmiRegistryStrategy)) { FailureReason = failureReason }, - new(typeof(RemoteRegistryStrategy)) - { + 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( - new StrategyExecutorResult { - FailureAttempts = attempts, - WasSuccessful = false - } - ); + .ReturnsAsync(executorResult); var results = await _registryProcessor.ReadRegistrySettings(TargetName); @@ -81,19 +78,20 @@ public async Task RegistryProcessor_ReadRegistrySettings_CollectionFailed() { 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( - 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, - } - ); + .ReturnsAsync(executorResult); var results = await _registryProcessor.ReadRegistrySettings(TargetName); @@ -113,27 +111,25 @@ public async Task RegistryProcessor_ReadRegistrySettings_SecondStrategySuccessfu const string failureReason = "No such host is known."; const uint minClientSecValue = 536870912; - var attempts = new List> - { - new(typeof(DotNetWmiRegistryStrategy)) - { + 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( - new StrategyExecutorResult { - FailureAttempts = attempts, - Results = [ - new RegistryQueryResult(@"SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0", "NtlmMinClientSec", minClientSecValue, RegistryValueKind.DWord, true) - ], - WasSuccessful = true - } - ); + .ReturnsAsync(executorResult); var results = await _registryProcessor.ReadRegistrySettings(TargetName); @@ -152,6 +148,7 @@ public async Task RegistryProcessor_ReadRegistrySettings_SecondStrategySuccessfu [Fact] public async Task RegistryProcessor_ReadRegistrySettings_HandlesException() { var exception = new Exception("test exception"); + _mockStrategyExecutor.Setup(se => se.CollectAsync( It.IsAny(), It.IsAny>(), @@ -162,7 +159,7 @@ public async Task RegistryProcessor_ReadRegistrySettings_HandlesException() { //Validate result Assert.False(results.Collected); - Assert.Equal(results.FailureReason, exception.ToString()); + Assert.Equal(exception.ToString(), results.FailureReason); //Validate logs _mockLogger.VerifyLogContains(LogLevel.Error, $"Unhandled Registry Processor exception {TargetName}: {exception}"); @@ -173,27 +170,28 @@ public async Task RegistryProcessor_ReadRegistrySettings_HandlesException() { 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( - 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, - } - ); + .ReturnsAsync(executorResult); var results = await _registryProcessor.ReadRegistrySettings(TargetName); @@ -216,15 +214,15 @@ public async Task RegistryProcessor_ReadRegistrySettings_SetsAllValues() { [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( - new StrategyExecutorResult { - WasSuccessful = false - } - ); + .ReturnsAsync(executorResult); var results = await _registryProcessor.ReadRegistrySettings(TargetName); From 92f735167e85ac08745f2600c4fe582e4e07cdb5 Mon Sep 17 00:00:00 2001 From: Lucas Falslev Date: Mon, 23 Feb 2026 09:22:08 -0700 Subject: [PATCH 09/10] add strategyExecutor tests --- RPCTest/RPCTest.csproj | 22 +++ RPCTest/Registry/StrategyExecutorTests.cs | 130 ++++++++++++++++++ SharpHoundCommon.sln | 6 + .../Registry/StrategyExecutor.cs | 12 +- 4 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 RPCTest/RPCTest.csproj create mode 100644 RPCTest/Registry/StrategyExecutorTests.cs diff --git a/RPCTest/RPCTest.csproj b/RPCTest/RPCTest.csproj new file mode 100644 index 000000000..18cc82477 --- /dev/null +++ b/RPCTest/RPCTest.csproj @@ -0,0 +1,22 @@ + + + + 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/SharpHoundRPC/Registry/StrategyExecutor.cs b/src/SharpHoundRPC/Registry/StrategyExecutor.cs index 55239aba3..a95f5fd89 100644 --- a/src/SharpHoundRPC/Registry/StrategyExecutor.cs +++ b/src/SharpHoundRPC/Registry/StrategyExecutor.cs @@ -36,18 +36,22 @@ public async Task> CollectAsync( Results = results, FailureAttempts = attempts, WasSuccessful = true, - SuccessfulStrategy = strategy.GetType(), + 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, }; } } From ad7495d2dd5517b864674f7fce0cc46ea03ecef4 Mon Sep 17 00:00:00 2001 From: Lucas Falslev Date: Mon, 23 Feb 2026 09:47:47 -0700 Subject: [PATCH 10/10] .net test dependency --- RPCTest/RPCTest.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/RPCTest/RPCTest.csproj b/RPCTest/RPCTest.csproj index 18cc82477..8638c026d 100644 --- a/RPCTest/RPCTest.csproj +++ b/RPCTest/RPCTest.csproj @@ -8,6 +8,7 @@ + all