Skip to content

Comments

Improve NTLM Collection Logging BED-7374#196

Open
lrfalslev wants to merge 3 commits into2.Xfrom
lfalslev/bed-7374
Open

Improve NTLM Collection Logging BED-7374#196
lrfalslev wants to merge 3 commits into2.Xfrom
lfalslev/bed-7374

Conversation

@lrfalslev
Copy link
Contributor

@lrfalslev lrfalslev commented Feb 20, 2026

Description

Add CompStatus Logging To Registry Processor
RunLog on Collection Failure
RegistryProcessor Unit Tests

Motivation and Context

BED-7374

How Has This Been Tested?

See SHCommon PR #272

Types of changes

  • Chore (a change that does not modify the application functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • Documentation updates are needed, and have been made accordingly.
  • [] I have added and/or updated tests to cover my changes.
  • All new and existing tests passed.
  • My changes include a database migration.

Summary by CodeRabbit

  • Chores
    • Improved internal processor initialization and lifecycle to enhance registry status handling and mapping; updated initialization to support strategy execution.
    • No runtime behavior changes; no user-facing functionality altered.

@lrfalslev lrfalslev self-assigned this Feb 20, 2026
@lrfalslev lrfalslev added the enhancement New feature or request label Feb 20, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Walkthrough

Updated project comments in the MSBuild file and changed RegistryProcessor creation/lifecycle in ObjectProcessors to use a StrategyExecutor, register per-domain processors via GetOrAdd, and subscribe/detach ComputerStatusEvent handlers.

Changes

Cohort / File(s) Summary
Project configuration
Sharphound.csproj
Added a new block of commented-out references for SharpHoundRPC in an ItemGroup; no active build references changed.
Registry processor lifecycle & wiring
src/Runtime/ObjectProcessors.cs
Introduced using SharpHoundRPC.Registry; changed RegistryProcessor construction to include a StrategyExecutor; switched to GetOrAdd per-domain creation; subscribe new processors to ComputerStatusEvent; detach registry processors in ClearEventHandlers; updated ReadNTLMRegistryData to use the GetOrAdd path.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as ObjectProcessors
    participant Map as RegistryProcessorMap
    participant RP as RegistryProcessor
    participant Exec as StrategyExecutor
    Caller->>Map: GetOrAdd(domain)
    alt processor exists
        Map-->>Caller: existing RP
        Caller->>RP: ReadRegistrySettings(...)
    else create new
        Map-->>RP: create RP(null, Exec, domain)
        RP->>Exec: (held internally)
        RP-->>Map: store RP
        Caller->>RP: subscribe ComputerStatusEvent
        Caller->>RP: ReadRegistrySettings(...)
    end
    Caller->>RP: Detach in ClearEventHandlers()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • definitelynotagoblin
  • mykeelium

Poem

"🐇 I hop through code with keen delight,
A StrategyExecutor tucked in tight.
Registry processors gather by domain,
Events subscribed — then freed again.
Comments rest easy in the project light."

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Improve NTLM Collection Logging BED-7374' is partially related to the changeset but lacks specificity about the main technical changes. Consider a more specific title that reflects the core changes, such as 'Add RegistryProcessor lifecycle management and CompStatus logging' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description covers key sections (motivation, testing reference) but lacks detailed explanation of the actual technical changes and the testing checkbox was not properly marked.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch lfalslev/bed-7374

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Runtime/ObjectProcessors.cs (1)

377-386: ⚠️ Potential issue | 🟠 Major

TOCTOU race on _registryProcessorMap: use GetOrAdd to avoid leaked processors and duplicate event subscriptions.

TryGetValue + TryAdd on a ConcurrentDictionary is not atomic. If two threads process computers with the same DomainSid concurrently, both can fail TryGetValue, both create a RegistryProcessor with a subscribed event handler, and only one wins TryAdd. The losing thread's processor is used but never tracked in the map, so ClearEventHandlers (Line 89-91) will never unsubscribe it — leaking the event handler.

Additionally, new StrategyExecutor() is allocated per race loser unnecessarily.

Replace with GetOrAdd (or the Lazy<T> variant if RegistryProcessor construction has side effects you want to deduplicate):

Proposed fix
         if (_methods.HasFlag(CollectionMethod.NTLMRegistry)) {
             await _context.DoDelay();
-            if (_registryProcessorMap.TryGetValue(resolvedSearchResult.DomainSid, out var processor)) {
-                ret.NTLMRegistryData = await processor.ReadRegistrySettings(resolvedSearchResult.DisplayName);
-            } else {
-                var newProcessor = new RegistryProcessor(null, new StrategyExecutor(), resolvedSearchResult.Domain);
-                newProcessor.ComputerStatusEvent += HandleCompStatusEvent;
-                _registryProcessorMap.TryAdd(resolvedSearchResult.DomainSid, newProcessor);
-                ret.NTLMRegistryData = await newProcessor.ReadRegistrySettings(resolvedSearchResult.DisplayName);
-            }
+            var processor = _registryProcessorMap.GetOrAdd(resolvedSearchResult.DomainSid, _ => {
+                var p = new RegistryProcessor(null, new StrategyExecutor(), resolvedSearchResult.Domain);
+                p.ComputerStatusEvent += HandleCompStatusEvent;
+                return p;
+            });
+            ret.NTLMRegistryData = await processor.ReadRegistrySettings(resolvedSearchResult.DisplayName);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Runtime/ObjectProcessors.cs` around lines 377 - 386, The code has a
TOCTOU on _registryProcessorMap around the NTLMRegistry path: replace the
TryGetValue/TryAdd pattern with ConcurrentDictionary.GetOrAdd (or GetOrAdd with
Lazy<RegistryProcessor>) so RegistryProcessor instances and their
ComputerStatusEvent subscription (HandleCompStatusEvent) are created and
attached atomically; ensure you only subscribe HandleCompStatusEvent once on the
instance returned from GetOrAdd and avoid allocating new StrategyExecutor() for
losers by creating the RegistryProcessor only inside the factory passed to
GetOrAdd (or Lazy.Value), then call ReadRegistrySettings on the returned
processor to populate ret.NTLMRegistryData.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/Runtime/ObjectProcessors.cs`:
- Around line 377-386: The code has a TOCTOU on _registryProcessorMap around the
NTLMRegistry path: replace the TryGetValue/TryAdd pattern with
ConcurrentDictionary.GetOrAdd (or GetOrAdd with Lazy<RegistryProcessor>) so
RegistryProcessor instances and their ComputerStatusEvent subscription
(HandleCompStatusEvent) are created and attached atomically; ensure you only
subscribe HandleCompStatusEvent once on the instance returned from GetOrAdd and
avoid allocating new StrategyExecutor() for losers by creating the
RegistryProcessor only inside the factory passed to GetOrAdd (or Lazy.Value),
then call ReadRegistrySettings on the returned processor to populate
ret.NTLMRegistryData.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Runtime/ObjectProcessors.cs`:
- Around line 379-383: The current use of _registryProcessorMap.GetOrAdd with a
factory can create multiple RegistryProcessor instances that subscribe to
ComputerStatusEvent and leak if discarded; change the dictionary to store
Lazy<RegistryProcessor> instead, update the GetOrAdd call to add a new Lazy that
constructs the RegistryProcessor and subscribes to ComputerStatusEvent inside
the Lazy factory (using resolvedSearchResult.Domain and DomainSid), and update
any code that reads processors to use .Value (e.g., when accessing the
processor) as well as modify ClearEventHandlers to iterate the stored Lazy
values and unsubscribe from each Lazy.Value.ComputerStatusEvent (or otherwise
access Value once) so that construction and subscription happen exactly once per
key.

Comment on lines +379 to +383
var processor = _registryProcessorMap.GetOrAdd(resolvedSearchResult.DomainSid, _ => {
var newProcessor = new RegistryProcessor(null, new StrategyExecutor(), resolvedSearchResult.Domain);
newProcessor.ComputerStatusEvent += HandleCompStatusEvent;
return newProcessor;
});
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:

git ls-files | grep -E "ObjectProcessors|RegistryProcessor"

Repository: SpecterOps/SharpHound

Length of output: 96


🏁 Script executed:

head -50 src/Runtime/ObjectProcessors.cs | cat -n

Repository: SpecterOps/SharpHound

Length of output: 2901


🏁 Script executed:

sed -n '35,55p' src/Runtime/ObjectProcessors.cs | cat -n

Repository: SpecterOps/SharpHound

Length of output: 1728


🏁 Script executed:

sed -n '370,395p' src/Runtime/ObjectProcessors.cs | cat -n

Repository: SpecterOps/SharpHound

Length of output: 1664


🏁 Script executed:

sed -n '85,100p' src/Runtime/ObjectProcessors.cs | cat -n

Repository: SpecterOps/SharpHound

Length of output: 998


🏁 Script executed:

rg -n "class RegistryProcessor" --type=cs

Repository: SpecterOps/SharpHound

Length of output: 47


🏁 Script executed:

rg -n "class StrategyExecutor" --type=cs

Repository: SpecterOps/SharpHound

Length of output: 47


🏁 Script executed:

rg -n "public RegistryProcessor" --type=cs -A 5

Repository: SpecterOps/SharpHound

Length of output: 47


🏁 Script executed:

fd -type f -name "*.cs" | xargs rg -l "class RegistryProcessor" 2>/dev/null

Repository: SpecterOps/SharpHound

Length of output: 237


🏁 Script executed:

fd -type f -name "*.cs" | xargs rg -l "class StrategyExecutor" 2>/dev/null

Repository: SpecterOps/SharpHound

Length of output: 237


🏁 Script executed:

find . -name "*.csproj" -o -name "*.sln" | head -5

Repository: SpecterOps/SharpHound

Length of output: 101


🏁 Script executed:

cat .csproj 2>/dev/null || cat *.csproj 2>/dev/null | head -100

Repository: SpecterOps/SharpHound

Length of output: 2726


🏁 Script executed:

git ls-files | grep -i "csproj\|packages"

Repository: SpecterOps/SharpHound

Length of output: 82


🏁 Script executed:

rg -n "RegistryProcessor" --type-add 'src:*.cs'

Repository: SpecterOps/SharpHound

Length of output: 980


🏁 Script executed:

rg -n "StrategyExecutor" --type-add 'src:*.cs'

Repository: SpecterOps/SharpHound

Length of output: 221


🏁 Script executed:

rg -n "GetOrAdd" --type-add 'src:*.cs'

Repository: SpecterOps/SharpHound

Length of output: 202


🏁 Script executed:

rg -n "ClearEventHandlers" --type-add 'src:*.cs'

Repository: SpecterOps/SharpHound

Length of output: 297


🏁 Script executed:

rg -n "_registryProcessorMap" --type-add 'src:*.cs'

Repository: SpecterOps/SharpHound

Length of output: 454


🏁 Script executed:

sed -n '375,390p' src/Runtime/ObjectProcessors.cs | cat -n

Repository: SpecterOps/SharpHound

Length of output: 971


🏁 Script executed:

sed -n '77,95p' src/Runtime/ObjectProcessors.cs | cat -n

Repository: SpecterOps/SharpHound

Length of output: 1253


GetOrAdd factory may run multiple times for the same key, leaking processors with dangling event subscriptions.

ConcurrentDictionary.GetOrAdd(key, factory) does not guarantee the factory executes only once per key under concurrent access. If two threads hit this for the same DomainSid simultaneously, both may create a RegistryProcessor and subscribe to ComputerStatusEvent, but only one is stored. The discarded instance retains its subscription — it is never unsubscribed in ClearEventHandlers (which only iterates the map) and may produce duplicate status events.

Wrap the value in Lazy<T> so the actual construction + subscription happens exactly once:

Proposed fix using Lazy<T>

Change the field declaration (line 44):

-private readonly ConcurrentDictionary<string, RegistryProcessor> _registryProcessorMap = new();
+private readonly ConcurrentDictionary<string, Lazy<RegistryProcessor>> _registryProcessorMap = new();

Update the GetOrAdd call (lines 379–384):

-var processor = _registryProcessorMap.GetOrAdd(resolvedSearchResult.DomainSid, _ => {
-    var newProcessor = new RegistryProcessor(null, new StrategyExecutor(), resolvedSearchResult.Domain);
-    newProcessor.ComputerStatusEvent += HandleCompStatusEvent;
-    return newProcessor;
-});
-ret.NTLMRegistryData = await processor.ReadRegistrySettings(resolvedSearchResult.DisplayName);
+var processor = _registryProcessorMap.GetOrAdd(resolvedSearchResult.DomainSid, _ =>
+    new Lazy<RegistryProcessor>(() => {
+        var newProcessor = new RegistryProcessor(null, new StrategyExecutor(), resolvedSearchResult.Domain);
+        newProcessor.ComputerStatusEvent += HandleCompStatusEvent;
+        return newProcessor;
+    })).Value;
+ret.NTLMRegistryData = await processor.ReadRegistrySettings(resolvedSearchResult.DisplayName);

Update ClearEventHandlers (lines 89–91):

-foreach (var registryProcessor in _registryProcessorMap.Values) {
-    registryProcessor.ComputerStatusEvent -= HandleCompStatusEvent;
+foreach (var lazy in _registryProcessorMap.Values) {
+    if (lazy.IsValueCreated) {
+        lazy.Value.ComputerStatusEvent -= HandleCompStatusEvent;
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Runtime/ObjectProcessors.cs` around lines 379 - 383, The current use of
_registryProcessorMap.GetOrAdd with a factory can create multiple
RegistryProcessor instances that subscribe to ComputerStatusEvent and leak if
discarded; change the dictionary to store Lazy<RegistryProcessor> instead,
update the GetOrAdd call to add a new Lazy that constructs the RegistryProcessor
and subscribes to ComputerStatusEvent inside the Lazy factory (using
resolvedSearchResult.Domain and DomainSid), and update any code that reads
processors to use .Value (e.g., when accessing the processor) as well as modify
ClearEventHandlers to iterate the stored Lazy values and unsubscribe from each
Lazy.Value.ComputerStatusEvent (or otherwise access Value once) so that
construction and subscription happen exactly once per key.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant