Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/CommonLib/Processors/ComputerAvailability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
52 changes: 40 additions & 12 deletions src/CommonLib/Processors/RegistryProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RegistryQueryResult, RegistryQuery>[] _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),
Expand Down Expand Up @@ -50,12 +54,13 @@ public RegistryProcessor(ILogger log, string domain) {
];
}

public event ComputerStatusDelegate ComputerStatusEvent;

public async Task<APIResult<RegistryData>> 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));

Expand All @@ -65,6 +70,32 @@ public async Task<APIResult<RegistryData>> 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<RegistryData>.Failure(msg);
}

await SendComputerStatus(new CSVComputerStatus
{
Task = $"{nameof(ReadRegistrySettings)} - {collectedData.SuccessfulStrategy?.Name ?? ""}",
ComputerName = targetMachine,
Status = CSVComputerStatus.StatusSuccess
});
Comment on lines 92 to 97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and examine StrategyExecutorResult.cs
echo "=== Locating StrategyExecutorResult.cs ==="
fd "StrategyExecutorResult.cs" -type f

echo ""
echo "=== Locating StrategyExecutor.cs ==="
fd "StrategyExecutor.cs" -type f

echo ""
echo "=== Locating RegistryProcessor.cs ==="
fd "RegistryProcessor.cs" -type f

Repository: SpecterOps/SharpHoundCommon

Length of output: 720


🏁 Script executed:

#!/bin/bash
# Examine the StrategyExecutorResult.cs file
if [ -f "$(fd 'StrategyExecutorResult.cs' -type f | head -1)" ]; then
  cat "$(fd 'StrategyExecutorResult.cs' -type f | head -1)"
fi

Repository: SpecterOps/SharpHoundCommon

Length of output: 241


🏁 Script executed:

#!/bin/bash
# Examine StrategyExecutor.cs - look for success path and WasSuccessful assignments
echo "=== StrategyExecutor.cs - Full file (if manageable) ==="
fd "StrategyExecutor.cs" -type f -exec wc -l {} \;
fd "StrategyExecutor.cs" -type f -exec head -200 {} \;

echo ""
echo "=== Searching for WasSuccessful assignments ==="
rg -n "WasSuccessful\s*=" --type cs

Repository: SpecterOps/SharpHoundCommon

Length of output: 1154


🏁 Script executed:

#!/bin/bash
# Examine RegistryProcessor.cs around lines 92-97
echo "=== RegistryProcessor.cs lines 85-105 ==="
sed -n '85,105p' src/CommonLib/Processors/RegistryProcessor.cs

echo ""
echo "=== Search for _strategies field in RegistryProcessor ==="
rg -n "_strategies" src/CommonLib/Processors/RegistryProcessor.cs -A2 -B2

Repository: SpecterOps/SharpHoundCommon

Length of output: 2263


🏁 Script executed:

#!/bin/bash
# Examine StrategyExecutorResult.cs
echo "=== StrategyExecutorResult.cs ==="
cat src/SharpHoundRPC/Registry/StrategyExecutorResult.cs

Repository: SpecterOps/SharpHoundCommon

Length of output: 457


🏁 Script executed:

#!/bin/bash
# Examine StrategyExecutor.cs - look at the success path around line 38
echo "=== StrategyExecutor.cs context around WasSuccessful = true ==="
sed -n '30,50p' src/SharpHoundRPC/Registry/StrategyExecutor.cs

echo ""
echo "=== Full StrategyExecutor.cs (check file size) ==="
wc -l src/SharpHoundRPC/Registry/StrategyExecutor.cs

Repository: SpecterOps/SharpHoundCommon

Length of output: 982


🏁 Script executed:

#!/bin/bash
# Get more context from StrategyExecutor.cs - check the full file
cat src/SharpHoundRPC/Registry/StrategyExecutor.cs

Repository: SpecterOps/SharpHoundCommon

Length of output: 1991


🏁 Script executed:

#!/bin/bash
# Examine StrategyResult to see what info is available
echo "=== StrategyResult.cs or related ==="
fd "StrategyResult.cs" -type f

echo ""
echo "=== Search for StrategyResult definition and StrategyType ==="
rg -n "class StrategyResult|StrategyType" src/SharpHoundRPC/Registry/ -A3

Repository: SpecterOps/SharpHoundCommon

Length of output: 1705


🏁 Script executed:

#!/bin/bash
# Verify the _strategies array definition in RegistryProcessor to confirm ordering assumptions
echo "=== RegistryProcessor._strategies initialization ==="
sed -n '27,37p' src/CommonLib/Processors/RegistryProcessor.cs

Repository: SpecterOps/SharpHoundCommon

Length of output: 780


🏁 Script executed:

#!/bin/bash
# Check if there are any other usages of FailureAttempts?.Count() that might have similar issues
echo "=== Search for FailureAttempts usage ==="
rg "FailureAttempts" src/ -n -A2 -B2

Repository: SpecterOps/SharpHoundCommon

Length of output: 3475


Fragile strategy-name resolution via index arithmetic.

Line 94 infers the successful strategy by indexing _strategies[collectedData.FailureAttempts?.Count() ?? 0]. This silently couples RegistryProcessor to the internal iteration order of StrategyExecutor and the exact composition of _strategies. If StrategyExecutor ever skips a strategy without recording a failure attempt, reorders strategies, or if _strategies is modified independently, this will silently report the wrong strategy name—or throw IndexOutOfRangeException.

The successful strategy type is available in StrategyExecutor.CollectAsync at the point of success but is never captured in the result. Add a SuccessfulStrategyType property to StrategyExecutorResult<T> and set it on the success path, then use that instead of array indexing.

Proposed fix in StrategyExecutorResult

In StrategyExecutorResult.cs:

 public class StrategyExecutorResult<T> {
     public IEnumerable<T>? Results { get; set; } = null;
     public IEnumerable<StrategyResult<T>>? FailureAttempts { get; set; } = null;
     public bool WasSuccessful = false;
+    public Type? SuccessfulStrategyType { get; set; } = null;
 }

In StrategyExecutor.cs, set it on the success path:

 return new StrategyExecutorResult<T> {
     Results = results,
     FailureAttempts = attempts,
-    WasSuccessful = true
+    WasSuccessful = true,
+    SuccessfulStrategyType = strategy.GetType()
 };

Then in RegistryProcessor.cs:

 await SendComputerStatus(new CSVComputerStatus
 {
-    Task = $"{nameof(ReadRegistrySettings)} - {_strategies[collectedData.FailureAttempts?.Count() ?? 0].GetType().Name}",
+    Task = $"{nameof(ReadRegistrySettings)} - {collectedData.SuccessfulStrategyType?.Name ?? "Unknown"}",
     ComputerName = targetMachine,
     Status = CSVComputerStatus.StatusSuccess
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CommonLib/Processors/RegistryProcessor.cs` around lines 92 - 97, The
success message in RegistryProcessor (inside ReadRegistrySettings) should not
infer the successful strategy via
_strategies[collectedData.FailureAttempts?.Count() ?? 0]; add a
SuccessfulStrategyType property to StrategyExecutorResult<T>, set that property
to the strategy's GetType() on the success path inside
StrategyExecutor.CollectAsync when a strategy succeeds, and update
RegistryProcessor to use collectedData.SuccessfulStrategyType?.Name (or
fallback) for the Task string instead of indexing _strategies; this removes
fragile coupling to _strategies ordering and prevents IndexOutOfRangeException.


foreach (var key in collectedData.Results ?? []) {
if (!key.ValueExists)
continue;
Expand Down Expand Up @@ -101,13 +132,6 @@ public async Task<APIResult<RegistryData>> 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<RegistryData>.Failure(msg);
}

return APIResult<RegistryData>.Success(output);
} catch (Exception ex) {
_log.LogError(
Expand All @@ -118,4 +142,8 @@ public async Task<APIResult<RegistryData>> ReadRegistrySettings(string targetMac
return APIResult<RegistryData>.Failure(ex.ToString());
}
}

private async Task SendComputerStatus(CSVComputerStatus status) {
if (ComputerStatusEvent is not null) await ComputerStatusEvent.Invoke(status);
}
}
4 changes: 3 additions & 1 deletion src/SharpHoundRPC/Registry/DotNetWmiRegistryStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ public class DotNetWmiRegistryStrategy : ICollectionStrategy<RegistryQueryResult
/// </summary>
public bool UseKerberos { get; set; } = true;


/// <summary>
/// Creates a new WMI registry strategy
/// </summary>
Expand All @@ -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);
Expand Down
2 changes: 0 additions & 2 deletions src/SharpHoundRPC/Registry/NativeUtils.cs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
3 changes: 1 addition & 2 deletions src/SharpHoundRPC/Registry/RemoteRegistryStrategy.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -38,7 +38,6 @@ public async Task<IEnumerable<RegistryQueryResult>> 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<RegistryQueryResult>();

Expand Down
17 changes: 11 additions & 6 deletions src/SharpHoundRPC/Registry/StrategyExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ namespace SharpHoundRPC.Registry {
using System.Collections.Generic;
using System.Threading.Tasks;


public class StrategyExecutor {
public interface IStrategyExecutor
{
Task<StrategyExecutorResult<T>> CollectAsync<T, TQuery>(
string targetMachine,
IEnumerable<TQuery> queries,
IEnumerable<ICollectionStrategy<T, TQuery>> strategies);
}

public class StrategyExecutor : IStrategyExecutor {
public async Task<StrategyExecutorResult<T>> CollectAsync<T, TQuery>(
string targetMachine,
IEnumerable<TQuery> queries,
Expand All @@ -25,13 +32,11 @@ public async Task<StrategyExecutorResult<T>> CollectAsync<T, TQuery>(
try {
var results = await strategy.ExecuteAsync(targetMachine, queries).ConfigureAwait(false);

attempt.WasSuccessful = true;
attempt.Results = results;

return new StrategyExecutorResult<T> {
Results = results,
FailureAttempts = attempts,
WasSuccessful = true
WasSuccessful = true,
SuccessfulStrategy = strategy.GetType(),
};
} catch (Exception ex) {
attempt.FailureReason = $"Collector failed: {ex.Message}.\nInner Exception: {ex.InnerException}";
Expand Down
12 changes: 8 additions & 4 deletions src/SharpHoundRPC/Registry/StrategyExecutorResult.cs
Original file line number Diff line number Diff line change
@@ -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<T> {
public IEnumerable<T>? Results { get; set; } = null;
public IEnumerable<StrategyResult<T>>? FailureAttempts { get; set; } = null;
public bool WasSuccessful = false;
public Type? SuccessfulStrategy { get; set; } = null;
}
#nullable disable
}
}

#nullable disable
12 changes: 12 additions & 0 deletions test/unit/Facades/MockExtentions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,16 @@ public static void VerifyLog<T>(this Mock<ILogger<T>> mockLogger, LogLevel logLe
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}

public static void VerifyNoLogs<T>(this Mock<ILogger<T>> mockLogger, LogLevel logLevel)
{
mockLogger.Verify(
x => x.Log(
logLevel,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()),
Times.Never);
}
}
Loading
Loading