Skip to content

Comments

Improve NTLM Collection Logging BED-7374#196

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

Improve NTLM Collection Logging BED-7374#196
lrfalslev wants to merge 4 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, mapping, concurrency, and resource usage; introduced deferred initialization and updated event wiring to align lifecycle with 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

Reworked RegistryProcessor creation and lifecycle in ObjectProcessors to use Lazy initialization with a StrategyExecutor, per-domain GetOrAdd caching, and ComputerStatusEvent subscribe/detach; added a commented reference block in Sharphound.csproj. No other behavioral changes reported.

Changes

Cohort / File(s) Summary
Project configuration
Sharphound.csproj
Added a commented-out ItemGroup block referencing SharpHoundRPC (no active build references changed).
Registry processor lifecycle & wiring
src/Runtime/ObjectProcessors.cs
Switched internal map to ConcurrentDictionary<string, Lazy<RegistryProcessor>>; construct RegistryProcessor with a StrategyExecutor; use GetOrAdd + Lazy to defer creation; subscribe new instances to ComputerStatusEvent; detach handlers in ClearEventHandlers; added using SharpHoundRPC.Registry.

Sequence Diagram(s)

sequenceDiagram
    participant OP as ObjectProcessors
    participant Map as RegistryProcessorMap
    participant LazyRP as Lazy<RegistryProcessor>
    participant RP as RegistryProcessor
    participant Exec as StrategyExecutor

    OP->>Map: GetOrAdd(domain, Lazy{ new RP(null, Exec, domain) })
    alt Lazy already created
        Map-->>OP: Lazy (value created) -> RP
        OP->>RP: ReadRegistrySettings(...)
    else Lazy not yet created
        Map-->>OP: Lazy (not created)
        OP->>LazyRP: Value (creates RP with Exec)
        LazyRP-->>RP: instantiate RP(null, Exec, domain)
        RP->>OP: subscribe ComputerStatusEvent
        OP->>RP: ReadRegistrySettings(...)
    end
    OP->>RP: Detach ComputerStatusEvent in ClearEventHandlers()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • definitelynotagoblin
  • mykeelium

Poem

"🐇 I hop through code where registries sleep,
Lazy seeds planted, creations to keep.
Executors whisper when domains call,
Events attach gently — then released all.
A commented reference nods from the heap."

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Improve NTLM Collection Logging BED-7374' partially relates to the changeset; it emphasizes logging improvements, but the actual changes focus on lazy initialization of registry processors with event handling, which is an implementation detail supporting those improvements.
Description check ✅ Passed The description covers motivation, testing references, and change type, but the 'How Has This Been Tested?' section only links to an external PR without detailing local testing specifics, and the testing checklist item remains unchecked despite the claim that all tests passed.

✏️ 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.

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 382-386: The RegistryProcessor is being constructed with the wrong
signature in the Lazy factory: replace the three-argument call new
RegistryProcessor(null, new StrategyExecutor(), resolvedSearchResult.Domain)
with the two-argument constructor that matches RegistryProcessor(ILogger log,
string domain); i.e., call new RegistryProcessor(null,
resolvedSearchResult.Domain) inside the Lazy<RegistryProcessor> factory and keep
the existing ComputerStatusEvent hookup (HandleCompStatusEvent) unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80c17fb and 959a27e.

📒 Files selected for processing (1)
  • src/Runtime/ObjectProcessors.cs

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