Skip to content

Conversation

@zinic
Copy link
Contributor

@zinic zinic commented Jan 15, 2026

Description

TODO

Motivation and Context

Resolves BED-7147

Why is this change required? What problem does it solve?

How Has This Been Tested?

Please describe in detail how you tested your changes.
Include details of your testing environment, and the tests you ran to
see how your change affects other areas of the code, etc.

Screenshots (optional):

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)
  • Database Migrations

Checklist:

Summary by CodeRabbit

Release Notes

  • Chores

    • Updated multiple dependencies to latest versions, including security and performance improvements across cryptography, tooling, and core libraries.
  • Refactor

    • Restructured internal analysis data processing to improve efficiency and maintainability of group membership and access control analysis.
    • Removed deprecated analysis function and introduced improved data structures for local group and remote desktop access analysis.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 15, 2026

Walkthrough

Comprehensive refactoring replacing PathAggregator-based group expansion with a new LocalGroupData structure throughout AD post-processing. Introduces LocalGroupData, CanRDPData, and CanRDPComputerData to centralize group membership and RDP data. Updates ADCS ESC implementations and post-processing functions to use reachability caches. Removes ResolveAllGroupMemberships; updates go.mod dependencies including dawgs v0.3.2→v0.4.2.

Changes

Cohort / File(s) Summary
Core Data Structure Introduction
packages/go/analysis/ad/post.go
Major refactoring introducing LocalGroupData, CanRDPData, and CanRDPComputerData structs. Replaces groupExpansions with LocalGroupData throughout PostSyncLAPSPassword and PostDCSync paths. Adds new fetchers (FetchLocalGroupData, FetchCanRDPData, FetchComputerIDs, FetchAdminGroups) and helper methods for RDP data handling. Extensive logic rework (+328/-280 lines).
ADCS ESC Multi-Parameter Updates
packages/go/analysis/ad/esc1.go, esc3.go, esc4.go, esc6.go, esc9.go, esc10.go, esc13.go
Consistently replace groupExpansions parameter (impact.PathAggregator) with localGroupData (*LocalGroupData) across all ESC post-processing functions. Update CalculateCrossProductNodeSets calls to use LocalGroupData. Remove impact package imports.
ADCS Infrastructure
packages/go/analysis/ad/adcs.go, adcscache.go
adcs.go: Update PostADCS and processEnterpriseCAWithValidCertChainToDomain signatures to accept LocalGroupData. adcscache.go: Add defer measure.ContextMeasure() timing instrumentation; remove informational log.
Shared ESC Helpers
packages/go/analysis/ad/esc_shared.go
Update expandNodeSliceToBitmapWithoutGroups and getVictimBitmap signatures to use *algo.ReachabilityCache and *LocalGroupData respectively. Replace cardinality-based checks with OrReach-based expansion logic.
Group Membership & Cache
packages/go/analysis/ad/membership.go, ntlm.go, owns.go
membership.go: Remove ResolveAllGroupMemberships function and related impact imports. ntlm.go: Replace NTLMCache.GroupExpansions field with LocalGroupData; update NewNTLMCache, PostNTLM, FetchProtectedUsersMappedToDomains signatures. owns.go: Update PostOwnsAndWriteOwner to use LocalGroupData; change FetchAdminGroupIds to use *algo.ReachabilityCache.
New Local Groups Post-Processing
packages/go/analysis/ad/local_groups.go
New file introducing PostCanRDP and PostLocalGroups functions for concurrent local group analysis. Both leverage LocalGroupData and coordinate worker pools with batched channels for post-relationship generation.
Dependency Updates
go.mod
Update github.com/specterops/dawgs from v0.3.2 to v0.4.2; bump cuelang.org/go, pgx/v5, golang.org/x packages (crypto, mod, text, tools, exp, net, sys, telemetry).
Pruning Pipeline
cmd/api/src/daemons/datapipe/datapipe.go
Remove s.pipeline.PruneData invocation from pruningTicker case in Daemon.Start.
AD Analysis Post Integration
cmd/api/src/analysis/ad/post.go
Replace ExpandAllRDPLocalGroups calls with FetchLocalGroupData; update variable names and dependent analysis calls (PostDCSync, PostSyncLAPSPassword, PostLocalGroups, PostCanRDP, PostADCS, PostOwnsAndWriteOwner, PostNTLM) to accept localGroupData; reorder final stats merge.
Test Integrations
cmd/api/src/analysis/ad/ad_integration_test.go, adcs_integration_test.go, ntlm_integration_test.go; cmd/api/src/analysis/analysis_integration_test.go, membership_integration_test.go; cmd/api/src/analysis/post_integration_test.go
Update test harness setup and test functions to use FetchLocalGroupData instead of ExpandAllRDPLocalGroups. Rename test functions (e.g., TestResolveAllGroupMemberships→TestResolveReachOfGroupMembershipComponents). Update cross-product test flow to use new FetchLocalGroupData and CalculateCrossProductNodeSets signatures.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

enhancement, api

Suggested reviewers

  • superlinkx
  • urangel
  • ddlees
  • kpowderly

Poem

🐰 Hops with glee through code refactored clean,
LocalGroupData reigns where PathAggregators convene,
ADCS ESC signatures dance in line,
Reachability caches make membership shine!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is largely incomplete with TODO placeholders and lacks implementation details, testing information, and proper checkbox selections. Complete the Description section with implementation details, fill in testing methodology, select appropriate change types, and verify all checklist items are addressed before merging.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main objective: a rework of local group post-processing functionality related to BED-7147.

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

✨ Finishing touches
  • 📝 Generate docstrings

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
Contributor

@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: 9

Caution

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

⚠️ Outside diff range comments (3)
packages/go/analysis/ad/esc_shared.go (1)

303-334: Dead code and always-false condition - logic appears incomplete.

The isGroup variable is declared at line 314 but is never set to true because the code that would set it (lines 316-320) is commented out. This means:

  1. isGroup is always false
  2. The condition if !isGroup at line 322 is always true
  3. Every member is unconditionally added to the bitmap

If filtering out groups is no longer needed, simplify the code by removing the dead logic. If it is needed, uncomment and fix the implementation.

♻️ Option 1: Remove dead code if groups should be included
 func expandNodeSliceToBitmapWithoutGroups(nodes []*graph.Node, groupExpansions *algo.ReachabilityCache) cardinality.Duplex[uint64] {
 	var (
 		bitmap  = cardinality.NewBitmap64()
 		scratch = cardinality.NewBitmap64()
 	)
 
 	for _, controller := range nodes {
 		if controller.Kinds.ContainsOneOf(ad.Group) {
 			groupExpansions.OrReachability(controller.ID.Uint64(), graph.DirectionInbound, scratch)
-
-			scratch.Each(func(memberID uint64) bool {
-				isGroup := false
-
-				// groupExpansions.Digraph.EachAdjacentNode(memberID, graph.DirectionInbound, func(adjacent uint64) bool {
-				// 	// If the expanded member of the controller group has an adjacent inbound node then it must be a group
-				// 	isGroup = true
-				// 	return false
-				// })
-
-				if !isGroup {
-					bitmap.Add(memberID)
-				}
-
-				return true
-			})
+			bitmap.Or(scratch)
+			scratch.Clear()
 		} else {
 			bitmap.Add(controller.ID.Uint64())
 		}
 	}
 
 	return bitmap
 }
cmd/api/src/daemons/datapipe/datapipe.go (1)

72-104: Remove early return or guard it with a feature flag.

Lines 73-74 introduce an early return that makes the entire daemon loop (lines 76-103) unreachable dead code. This disables critical pipeline operations:

  • Timer initialization and cleanup
  • Pruning ticker
  • Pipeline lifecycle (Start, PruneData, DeleteData, IngestTasks)
  • Context cancellation handling

There is no feature flag, build tag, or TODO comment explaining the temporary nature. If this is intentional for testing the LocalGroupData rework, apply a feature flag to control it consistently with the existing pattern used elsewhere in datapipe (e.g., GetFlagByKey in analysis.go).

packages/go/analysis/ad/post.go (1)

209-226: Bug: FetchAdminGroups uses relationship properties in a node query.

The filter uses query.StartProperty and query.EndProperty which are relationship-specific, but the query is on tx.Nodes(). These properties don't apply to node queries and will likely cause unexpected behavior or empty results.

Proposed fix
 func FetchAdminGroups(ctx context.Context, db graph.Database) (cardinality.Duplex[uint64], error) {
 	computerNodeIds := cardinality.NewBitmap64()

 	return computerNodeIds, db.ReadTransaction(ctx, func(tx graph.Transaction) error {
 		return tx.Nodes().Filter(query.And(
-			query.Or(
-				query.StringEndsWith(query.StartProperty(common.ObjectID.String()), wellknown.AdministratorsSIDSuffix.String()),
-				query.StringEndsWith(query.EndProperty(common.ObjectID.String()), wellknown.AdministratorsSIDSuffix.String()),
-			),
+			query.Kind(query.Node(), ad.LocalGroup),
+			query.StringEndsWith(query.NodeProperty(common.ObjectID.String()), wellknown.AdministratorsSIDSuffix.String()),
 		)).FetchIDs(func(cursor graph.Cursor[graph.ID]) error {
 			for id := range cursor.Chan() {
 				computerNodeIds.Add(id.Uint64())
 			}

 			return nil
 		})
 	})
 }
🤖 Fix all issues with AI agents
In `@cmd/api/src/analysis/ad/post.go`:
- Around line 26-61: The Post function in ad.post.go immediately returns,
disabling all AD post-processing (DeleteTransitEdges, PostDCSync,
PostLocalGroups, PostADCS, PostNTLM) and leaving parameters adcsEnabled,
citrixEnabled, ntlmEnabled, compositionCounter unused; add a single-line TODO
comment (e.g., "// TODO: BED-7147 - AD post-processing disabled pending Local
Group Post-Processing Rework; re-enable
DeleteTransitEdges/PostDCSync/PostLocalGroups/PostADCS/PostNTLM and restore use
of adcsEnabled/citrixEnabled/ntlmEnabled/compositionCounter when BED-7147 is
complete or gate via a feature flag") immediately before the early return in
Post to explain the temporary state and next steps for re-enabling.

In `@cmd/api/src/analysis/membership_integration_test.go`:
- Line 17: The file's integration-only build constraint is accidentally
commented out ("// //go:build integration"), so remove the extra leading "// "
to restore the directive (make the first non-blank line read "//go:build
integration"), ensure there's a blank line after the build tag per Go rules, and
save—this will prevent the integration tests in membership_integration_test.go
from being built in normal test runs.

In `@packages/go/analysis/ad/ad.go`:
- Around line 476-533: The function expandNodeSetGroupMemberships is currently
unused; either remove it entirely or retain it with a short comment explaining
its intended future purpose and why it must remain (e.g., reserved for group
expansion optimizations), and fix the typo in the comment/inline string from
"exlcuded" to "excluded" so the message is correct; be sure to adjust any
import/unused warnings if you remove the function.

In `@packages/go/analysis/ad/local_groups.go`:
- Around line 126-128: The slog.Error call currently uses an empty message which
makes logs unclear; replace the empty string in the slog.Error(...) call with a
concise, descriptive message (e.g., "failed to <operation>" reflecting the
surrounding operation) and keep the error field (use slog.String("err",
err.Error()) or slog.Any("err", err)) so the log includes both context and the
error details—update the slog.Error invocation in local_groups.go where
slog.Error("", slog.String("err", err.Error())) is present.
- Line 206: The worker loop uses runtime.NumCPU()/2 and can yield zero workers;
change the loop to compute a worker count variable (e.g., workers :=
runtime.NumCPU()/2), clamp it to at least 1 (if workers < 1 { workers = 1 }),
and then iterate for workerID := 0; workerID < workers; workerID++ so the loop
in local_groups.go never runs with zero workers (mirror the same fix applied to
PostCanRDP).
- Line 58: The for-loop that spawns workers uses runtime.NumCPU()/2 which can be
zero on single-core systems, so change the worker count calculation to ensure at
least one worker (e.g. set workers := runtime.NumCPU()/2; if workers < 1 {
workers = 1 }) and iterate for workerID := 0; workerID < workers; workerID++ to
guarantee the computerC channel is drained and avoid hangs when channels.Submit
blocks; update any related comments and use the same workerID/computerC symbols
in local_groups.go.

In `@packages/go/analysis/ad/post.go`:
- Around line 557-581: FetchNodePropertyMap currently ignores the error returned
by tx.Nodes().Filter(criteria).Query(...); capture the Query return value (e.g.
err := tx.Nodes().Filter(criteria).Query(...)) and if err != nil return nil, err
so query execution errors are propagated instead of silently returning a
possibly incomplete nodePropertyMap; keep the existing map population logic
inside the callback and only return nodePropertyMap, nil when Query succeeds.
- Around line 494-502: FetchLocalGroupMembership currently ignores the
groupSuffix argument and always uses canRDPData.RemoteDesktopUsersLocalGroup.ID;
either remove the unused groupSuffix parameter from the signature or use it to
select the correct local group before calling container.AdjacentNodes (e.g.,
look up the appropriate group ID from canRDPData using the suffix such as a
LocalGroupsBySuffix map or selector and then pass that group's ID.Uint64() into
container.AdjacentNodes instead of RemoteDesktopUsersLocalGroup.ID); ensure the
returned cardinality.NewBitmap64With still uses the adjacent nodes from the
chosen group.
🧹 Nitpick comments (3)
packages/go/analysis/ad/local_groups.go (1)

61-95: Unused workerID parameter in goroutines.

The workerID is passed to each worker goroutine but never used inside. Consider either removing the parameter or using it for logging/debugging to aid in troubleshooting.

Also applies to: 104-129, 209-239, 245-297

packages/go/analysis/ad/adcs.go (1)

36-36: Consider adding nil check for localGroupData parameter.

The function accepts localGroupData *LocalGroupData but doesn't validate it before passing to downstream functions. If a caller passes nil, this will cause panics in the ESC processing functions when accessing LocalGroupData.GroupMembershipCache.

Proposed fix
 func PostADCS(ctx context.Context, db graph.Database, localGroupData *LocalGroupData, adcsEnabled bool) (*analysis.AtomicPostProcessingStats, ADCSCache, error) {
+	if localGroupData == nil {
+		return &analysis.AtomicPostProcessingStats{}, ADCSCache{}, fmt.Errorf("localGroupData cannot be nil")
+	}
 	var cache = NewADCSCache()
cmd/api/src/test/integration/harnesses.go (1)

5534-5543: Make the “Membership” comment unambiguous (or drop the numeric mapping).

The // 1: 1, 3, 4, 5 style lines aren’t self-describing and are easy to desync from the actual edges below. Consider rewriting as explicit, verifiable statements (e.g., “User1 is transitively member of Group1 via Group4, and Group2 via Group3”) or remove the numeric mapping entirely.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b9501e2 and 734b965.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (22)
  • cmd/api/src/analysis/ad/post.go
  • cmd/api/src/analysis/membership_integration_test.go
  • cmd/api/src/analysis/post_integration_test.go
  • cmd/api/src/daemons/datapipe/datapipe.go
  • cmd/api/src/test/integration/harnesses.go
  • go.mod
  • packages/go/analysis/ad/ad.go
  • packages/go/analysis/ad/adcs.go
  • packages/go/analysis/ad/adcscache.go
  • packages/go/analysis/ad/esc1.go
  • packages/go/analysis/ad/esc10.go
  • packages/go/analysis/ad/esc13.go
  • packages/go/analysis/ad/esc3.go
  • packages/go/analysis/ad/esc4.go
  • packages/go/analysis/ad/esc6.go
  • packages/go/analysis/ad/esc9.go
  • packages/go/analysis/ad/esc_shared.go
  • packages/go/analysis/ad/local_groups.go
  • packages/go/analysis/ad/membership.go
  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/owns.go
  • packages/go/analysis/ad/post.go
🧰 Additional context used
🧠 Learnings (13)
📚 Learning: 2025-06-06T23:12:14.181Z
Learnt from: elikmiller
Repo: SpecterOps/BloodHound PR: 1563
File: packages/go/graphschema/azure/azure.go:24-24
Timestamp: 2025-06-06T23:12:14.181Z
Learning: In BloodHound, files in packages/go/graphschema/*/`*.go` are generated from CUE schemas. When `just prepare-for-codereview` is run, it triggers code generation that may automatically add import aliases or other formatting changes. These changes are legitimate outputs of the generation process, not manual edits that would be overwritten.

Applied to files:

  • packages/go/analysis/ad/esc13.go
  • packages/go/analysis/ad/esc10.go
  • packages/go/analysis/ad/esc3.go
  • cmd/api/src/analysis/membership_integration_test.go
  • packages/go/analysis/ad/owns.go
  • packages/go/analysis/ad/esc9.go
  • packages/go/analysis/ad/ad.go
  • cmd/api/src/analysis/post_integration_test.go
  • packages/go/analysis/ad/esc_shared.go
  • packages/go/analysis/ad/esc1.go
  • packages/go/analysis/ad/esc6.go
  • packages/go/analysis/ad/esc4.go
  • packages/go/analysis/ad/ntlm.go
  • go.mod
  • packages/go/analysis/ad/adcs.go
📚 Learning: 2025-07-10T14:31:01.781Z
Learnt from: JonasBK
Repo: SpecterOps/BloodHound PR: 1671
File: packages/go/analysis/ad/esc16.go:65-69
Timestamp: 2025-07-10T14:31:01.781Z
Learning: In BloodHound ADCS ESC analysis functions (PostADCSESC1, PostADCSESC4, PostADCSESC6, PostADCSESC9, PostADCSESC10, PostADCSESC13, PostADCSESC16, etc.), the established pattern for handling cert template validation errors and user filtering errors is to use slog.WarnContext to log warnings and continue processing with the next template, rather than returning errors to the caller. This pattern is consistently applied across all ADCS ESC edge implementations.

Applied to files:

  • packages/go/analysis/ad/esc13.go
  • packages/go/analysis/ad/esc10.go
  • packages/go/analysis/ad/esc3.go
  • packages/go/analysis/ad/esc9.go
  • packages/go/analysis/ad/esc1.go
  • packages/go/analysis/ad/esc6.go
  • packages/go/analysis/ad/esc4.go
📚 Learning: 2025-11-05T21:13:08.542Z
Learnt from: ktstrader
Repo: SpecterOps/BloodHound PR: 2020
File: cmd/api/src/queries/graph.go:198-203
Timestamp: 2025-11-05T21:13:08.542Z
Learning: In cmd/api/src/queries/graph.go, when ETAC filtering is enabled in GetAssetGroupComboNode, an empty etacAllowedList (when user.AllEnvironments is false and user.EnvironmentTargetedAccessControl is empty) is intentional and should not be guarded against. The empty list will cause query.In() to filter out all nodes, which is the correct security behavior—users with no environment assignments should see no results.

Applied to files:

  • packages/go/analysis/ad/esc13.go
  • packages/go/analysis/ad/ntlm.go
📚 Learning: 2025-06-25T17:52:33.291Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1606
File: cmd/api/src/analysis/azure/post.go:33-35
Timestamp: 2025-06-25T17:52:33.291Z
Learning: In BloodHound Go code, prefer using explicit slog type functions like slog.Any(), slog.String(), slog.Int(), etc. over simple key-value pairs for structured logging. This provides better type safety and makes key-value pairs more visually distinct. For error types, use slog.Any("key", err) or slog.String("key", err.Error()).

Applied to files:

  • packages/go/analysis/ad/owns.go
  • packages/go/analysis/ad/adcs.go
📚 Learning: 2025-07-10T14:33:20.317Z
Learnt from: JonasBK
Repo: SpecterOps/BloodHound PR: 1671
File: cmd/api/src/analysis/ad/adcs_integration_test.go:3687-3687
Timestamp: 2025-07-10T14:33:20.317Z
Learning: When reviewing Go code, functions defined in one file within a package are accessible from other files in the same package. Before flagging missing functions as compilation errors, check if they exist in other files within the same package directory.

Applied to files:

  • packages/go/analysis/ad/owns.go
📚 Learning: 2025-06-25T18:24:25.014Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1606
File: cmd/api/src/analysis/azure/post.go:33-35
Timestamp: 2025-06-25T18:24:25.014Z
Learning: In BloodHound Go code, for error types in slog structured logging, prefer using slog.String("key", err.Error()) over slog.Any("key", err). The explicit string conversion with err.Error() is preferred over using slog.Any() for error types.

Applied to files:

  • packages/go/analysis/ad/owns.go
📚 Learning: 2025-07-22T20:30:34.839Z
Learnt from: LawsonWillard
Repo: SpecterOps/BloodHound PR: 1700
File: cmd/api/src/api/v2/saved_queries_test.go:3182-3182
Timestamp: 2025-07-22T20:30:34.839Z
Learning: In Go table-driven tests in cmd/api/src/api/v2/saved_queries_test.go, subtest parallelization with t.Parallel() is acceptable when tests are self-contained, each creating their own mock controller (gomock.NewController(t)) and having isolated mock expectations without shared state between subtests.

Applied to files:

  • cmd/api/src/analysis/post_integration_test.go
📚 Learning: 2025-05-27T16:58:33.295Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1503
File: cmd/api/src/services/job/jobs_test.go:19-143
Timestamp: 2025-05-27T16:58:33.295Z
Learning: Tests in cmd/api/src/services/job/jobs_test.go have been found to be flaky in the past and are due for rewrite. They should be skipped with t.Skip() until they can be properly rewritten.

Applied to files:

  • cmd/api/src/analysis/post_integration_test.go
📚 Learning: 2025-06-17T22:37:36.389Z
Learnt from: LawsonWillard
Repo: SpecterOps/BloodHound PR: 1595
File: cmd/api/src/api/v2/saved_queries_test.go:2594-2594
Timestamp: 2025-06-17T22:37:36.389Z
Learning: In Go table-driven tests, there's a distinction between main test function parallelism and subtest parallelism. Main test functions can safely use t.Parallel() for performance benefits, but individual subtests within table-driven tests may need to run sequentially to avoid race conditions with mocks, deferred functions, or shared resources.

Applied to files:

  • cmd/api/src/analysis/post_integration_test.go
📚 Learning: 2025-06-11T20:49:35.177Z
Learnt from: kpom-specter
Repo: SpecterOps/BloodHound PR: 1562
File: cmd/api/src/daemons/datapipe/datapipe.go:100-120
Timestamp: 2025-06-11T20:49:35.177Z
Learning: In BloodHound's datapipe architecture, only the datapipe (e.g., `WithDatapipeStatus`) is responsible for mutating the datapipe status; pipeline actions themselves do not alter the status and instead communicate any job-level state elsewhere.

Applied to files:

  • cmd/api/src/daemons/datapipe/datapipe.go
📚 Learning: 2025-05-23T19:56:13.423Z
Learnt from: definitelynotagoblin
Repo: SpecterOps/BloodHound PR: 1440
File: packages/go/analysis/ad/ntlm.go:123-138
Timestamp: 2025-05-23T19:56:13.423Z
Learning: In the BloodHound NTLM analysis code (packages/go/analysis/ad/ntlm.go), the `isProtectedComputer` function intentionally fails open (returns false/unprotected) when the Protected Users group cannot be retrieved, maintaining consistency with the original logic patterns in the codebase.

Applied to files:

  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/post.go
📚 Learning: 2025-06-30T22:58:54.834Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1629
File: go.mod:18-18
Timestamp: 2025-06-30T22:58:54.834Z
Learning: Go 1.24 supports patch versions in the go directive of go.mod files. Syntax like "go 1.24.4" is valid and specifies the minimum Go version required to build the module, including the patch level.

Applied to files:

  • go.mod
📚 Learning: 2025-08-26T18:13:36.643Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1820
File: .github/workflows/audit-release.yml:0-0
Timestamp: 2025-08-26T18:13:36.643Z
Learning: The go tool command supports custom/third-party binaries as of Go 1.24, allowing invocation of custom CLI tools like "go tool stbernard" rather than being limited to only built-in Go toolchain binaries.

Applied to files:

  • go.mod
🧬 Code graph analysis (14)
packages/go/analysis/ad/local_groups.go (6)
cmd/api/src/database/db.go (1)
  • Database (73-192)
packages/go/analysis/ad/post.go (5)
  • LocalGroupData (620-625)
  • CanRDPComputerData (719-726)
  • FetchCanRDPEntityBitmapForComputer (527-555)
  • FetchCanRDPData (693-699)
  • FetchComputerLocalGroupIDBySIDSuffix (452-477)
packages/go/analysis/post_operation.go (2)
  • AtomicPostProcessingStats (86-90)
  • NewAtomicPostProcessingStats (92-98)
packages/go/analysis/post.go (1)
  • CreatePostRelationshipJob (151-157)
packages/go/graphschema/ad/ad.go (4)
  • CanRDP (68-68)
  • AdminTo (66-66)
  • CanPSRemote (67-67)
  • ExecuteDCOM (69-69)
packages/go/analysis/analysis.go (1)
  • MaximumDatabaseParallelWorkers (36-36)
packages/go/analysis/ad/esc13.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (620-625)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (537-698)
packages/go/analysis/ad/esc10.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (620-625)
packages/go/analysis/ad/adcscache.go (1)
  • ADCSCache (33-55)
packages/go/analysis/ad/esc3.go (3)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (620-625)
packages/go/analysis/ad/adcscache.go (1)
  • ADCSCache (33-55)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (537-698)
packages/go/analysis/ad/owns.go (1)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (620-625)
packages/go/analysis/ad/esc9.go (3)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (620-625)
packages/go/analysis/ad/adcscache.go (1)
  • ADCSCache (33-55)
packages/go/schemagen/tsgen/tsgen.go (1)
  • ID (216-220)
cmd/api/src/analysis/post_integration_test.go (2)
packages/go/analysis/ad/post.go (1)
  • FetchLocalGroupData (627-661)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (537-698)
cmd/api/src/daemons/datapipe/datapipe.go (1)
cmd/api/src/model/datapipestatus.go (1)
  • DatapipeStatusAnalyzing (26-26)
packages/go/analysis/ad/esc1.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (620-625)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (537-698)
packages/go/analysis/ad/esc6.go (2)
packages/go/analysis/ad/adcscache.go (1)
  • ADCSCache (33-55)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (537-698)
packages/go/analysis/ad/esc4.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (620-625)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (537-698)
packages/go/analysis/ad/ntlm.go (1)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (620-625)
packages/go/analysis/ad/adcs.go (4)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (620-625)
packages/go/analysis/ad/adcscache.go (1)
  • ADCSCache (33-55)
packages/go/analysis/ad/esc1.go (1)
  • PostADCSESC1 (35-64)
packages/go/analysis/ad/esc13.go (1)
  • PostADCSESC13 (36-78)
packages/go/analysis/ad/post.go (2)
packages/go/analysis/ad/wellknown/suffix.go (2)
  • AdministratorsSIDSuffix (83-83)
  • RemoteDesktopUsersSIDSuffix (86-86)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (537-698)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build BloodHound Container Image / Build and Package Container
  • GitHub Check: build-ui
🔇 Additional comments (59)
cmd/api/src/analysis/post_integration_test.go (5)

17-17: Build tag has been disabled - verify if intentional.

The //go:build serial_integration directive is now double-commented (// //go:build), which turns it into a regular comment and removes the build constraint entirely. These tests will now run in all builds instead of only during serial integration testing.

If intentional, consider removing the line entirely. If not, restore the original:

-// //go:build serial_integration
+//go:build serial_integration

84-92: LGTM - refactored to use new LocalGroupData API.

The test correctly uses the new pattern: fetch LocalGroupData first, then compute cross-product node sets. The parameter rename to graphDB is consistent with the new API usage.

Note: The tx parameter is unused in this callback. If the transaction isn't needed for any assertions, consider whether the test could use a simpler callback signature (if the test framework supports it).


101-109: LGTM - consistent refactoring pattern.

The changes align with the LocalGroupData API refactor applied across all cross-product tests.


118-126: LGTM - consistent with other cross-product tests.


135-143: LGTM - consistent refactoring with appropriate multi-assertion verification.

cmd/api/src/analysis/membership_integration_test.go (1)

83-91: LGTM - Test correctly refactored to use ReachabilityCache pattern.

The test properly invokes algo.FetchReachabilityCache with appropriate relationship kinds (ad.MemberOf, ad.MemberOfLocalGroup) and validates membership cardinalities using ReachExclusive with graph.DirectionInbound. The expected cardinality values (3, 1, 1, 1, 2) are correct: DomainGroupA has 3 transitive members (JohnUser, RDPDomainUsersGroup, RohanUser); DomainGroupB, C, and D each have 1 direct member; DomainGroupE has 2 members through group nesting (DomainGroupD + IrshadUser).

packages/go/analysis/ad/membership.go (1)

81-108: LGTM - Simplified direction reversal.

The inline call to direction.Reverse() at line 93 is a clean simplification that maintains the same behavior while removing the unnecessary intermediate variable. The overall FetchPathMembers function logic remains correct.

packages/go/analysis/ad/local_groups.go (1)

30-56: LGTM - Batch write pattern is well-structured.

Both PostCanRDP and PostLocalGroups use a consistent pattern for batching relationship writes with proper channel coordination and statistics tracking. The deferred channel closures and wait group synchronization ensure clean shutdown.

Also applies to: 177-203

packages/go/analysis/ad/esc_shared.go (1)

394-399: LGTM - LocalGroupData integration in getVictimBitmap.

The function correctly uses localGroupData.GroupMembershipCache to expand group memberships, aligning with the broader refactor from PathAggregator to LocalGroupData.

go.mod (1)

50-50: Dependency update verified: v0.4.1 provides the required ReachabilityCache API.

The bump from v0.3.2 to v0.4.1 introduces algo.ReachabilityCache and is confirmed compatible. The codebase actively uses FetchReachabilityCache() and FetchFilteredReachabilityCache() functions, and the version is available in the Go proxy registry.

packages/go/analysis/ad/esc1.go (1)

35-35: LGTM - Signature migration to LocalGroupData.

The function signature correctly migrates from impact.PathAggregator to *LocalGroupData, and the call to CalculateCrossProductNodeSets is properly updated to pass localGroupData as the first argument, matching the new signature in ad/ad.go.

Also applies to: 48-48

packages/go/analysis/ad/esc13.go (1)

36-36: LGTM - Consistent signature migration.

The signature change from impact.PathAggregator to *LocalGroupData is consistent with the broader refactor. The updated call to CalculateCrossProductNodeSets correctly passes localGroupData as the first argument. Error handling continues to follow the established pattern of logging warnings and continuing to the next template. Based on learnings, this is the expected pattern for ADCS ESC analysis functions.

Also applies to: 53-53

packages/go/analysis/ad/owns.go (2)

38-45: LGTM - LocalGroupData integration in PostOwnsAndWriteOwner.

The function correctly accepts *LocalGroupData and extracts GroupMembershipCache for use in FetchAdminGroupIds. This aligns with the broader migration from PathAggregator to LocalGroupData.


196-213: OrReachability method signature and behavior confirmed.

The OrReachability method exists in the algo.ReachabilityCache type with the expected signature and correctly ORs the inbound reachability set (all members of the group) into adminIds. The implementation properly expands group memberships through the reachability cache.

packages/go/analysis/ad/adcscache.go (1)

30-30: LGTM - Timing instrumentation addition.

The deferred timing wrapper using util.SLogMeasureFunction follows the standard Go pattern for measuring function execution time. The double invocation ()() correctly captures the start time immediately while deferring the duration logging. This usage is consistent with similar instrumentation patterns in the codebase (e.g., local_groups.go, post.go).

packages/go/analysis/ad/esc9.go (4)

35-35: LGTM! Signature updated to use LocalGroupData.

The function signature change from groupExpansions impact.PathAggregator to localGroupData *LocalGroupData aligns with the broader refactor across the AD post-processing modules. This centralizes group membership data into a single container structure.


84-84: LGTM! Consistent signature change for PostADCSESC9b.

The signature update mirrors PostADCSESC9a, maintaining consistency across the ESC9 implementations.


102-102: LGTM! Consistent usage of localGroupData in ESC9b.

The getVictimBitmap call correctly uses the new localGroupData parameter.


53-53: Parameter propagation to getVictimBitmap is correct.

The function signature in esc_shared.go (line 394) confirms getVictimBitmap accepts *LocalGroupData as the first parameter, and the call site correctly passes localGroupData along with all other parameters in the correct order and types. The logic flow is maintained with the updated data structure.

packages/go/analysis/ad/esc3.go (3)

38-38: LGTM! Signature updated for PostADCSESC3.

The function signature change is consistent with the broader refactor, replacing groupExpansions impact.PathAggregator with localGroupData *LocalGroupData.


91-97: LGTM! Cross-product calculation updated for enrollment agent restrictions case.

The CalculateCrossProductNodeSets call correctly passes localGroupData as the first argument, with the remaining node slice arguments preserved. The delegatedAgents.Slice() is appropriately included when enrollment agent restrictions apply.


109-114: LGTM! Cross-product calculation updated for non-restricted case.

The call correctly uses localGroupData and maintains the same node slice arguments for computing the cross-product when enrollment agent restrictions do not apply.

packages/go/analysis/ad/esc10.go (4)

36-36: LGTM! Signature updated for PostADCSESC10a.

The function signature change aligns with the broader refactor, replacing the groupExpansions parameter with localGroupData *LocalGroupData.


54-54: LGTM! Victim bitmap calculation updated.

The getVictimBitmap call correctly uses localGroupData to compute the victim bitmap for ESC10a.


84-84: LGTM! Consistent signature change for PostADCSESC10b.

The signature update mirrors PostADCSESC10a, maintaining consistency across the ESC10 implementations.


102-102: LGTM! Consistent usage of localGroupData in ESC10b.

The getVictimBitmap call correctly uses the new localGroupData parameter.

packages/go/analysis/ad/ad.go (7)

32-32: LGTM! Import added for algo package.

The algo import is required for accessing algo.ReachabilityCache, which is used within LocalGroupData.GroupMembershipCache.


537-537: LGTM! Signature updated for CalculateCrossProductNodeSets.

The function now accepts localGroupData *LocalGroupData as its first parameter, centralizing access to group membership data and excluded shortcut groups.


562-603: LGTM! Cross-product logic refactored to use LocalGroupData.

The loop correctly:

  1. Checks localGroupData.ExcludedShortcutGroups.Contains(entityID) to skip excluded groups (Auth Users/Everyone)
  2. Uses localGroupData.GroupMembershipCache.Reach() to expand group memberships
  3. Iterates over excluded nodes to detect if any are reachable from the current entity

The early-break optimization when nodeExcluded is true prevents unnecessary processing.


614-624: LGTM! Edge case handling for single remaining set.

When all but one nodeset contains excluded groups (Auth Users/Everyone), returning just that single first-degree set is the correct behavior.


631-631: LGTM! Reachability expansion via OrReachability.

Using localGroupData.GroupMembershipCache.OrReachability to accumulate reachable nodes into unrolledRefSet is consistent with the new data structure pattern.


639-657: LGTM! Efficient cardinality calculation with eager bitmap clearing.

The pattern of using a temporary bitmap, computing cardinality, and immediately clearing it prevents memory accumulation during the iteration. This is a good practice for processing large node sets.


682-682: LGTM! XOR-based membership removal.

Using XorReachability to remove all members of a matched group from unrolledRefSet correctly implements the shortcut optimization that avoids redundant membership checks.

packages/go/analysis/ad/esc4.go (6)

35-35: LGTM! Signature updated for PostADCSESC4.

The function signature change is consistent with the broader refactor across all ESC modules.


63-67: LGTM! Cross-product for path 2a updated.

The call correctly uses localGroupData for computing principals that control the cert template.


71-76: LGTM! Cross-product for path 2b updated.

The call correctly computes the cross-product for principals with Enroll/AllExtendedRights + GenericWrite combinations.


88-94: LGTM! Cross-product for path 2d updated.

The four-way cross-product for Enroll/AllExtendedRights + WritePKINameFlag + WritePKIEnrollmentFlag correctly uses localGroupData.


99-104: LGTM! Cross-product for path 2e updated.

The conditional cross-product when enrolleeSuppliesSubject is true correctly uses localGroupData.


111-116: LGTM! Cross-product for path 2f updated.

The conditional cross-product when requiresManagerApproval is false correctly uses localGroupData.

packages/go/analysis/ad/adcs.go (1)

109-207: LGTM - Consistent refactoring of ESC processing calls.

The localGroupData parameter is correctly propagated to all PostADCSESC* functions. The error handling patterns and control flow remain unchanged, ensuring behavioral consistency with the previous implementation.

packages/go/analysis/ad/esc6.go (2)

37-79: LGTM - ESC6a refactored to use LocalGroupData.

The signature change and CalculateCrossProductNodeSets call update are consistent with the broader refactoring pattern. The error handling via slog.WarnContext continues to follow the established ADCS ESC pattern (per retrieved learnings).


82-127: LGTM - ESC6b refactored consistently with ESC6a.

The changes mirror the ESC6a refactoring, maintaining consistency across both ESC6 variants. The HasUPNCertMappingInForest check remains in place for the ESC6b-specific domain filtering.

packages/go/analysis/ad/ntlm.go (6)

41-47: LGTM - NTLMCache struct updated to use LocalGroupData.

The field change from GroupExpansions impact.PathAggregator to LocalGroupData *LocalGroupData aligns with the broader refactoring. The getter methods remain unchanged, preserving the cache's API.


64-125: LGTM - NewNTLMCache correctly propagates LocalGroupData.

The function properly passes localGroupData.GroupMembershipCache to FetchProtectedUsersMappedToDomains and stores the full LocalGroupData reference for later use. The fail-open behavior for protected users (per retrieved learnings) is maintained.


128-213: LGTM - PostNTLM signature and cache creation updated.

The function correctly passes localGroupData to NewNTLMCache. The early return for disabled NTLM feature and the nested error handling remain unchanged.


586-593: LGTM - Group expansion in PostCoerceAndRelayNTLMToSMB uses OrReachability.

The bitmap expansion correctly uses LocalGroupData.GroupMembershipCache.OrReachability for group membership resolution, replacing the previous PathAggregator approach.


856-881: LGTM - FetchProtectedUsersMappedToDomains accepts ReachabilityCache directly.

The function correctly uses OrReachability to populate the protected users bitmap. Taking *algo.ReachabilityCache directly (rather than full LocalGroupData) is appropriate since only the cache is needed.


441-448: The function expandNodeSliceToBitmapWithoutGroups exists with the correct signature accepting *algo.ReachabilityCache as its second parameter.

packages/go/analysis/ad/post.go (10)

39-72: LGTM - PostSyncLAPSPassword updated to use LocalGroupData.

The function correctly passes localGroupData to getLAPSSyncers. The domain iteration and relationship creation logic remain unchanged.


74-104: LGTM - PostDCSync updated to use LocalGroupData.

Consistent with PostSyncLAPSPassword, the function correctly propagates localGroupData to getDCSyncers.


187-207: LGTM - New FetchComputerIDs function.

Clean implementation using FetchIDs for efficient ID-only retrieval. The measurement logging is helpful for performance monitoring.


288-303: LGTM - getLAPSSyncers updated to use LocalGroupData.

The function correctly passes localGroupData to CalculateCrossProductNodeSets for computing the cross-product of GetChanges and GetChangesInFilteredSet principals.


305-320: LGTM - getDCSyncers updated to use LocalGroupData.

Consistent with getLAPSSyncers, the cross-product calculation now uses localGroupData.


425-477: LGTM - Refactored local group lookup functions.

Both FetchComputerLocalGroupBySIDSuffix and FetchComputerLocalGroupIDBySIDSuffix now use explicit query/scan patterns with proper resource cleanup via results.Close().


620-625: LGTM - LocalGroupData struct definition.

The struct consolidates the necessary data for local group post-processing:

  • Computers: bitmap of computer IDs
  • GroupMembershipCache: reachability cache for group membership
  • ExcludedShortcutGroups: Auth Users/Everyone for shortcut handling
  • LocalGroupMembershipDigraph: directed graph for local group membership

The TODO comment on ExcludedShortcutGroups should be addressed in a follow-up.


627-661: LGTM - FetchLocalGroupData builds the consolidated data structure.

The function correctly:

  1. Fetches excluded shortcut groups (Auth Users/Everyone)
  2. Fetches computer IDs
  3. Builds the reachability cache for MemberOf and MemberOfLocalGroup relationships
  4. Builds the directed graph for local group membership

701-755: LGTM - CanRDPData and CanRDPComputerData types.

The type hierarchy is well-structured:

  • CanRDPData extends LocalGroupData with URA-specific data
  • CanRDPComputerData extends CanRDPData with per-computer context
  • HasRemoteInteractiveLogonRight method provides efficient lookup via digraph

771-810: LGTM - Refactored RDP processing functions.

FetchRemoteDesktopUsersBitmapForComputer and ProcessRDPWithUra correctly use the new CanRDPComputerData structure. The ProcessRDPWithUra implementation:

  1. Gets RDP local group members via reachability cache
  2. Gets RIL entities from the digraph
  3. Performs cross-product validation

The logic is clear and efficient.

cmd/api/src/test/integration/harnesses.go (1)

5544-5548: Code changes are correct and require no test updates.

The new relationships (User1 -> Group3 and User1 -> Group4) properly expand the transitive membership coverage. Tests in post_integration_test.go that depend on ShortcutHarness only verify the presence of specific group relationships (e.g., that Group3 is reachable via cross-product calculation), not exact membership counts. These presence-based assertions will continue to pass with the expanded relationship set.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@zinic zinic changed the title BED-7147 - Local Group Post-Processing Rework feat: BED-7147 - Local Group Post-Processing Rework Jan 15, 2026
Copy link
Contributor

@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: 5

Caution

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

⚠️ Outside diff range comments (3)
cmd/api/src/daemons/datapipe/datapipe.go (1)

72-104: Critical: Early return leaves daemon loop unreachable.

The return on line 74 makes all code from line 76 onwards dead code. The daemon will only execute a single Analyze call and immediately exit, bypassing:

  • The normal Start pipeline action
  • Scheduled pruning, purging, and ingesting
  • The main event loop and graceful shutdown handling

If this is intentional for the rework, the unreachable code (lines 76–103) should be removed. If accidental (e.g., debugging code left in), remove lines 73–75 to restore normal daemon behavior.

Option A: Remove early return if this was debugging code
 func (s *Daemon) Start(ctx context.Context) {
-	s.WithDatapipeStatus(ctx, model.DatapipeStatusAnalyzing, s.pipeline.Analyze)
-	return
-
 	var (
 		datapipeLoopTimer = time.NewTimer(s.startDelay)
Option B: Remove dead code if early return is intentional
 func (s *Daemon) Start(ctx context.Context) {
 	s.WithDatapipeStatus(ctx, model.DatapipeStatusAnalyzing, s.pipeline.Analyze)
-	return
-
-	var (
-		datapipeLoopTimer = time.NewTimer(s.startDelay)
-		pruningTicker     = time.NewTicker(pruningInterval)
-	)
-
-	defer datapipeLoopTimer.Stop()
-	defer pruningTicker.Stop()
-
-	s.WithDatapipeStatus(ctx, model.DatapipeStatusStarting, s.pipeline.Start)
-
-	for {
-		select {
-		case <-pruningTicker.C:
-			s.WithDatapipeStatus(ctx, model.DatapipeStatusPruning, s.pipeline.PruneData)
-
-		case <-datapipeLoopTimer.C:
-			s.WithDatapipeStatus(ctx, model.DatapipeStatusPurging, s.pipeline.DeleteData)
-
-			s.WithDatapipeStatus(ctx, model.DatapipeStatusIngesting, s.pipeline.IngestTasks)
-
-			s.WithDatapipeStatus(ctx, model.DatapipeStatusAnalyzing, s.pipeline.Analyze)
-
-			datapipeLoopTimer.Reset(s.tickInterval)
-
-		case <-ctx.Done():
-			return
-		}
-	}
 }
packages/go/analysis/ad/esc_shared.go (1)

303-334: Remove dead variable and commented code; add scratch.Clear() between iterations.

The isGroup variable is initialized to false but the code block that would set it to true is commented out (lines 316-320), making the condition on line 322 always evaluate to true. This removes the group filtering logic entirely. Additionally, the scratch bitmap is reused across loop iterations without being cleared, causing data from previous controller iterations to accumulate. Each iteration should start with an empty scratch.

Remove the dead variable, commented code, and add scratch.Clear() after the scratch.Each() call to ensure proper state between iterations.

🔧 Proposed fix
 func expandNodeSliceToBitmapWithoutGroups(nodes []*graph.Node, groupExpansions *algo.ReachabilityCache) cardinality.Duplex[uint64] {
 	var (
 		bitmap  = cardinality.NewBitmap64()
 		scratch = cardinality.NewBitmap64()
 	)

 	for _, controller := range nodes {
 		if controller.Kinds.ContainsOneOf(ad.Group) {
 			groupExpansions.OrReachability(controller.ID.Uint64(), graph.DirectionInbound, scratch)

 			scratch.Each(func(memberID uint64) bool {
-				isGroup := false
-
-				// groupExpansions.Digraph.EachAdjacentNode(memberID, graph.DirectionInbound, func(adjacent uint64) bool {
-				// 	// If the expanded member of the controller group has an adjacent inbound node then it must be a group
-				// 	isGroup = true
-				// 	return false
-				// })
-
-				if !isGroup {
-					bitmap.Add(memberID)
-				}
-
+				bitmap.Add(memberID)
 				return true
 			})
+			scratch.Clear()
 		} else {
 			bitmap.Add(controller.ID.Uint64())
 		}
 	}

 	return bitmap
 }
packages/go/analysis/ad/post.go (1)

209-226: Bug: Using relationship query helpers in a node query.

query.StartProperty and query.EndProperty are designed for relationship queries (to access properties of the start/end nodes of a relationship). When used in a tx.Nodes().Filter(...) context, this won't produce the expected results. Use query.NodeProperty instead.

Proposed fix
 func FetchAdminGroups(ctx context.Context, db graph.Database) (cardinality.Duplex[uint64], error) {
 	computerNodeIds := cardinality.NewBitmap64()
 
 	return computerNodeIds, db.ReadTransaction(ctx, func(tx graph.Transaction) error {
 		return tx.Nodes().Filter(query.And(
-			query.Or(
-				query.StringEndsWith(query.StartProperty(common.ObjectID.String()), wellknown.AdministratorsSIDSuffix.String()),
-				query.StringEndsWith(query.EndProperty(common.ObjectID.String()), wellknown.AdministratorsSIDSuffix.String()),
-			),
+			query.StringEndsWith(query.NodeProperty(common.ObjectID.String()), wellknown.AdministratorsSIDSuffix.String()),
 		)).FetchIDs(func(cursor graph.Cursor[graph.ID]) error {
 			for id := range cursor.Chan() {
 				computerNodeIds.Add(id.Uint64())
 			}
 
 			return nil
 		})
 	})
 }
🤖 Fix all issues with AI agents
In `@packages/go/analysis/ad/local_groups.go`:
- Around line 237-244: The switch on nextJob.groupSuffix (cases
adminGroupSuffix, psRemoteGroupSuffix, dcomGroupSuffix) can leave edgeKind unset
for unknown suffixes; add a default case that handles unexpected values by
logging the unexpected nextJob.groupSuffix (or returning an error) and
skipping/continuing rather than using an uninitialized edgeKind, so that
invalid/empty edge kinds are never created.
- Around line 113-146: The early return when localGroupData.FetchCanRDPData(ctx,
graphDB) fails leaves the post goroutine (the one that reads from postC started
earlier) blocked because it never receives a close; fix by either moving the
post goroutine startup (the goroutine that reads from postC) so it is launched
only after FetchCanRDPData succeeds, or ensure all channels the post goroutine
consumes (postC and any upstream channels like workC/computerC) are closed on
every exit path (e.g., add a defer to close those channels before calling
FetchCanRDPData or before returning on error); update the code paths around
localGroupData.FetchCanRDPData, channels.Receive, channels.Submit, and the postC
goroutine accordingly so no goroutine can block on an unclosed channel on early
return.

In `@packages/go/analysis/ad/post.go`:
- Around line 452-477: FetchComputerLocalGroupIDBySIDSuffix currently returns a
zero graph.ID when no rows match; update the query callback (or post-query
handling) to return graph.ErrNoResultsFound instead of silently returning nil
error when results.Next() is false — e.g., inside the anonymous Query callback
for FetchComputerLocalGroupIDBySIDSuffix, if results.Next() is false return
graph.ErrNoResultsFound (or set queryErr accordingly) so callers receive a clear
no-results error similar to FetchComputerLocalGroupBySIDSuffix.
- Around line 772-778: FetchRemoteDesktopUsersBitmapForComputer calls
FetchRemoteDesktopUsersLocalGroupMembership with three arguments but that
function only accepts two; update the call in
FetchRemoteDesktopUsersBitmapForComputer to match
FetchRemoteDesktopUsersLocalGroupMembership’s signature (pass the two expected
parameters—e.g., computer and wellknown.RemoteDesktopUsersSIDSuffix.String()—and
drop canRDPData), or alternatively modify
FetchRemoteDesktopUsersLocalGroupMembership to accept the third parameter
(canRDPData) if its logic requires it; ensure ProcessRDPWithUra remains
unchanged and that parameter names (computer, canRDPData, rdpGroup,
wellknown.RemoteDesktopUsersSIDSuffix) are used consistently.
♻️ Duplicate comments (1)
packages/go/analysis/ad/post.go (1)

557-582: LGTM - Query error handling is now correct.

The error from tx.Nodes().Filter(criteria).Query(...) is properly captured and returned. This addresses the concern from the previous review.

🧹 Nitpick comments (2)
cmd/api/src/analysis/membership_integration_test.go (1)

77-92: Consider renaming the test to reflect the new API.

The implementation correctly adapts to the new algo.FetchReachabilityCache API and uses ReachExclusive for membership assertions. However, the test name TestResolveAllGroupMemberships references the removed ResolveAllGroupMemberships function.

Suggested rename
-func TestResolveAllGroupMemberships(t *testing.T) {
+func TestFetchReachabilityCache(t *testing.T) {
packages/go/analysis/ad/post.go (1)

621-626: TODO comment needs clarification.

There's a TODO: Document me comment for ExcludedShortcutGroups. Consider adding documentation explaining the purpose of this field and which groups it represents.

Would you like me to help draft documentation for this field, or should I open an issue to track this?

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 734b965 and b44edf3.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (22)
  • cmd/api/src/analysis/ad/post.go
  • cmd/api/src/analysis/membership_integration_test.go
  • cmd/api/src/analysis/post_integration_test.go
  • cmd/api/src/daemons/datapipe/datapipe.go
  • cmd/api/src/test/integration/harnesses.go
  • go.mod
  • packages/go/analysis/ad/ad.go
  • packages/go/analysis/ad/adcs.go
  • packages/go/analysis/ad/adcscache.go
  • packages/go/analysis/ad/esc1.go
  • packages/go/analysis/ad/esc10.go
  • packages/go/analysis/ad/esc13.go
  • packages/go/analysis/ad/esc3.go
  • packages/go/analysis/ad/esc4.go
  • packages/go/analysis/ad/esc6.go
  • packages/go/analysis/ad/esc9.go
  • packages/go/analysis/ad/esc_shared.go
  • packages/go/analysis/ad/local_groups.go
  • packages/go/analysis/ad/membership.go
  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/owns.go
  • packages/go/analysis/ad/post.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • cmd/api/src/test/integration/harnesses.go
  • packages/go/analysis/ad/esc9.go
  • packages/go/analysis/ad/esc13.go
  • packages/go/analysis/ad/esc1.go
🧰 Additional context used
🧠 Learnings (12)
📚 Learning: 2025-06-25T18:24:25.014Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1606
File: cmd/api/src/analysis/azure/post.go:33-35
Timestamp: 2025-06-25T18:24:25.014Z
Learning: In BloodHound Go code, for error types in slog structured logging, prefer using slog.String("key", err.Error()) over slog.Any("key", err). The explicit string conversion with err.Error() is preferred over using slog.Any() for error types.

Applied to files:

  • packages/go/analysis/ad/local_groups.go
  • packages/go/analysis/ad/owns.go
📚 Learning: 2025-06-25T17:52:33.291Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1606
File: cmd/api/src/analysis/azure/post.go:33-35
Timestamp: 2025-06-25T17:52:33.291Z
Learning: In BloodHound Go code, prefer using explicit slog type functions like slog.Any(), slog.String(), slog.Int(), etc. over simple key-value pairs for structured logging. This provides better type safety and makes key-value pairs more visually distinct. For error types, use slog.Any("key", err) or slog.String("key", err.Error()).

Applied to files:

  • packages/go/analysis/ad/local_groups.go
  • packages/go/analysis/ad/owns.go
  • packages/go/analysis/ad/adcs.go
📚 Learning: 2025-05-27T16:58:33.295Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1503
File: cmd/api/src/services/job/jobs_test.go:19-143
Timestamp: 2025-05-27T16:58:33.295Z
Learning: Tests in cmd/api/src/services/job/jobs_test.go have been found to be flaky in the past and are due for rewrite. They should be skipped with t.Skip() until they can be properly rewritten.

Applied to files:

  • cmd/api/src/analysis/membership_integration_test.go
📚 Learning: 2025-06-06T23:12:14.181Z
Learnt from: elikmiller
Repo: SpecterOps/BloodHound PR: 1563
File: packages/go/graphschema/azure/azure.go:24-24
Timestamp: 2025-06-06T23:12:14.181Z
Learning: In BloodHound, files in packages/go/graphschema/*/`*.go` are generated from CUE schemas. When `just prepare-for-codereview` is run, it triggers code generation that may automatically add import aliases or other formatting changes. These changes are legitimate outputs of the generation process, not manual edits that would be overwritten.

Applied to files:

  • cmd/api/src/analysis/membership_integration_test.go
  • packages/go/analysis/ad/esc_shared.go
  • packages/go/analysis/ad/owns.go
  • packages/go/analysis/ad/ad.go
  • packages/go/analysis/ad/esc4.go
  • packages/go/analysis/ad/adcs.go
  • packages/go/analysis/ad/esc6.go
  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/esc10.go
  • packages/go/analysis/ad/esc3.go
  • go.mod
📚 Learning: 2025-07-10T14:33:20.317Z
Learnt from: JonasBK
Repo: SpecterOps/BloodHound PR: 1671
File: cmd/api/src/analysis/ad/adcs_integration_test.go:3687-3687
Timestamp: 2025-07-10T14:33:20.317Z
Learning: When reviewing Go code, functions defined in one file within a package are accessible from other files in the same package. Before flagging missing functions as compilation errors, check if they exist in other files within the same package directory.

Applied to files:

  • packages/go/analysis/ad/owns.go
📚 Learning: 2025-07-10T14:31:01.781Z
Learnt from: JonasBK
Repo: SpecterOps/BloodHound PR: 1671
File: packages/go/analysis/ad/esc16.go:65-69
Timestamp: 2025-07-10T14:31:01.781Z
Learning: In BloodHound ADCS ESC analysis functions (PostADCSESC1, PostADCSESC4, PostADCSESC6, PostADCSESC9, PostADCSESC10, PostADCSESC13, PostADCSESC16, etc.), the established pattern for handling cert template validation errors and user filtering errors is to use slog.WarnContext to log warnings and continue processing with the next template, rather than returning errors to the caller. This pattern is consistently applied across all ADCS ESC edge implementations.

Applied to files:

  • packages/go/analysis/ad/esc4.go
  • cmd/api/src/analysis/ad/post.go
  • packages/go/analysis/ad/esc6.go
  • packages/go/analysis/ad/esc10.go
  • packages/go/analysis/ad/esc3.go
📚 Learning: 2025-05-23T19:56:13.423Z
Learnt from: definitelynotagoblin
Repo: SpecterOps/BloodHound PR: 1440
File: packages/go/analysis/ad/ntlm.go:123-138
Timestamp: 2025-05-23T19:56:13.423Z
Learning: In the BloodHound NTLM analysis code (packages/go/analysis/ad/ntlm.go), the `isProtectedComputer` function intentionally fails open (returns false/unprotected) when the Protected Users group cannot be retrieved, maintaining consistency with the original logic patterns in the codebase.

Applied to files:

  • cmd/api/src/analysis/ad/post.go
  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/post.go
📚 Learning: 2025-11-05T21:13:08.542Z
Learnt from: ktstrader
Repo: SpecterOps/BloodHound PR: 2020
File: cmd/api/src/queries/graph.go:198-203
Timestamp: 2025-11-05T21:13:08.542Z
Learning: In cmd/api/src/queries/graph.go, when ETAC filtering is enabled in GetAssetGroupComboNode, an empty etacAllowedList (when user.AllEnvironments is false and user.EnvironmentTargetedAccessControl is empty) is intentional and should not be guarded against. The empty list will cause query.In() to filter out all nodes, which is the correct security behavior—users with no environment assignments should see no results.

Applied to files:

  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/post.go
📚 Learning: 2025-07-22T20:30:34.839Z
Learnt from: LawsonWillard
Repo: SpecterOps/BloodHound PR: 1700
File: cmd/api/src/api/v2/saved_queries_test.go:3182-3182
Timestamp: 2025-07-22T20:30:34.839Z
Learning: In Go table-driven tests in cmd/api/src/api/v2/saved_queries_test.go, subtest parallelization with t.Parallel() is acceptable when tests are self-contained, each creating their own mock controller (gomock.NewController(t)) and having isolated mock expectations without shared state between subtests.

Applied to files:

  • cmd/api/src/analysis/post_integration_test.go
📚 Learning: 2025-06-11T20:49:35.177Z
Learnt from: kpom-specter
Repo: SpecterOps/BloodHound PR: 1562
File: cmd/api/src/daemons/datapipe/datapipe.go:100-120
Timestamp: 2025-06-11T20:49:35.177Z
Learning: In BloodHound's datapipe architecture, only the datapipe (e.g., `WithDatapipeStatus`) is responsible for mutating the datapipe status; pipeline actions themselves do not alter the status and instead communicate any job-level state elsewhere.

Applied to files:

  • cmd/api/src/daemons/datapipe/datapipe.go
📚 Learning: 2025-06-30T22:58:54.834Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1629
File: go.mod:18-18
Timestamp: 2025-06-30T22:58:54.834Z
Learning: Go 1.24 supports patch versions in the go directive of go.mod files. Syntax like "go 1.24.4" is valid and specifies the minimum Go version required to build the module, including the patch level.

Applied to files:

  • go.mod
📚 Learning: 2025-08-26T18:13:36.643Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1820
File: .github/workflows/audit-release.yml:0-0
Timestamp: 2025-08-26T18:13:36.643Z
Learning: The go tool command supports custom/third-party binaries as of Go 1.24, allowing invocation of custom CLI tools like "go tool stbernard" rather than being limited to only built-in Go toolchain binaries.

Applied to files:

  • go.mod
🧬 Code graph analysis (8)
packages/go/analysis/ad/local_groups.go (5)
packages/go/analysis/ad/post.go (3)
  • LocalGroupData (621-626)
  • CanRDPComputerData (720-727)
  • FetchCanRDPEntityBitmapForComputer (527-555)
packages/go/analysis/post_operation.go (3)
  • AtomicPostProcessingStats (86-90)
  • NewAtomicPostProcessingStats (92-98)
  • NewPropertiesWithLastSeen (166-171)
packages/go/analysis/post.go (1)
  • CreatePostRelationshipJob (151-157)
packages/go/graphschema/ad/ad.go (4)
  • CanRDP (68-68)
  • AdminTo (66-66)
  • CanPSRemote (67-67)
  • ExecuteDCOM (69-69)
packages/go/analysis/analysis.go (1)
  • MaximumDatabaseParallelWorkers (36-36)
packages/go/analysis/ad/owns.go (1)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (621-626)
packages/go/analysis/ad/ad.go (1)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (621-626)
packages/go/analysis/ad/esc6.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (621-626)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (477-638)
packages/go/analysis/ad/esc10.go (3)
packages/go/analysis/post.go (1)
  • CreatePostRelationshipJob (151-157)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (621-626)
packages/go/analysis/ad/adcscache.go (1)
  • ADCSCache (33-55)
packages/go/analysis/ad/esc3.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (621-626)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (477-638)
cmd/api/src/daemons/datapipe/datapipe.go (1)
cmd/api/src/model/datapipestatus.go (1)
  • DatapipeStatusAnalyzing (26-26)
packages/go/analysis/ad/post.go (2)
packages/go/analysis/ad/wellknown/suffix.go (2)
  • AdministratorsSIDSuffix (83-83)
  • RemoteDesktopUsersSIDSuffix (86-86)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (477-638)
🪛 GitHub Actions: Run Go Unit Tests
packages/go/analysis/ad/post.go

[error] 774-774: FetchRemoteDesktopUsersLocalGroupMembership called with too many arguments.


[error] Build failed due to compilation error in post.go (too many arguments in call).

🪛 golangci-lint (2.5.0)
packages/go/analysis/ad/post.go

[major] 774-774: : # github.com/specterops/bloodhound/packages/go/analysis/ad
packages/go/analysis/ad/post.go:774:112: too many arguments in call to FetchRemoteDesktopUsersLocalGroupMembership
have (graph.ID, string, *CanRDPComputerData)
want (graph.ID, *CanRDPComputerData)

(typecheck)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build BloodHound Container Image / Build and Package Container
  • GitHub Check: run-analysis
  • GitHub Check: run-tests
  • GitHub Check: build-ui
🔇 Additional comments (53)
go.mod (2)

87-87: LGTM: Indirect dependency updates align with direct updates.

These indirect dependency updates appear to be automatically pulled in by Go modules due to the direct dependency changes. The golang.org/x/* library updates are routine maintenance patches.

Also applies to: 276-276, 278-278, 280-281


21-21: Dependency updates verified and approved.

All versions exist and are secure:

  • github.com/jackc/pgx/v5 v5.8.0 includes patches for CVE-2024-27289 and CVE-2024-27304
  • github.com/specterops/dawgs v0.4.1 (minor bump from v0.3.2) has no known security advisories
  • Remaining updates (cuelang.org/go, golang.org/x/*, github.com/axiomhq/hyperloglog) all verified with no advisories

The specterops/dawgs minor version update is appropriate for supporting the local group post-processing refactor mentioned in the PR.

packages/go/analysis/ad/adcscache.go (1)

30-30: LGTM! Clean timing instrumentation addition.

The deferred timing wrapper pattern is idiomatic and correctly uses double parentheses to invoke the returned cleanup function. This provides consistent duration logging for cache building operations.

Also applies to: 76-77

packages/go/analysis/ad/membership.go (1)

92-104: LGTM! Clean simplification.

Calling direction.Reverse() directly eliminates the unnecessary intermediate variable and related error handling, making the code more concise.

cmd/api/src/analysis/post_integration_test.go (2)

79-93: LGTM! Test correctly adapted to new LocalGroupData API.

The test properly exercises the new two-step flow: FetchLocalGroupData followed by CalculateCrossProductNodeSets. The require.Nilf provides better error context on failure.


96-145: Remaining cross-product tests follow consistent pattern.

All four cross-product tests (TestCrossProduct, TestCrossProductAuthUsers, TestCrossProductEveryone, TestCrossProductEveryone2) correctly implement the new LocalGroupData workflow with consistent error handling and assertions.

cmd/api/src/analysis/ad/post.go (1)

29-67: Comprehensive refactor to LocalGroupData-based workflow.

The Post function is now properly wired with the new LocalGroupData abstraction replacing the previous groupExpansions/PathAggregator approach. Key observations:

  1. All processing steps correctly receive localGroupData
  2. The adcsCache is properly threaded from PostADCS to PostNTLM
  3. All enabled flags (adcsEnabled, citrixEnabled, ntlmEnabled) and compositionCounter are correctly utilized
  4. Stats aggregation is complete for all processing steps

The fail-fast error handling with early returns is appropriate for this sequential processing pipeline.

packages/go/analysis/ad/owns.go (3)

24-24: LGTM - Import added for algo package.

The new import supports the *algo.ReachabilityCache type used in FetchAdminGroupIds.


38-48: LGTM - Function signature updated to use LocalGroupData.

The migration from direct groupExpansions parameter to localGroupData *LocalGroupData is consistent with the broader refactor. The GroupMembershipCache is correctly extracted from localGroupData when calling FetchAdminGroupIds.


196-214: No action required — direction is correct.

DirectionInbound is the correct direction for expanding group membership. In AD graph semantics, "MemberOf" edges point from members to groups, so following DirectionInbound on a group correctly identifies all its members. This pattern is consistent across the codebase (ad.go, ntlm.go, esc_shared.go) and validated by integration tests that confirm ReachExclusive(groupID, DirectionInbound) returns the group's members.

Likely an incorrect or invalid review comment.

packages/go/analysis/ad/esc3.go (3)

38-38: LGTM - Function signature updated for LocalGroupData.

The parameter change from groupExpansions impact.PathAggregator to localGroupData *LocalGroupData aligns with the broader refactor across ADCS ESC functions.


91-97: LGTM - CalculateCrossProductNodeSets call updated.

The call correctly passes localGroupData as the first parameter, with the remaining enrollers/agents parameters preserved.


109-114: LGTM - Non-delegated path CalculateCrossProductNodeSets call updated.

Consistent with the delegated agents path above, this call correctly passes localGroupData as the first parameter.

packages/go/analysis/ad/esc4.go (6)

35-35: LGTM - Function signature updated for LocalGroupData.

The parameter change aligns with the broader ESC function refactor pattern.


63-67: LGTM - 2a scenario: Controllers cross-product updated.

The call correctly passes localGroupData for principals controlling the cert template.


71-77: LGTM - 2b scenario: Generic Write + Enroll cross-product updated.

Consistent parameter usage for the enrollment rights combination check.


88-94: LGTM - 2d scenario: PKI flags cross-product updated.

The four-way cross-product for PKI flag combinations is correctly updated.


99-105: LGTM - 2e scenario: EnrolleeSuppliesSubject path updated.

Correctly passes localGroupData when enrolleeSuppliesSubject is true.


111-117: LGTM - 2f scenario: Manager approval path updated.

Correctly passes localGroupData when requiresManagerApproval is false.

packages/go/analysis/ad/esc10.go (4)

36-36: LGTM - PostADCSESC10a signature updated.

The parameter change from groupExpansions impact.PathAggregator to localGroupData *LocalGroupData is consistent with other ESC functions.


54-54: LGTM - getVictimBitmap call updated in ESC10a.

The call correctly passes localGroupData as the first parameter for victim bitmap construction.


84-84: LGTM - PostADCSESC10b signature updated.

Consistent with PostADCSESC10a's parameter change.


102-102: LGTM - getVictimBitmap call updated in ESC10b.

Consistent parameter usage with ESC10a.

packages/go/analysis/ad/esc_shared.go (2)

30-30: LGTM - Import added for algo package.

Supports the *algo.ReachabilityCache type used in expandNodeSliceToBitmapWithoutGroups.


394-399: LGTM - getVictimBitmap updated to use LocalGroupData.

The function now correctly extracts GroupMembershipCache from localGroupData for both template and ECA bitmap expansions.

packages/go/analysis/ad/adcs.go (3)

36-36: LGTM! Signature updated to use LocalGroupData.

The function signature change from groupExpansions impact.PathAggregator to localGroupData *LocalGroupData aligns with the broader refactor replacing PathAggregator-based expansion with LocalGroupData-driven logic.


109-109: LGTM! Internal helper signature updated consistently.

The processEnterpriseCAWithValidCertChainToDomain signature change mirrors the parent function and correctly propagates localGroupData to all downstream PostADCSESC* calls.


119-207: LGTM! All PostADCSESC calls consistently updated.*

All ESC post-processing calls (ESC1, ESC3, ESC4, ESC6a, ESC6b, ESC9a, ESC9b, ESC10a, ESC10b, ESC13) correctly receive localGroupData instead of the previous groupExpansions parameter.

packages/go/analysis/ad/local_groups.go (5)

32-43: LGTM! Well-structured parallel processing setup.

The function initializes proper synchronization primitives (WaitGroups, channels) for a multi-stage pipeline: work distribution → computer data fetching → CanRDP bitmap computation → relationship posting.


73-73: Good fix for the zero-worker edge case.

Using runtime.NumCPU()/2+1 ensures at least one worker is always spawned, preventing potential hangs on single-core systems.


164-190: LGTM! Clean setup for PostLocalGroups.

The function properly defines group suffix constants and initializes the multi-stage pipeline with appropriate synchronization primitives.


221-221: Good fix for the zero-worker edge case (consistent with PostCanRDP).

Using runtime.NumCPU()/2+1 ensures at least one worker is spawned.


263-314: Database transaction errors are now logged.

Line 312 uses util.SLogError to log transaction errors, addressing the concern from the past review about silently ignoring errors.

packages/go/analysis/ad/esc6.go (4)

37-37: LGTM! Signature updated to use LocalGroupData.

The function signature change aligns with the broader refactor. The implementation correctly uses localGroupData for the CalculateCrossProductNodeSets call.


59-59: LGTM! CalculateCrossProductNodeSets call updated.

The call correctly passes localGroupData as the first argument, aligning with the updated function signature in ad.go.


82-82: LGTM! PostADCSESC6b signature updated consistently.

The signature change mirrors PostADCSESC6a for consistency across the ESC6 implementation.


104-104: LGTM! CalculateCrossProductNodeSets call updated for ESC6b.

Consistent with the ESC6a implementation.

packages/go/analysis/ad/ad.go (6)

476-481: LGTM! Signature updated to use LocalGroupData.

The function signature change from groupExpansions impact.PathAggregator to localGroupData *LocalGroupData aligns with the broader refactor. The function maintains the same contract of requiring at least 2 node slices.


499-543: LGTM! Unrolling logic refactored to use LocalGroupData.

The logic correctly:

  1. Uses localGroupData.ExcludedShortcutGroups to check if nodes should be excluded (Auth Users/Everyone)
  2. Uses localGroupData.GroupMembershipCache.Reach() to get inbound reachability for groups
  3. Checks if any reachable entities are in the excluded set
  4. Properly handles the exclusion flag to skip entire node sets when Auth Users/Everyone is found

554-564: LGTM! Simplified handling for edge cases.

The logic correctly handles:

  • When all node sets contain Auth Users/Everyone: return all first-degree principals
  • When only one node set remains without Auth Users/Everyone: return that set directly

567-575: LGTM! Reference set population updated.

Uses localGroupData.GroupMembershipCache.OrReachability to populate the unrolled reference set, maintaining equivalent semantics to the previous PathAggregator-based approach.


579-597: LGTM! Cardinality-based sorting refactored.

The temporary bitmap approach correctly:

  1. Uses GroupMembershipCache.OrReachability to compute cardinality
  2. Clears the bitmap after each use to avoid accumulation
  3. Builds a map of group IDs to their cardinalities for sorting

611-627: LGTM! Final cross-product logic updated.

Uses localGroupData.GroupMembershipCache.XorReachability for efficient set difference operations when processing shortcut groups.

packages/go/analysis/ad/ntlm.go (8)

41-47: LGTM! NTLMCache struct updated to use LocalGroupData.

The struct field change from GroupExpansions impact.PathAggregator to LocalGroupData *LocalGroupData aligns with the broader refactor.


64-64: LGTM! NewNTLMCache signature updated.

The function now accepts localGroupData *LocalGroupData and stores it in the cache for use by downstream operations.


75-75: LGTM! FetchProtectedUsersMappedToDomains call updated.

Correctly passes localGroupData.GroupMembershipCache to the updated function.


128-128: LGTM! PostNTLM signature updated.

The function now accepts localGroupData *LocalGroupData and passes it through to NewNTLMCache.


443-444: LGTM! Bitmap expansion updated to use LocalGroupData.

The expandNodeSliceToBitmapWithoutGroups calls now use ntlmCache.LocalGroupData.GroupMembershipCache instead of the previous GroupExpansions.


586-593: LGTM! PostCoerceAndRelayNTLMToSMB updated.

The admin principal expansion now uses ntlmCache.LocalGroupData.GroupMembershipCache.OrReachability for group membership unrolling.


856-856: LGTM! FetchProtectedUsersMappedToDomains signature updated.

The function now accepts *algo.ReachabilityCache directly instead of the full PathAggregator, providing a more focused interface.


869-871: LGTM! Protected users bitmap population updated.

Uses groupExpansions.OrReachability to populate the protected users bitmap, maintaining equivalent semantics.

packages/go/analysis/ad/post.go (2)

780-811: LGTM - Clear two-stage RDP entity resolution.

The implementation correctly handles both direct RIL entities with RDP group membership and members of expanded RIL groups. The comments clearly explain each stage of processing.


527-555: LGTM - Well-structured RDP entity resolution with clear branching.

The logic correctly handles the various combinations of URA enforcement, Citrix enablement, and RDP group processing. The early exit when DAU group is missing is a good optimization.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 425 to 393
func FetchComputerLocalGroupBySIDSuffix(tx graph.Transaction, computer graph.ID, groupSuffix string) (*graph.Node, error) {
if rel, err := tx.Relationships().Filter(query.And(
query.StringEndsWith(query.StartProperty(common.ObjectID.String()), groupSuffix),
query.Kind(query.Relationship(), ad.LocalToComputer),
query.InIDs(query.EndID(), computer),
)).First(); err != nil {
return nil, err
} else {
return ops.FetchNode(tx, rel.StartID)
}
var (
groupNode graph.Node
queryErr = tx.Relationships().Filter(query.And(
query.StringEndsWith(query.StartProperty(common.ObjectID.String()), groupSuffix),
query.Kind(query.Relationship(), ad.LocalToComputer),
query.InIDs(query.EndID(), computer),
)).Query(
func(results graph.Result) error {
if results.Next() {
if err := results.Scan(&groupNode); err != nil {
return err
}
}

results.Close()
return results.Error()
},
query.Returning(
query.Start(),
),
)
)

return &groupNode, queryErr
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing "not found" error when query returns no results.

When results.Next() returns false on the first call, groupNode remains a zero-value and the function returns &groupNode, nil. Callers (e.g., line 743) don't check for a zero-value node, which could cause downstream issues when accessing RemoteDesktopUsersLocalGroup.ID.

Proposed fix - return error when no results found
 func FetchComputerLocalGroupBySIDSuffix(tx graph.Transaction, computer graph.ID, groupSuffix string) (*graph.Node, error) {
 	var (
-		groupNode graph.Node
-		queryErr  = tx.Relationships().Filter(query.And(
+		groupNode *graph.Node
+		queryErr  = tx.Relationships().Filter(query.And(
 			query.StringEndsWith(query.StartProperty(common.ObjectID.String()), groupSuffix),
 			query.Kind(query.Relationship(), ad.LocalToComputer),
 			query.InIDs(query.EndID(), computer),
 		)).Query(
 			func(results graph.Result) error {
 				if results.Next() {
-					if err := results.Scan(&groupNode); err != nil {
+					var node graph.Node
+					if err := results.Scan(&node); err != nil {
 						return err
 					}
+					groupNode = &node
 				}
 
 				results.Close()
 				return results.Error()
 			},
 			query.Returning(
 				query.Start(),
 			),
 		)
 	)
 
-	return &groupNode, queryErr
+	if queryErr != nil {
+		return nil, queryErr
+	}
+	if groupNode == nil {
+		return nil, graph.ErrNoResultsFound
+	}
+	return groupNode, nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func FetchComputerLocalGroupBySIDSuffix(tx graph.Transaction, computer graph.ID, groupSuffix string) (*graph.Node, error) {
if rel, err := tx.Relationships().Filter(query.And(
query.StringEndsWith(query.StartProperty(common.ObjectID.String()), groupSuffix),
query.Kind(query.Relationship(), ad.LocalToComputer),
query.InIDs(query.EndID(), computer),
)).First(); err != nil {
return nil, err
} else {
return ops.FetchNode(tx, rel.StartID)
}
var (
groupNode graph.Node
queryErr = tx.Relationships().Filter(query.And(
query.StringEndsWith(query.StartProperty(common.ObjectID.String()), groupSuffix),
query.Kind(query.Relationship(), ad.LocalToComputer),
query.InIDs(query.EndID(), computer),
)).Query(
func(results graph.Result) error {
if results.Next() {
if err := results.Scan(&groupNode); err != nil {
return err
}
}
results.Close()
return results.Error()
},
query.Returning(
query.Start(),
),
)
)
return &groupNode, queryErr
}
func FetchComputerLocalGroupBySIDSuffix(tx graph.Transaction, computer graph.ID, groupSuffix string) (*graph.Node, error) {
var (
groupNode *graph.Node
queryErr = tx.Relationships().Filter(query.And(
query.StringEndsWith(query.StartProperty(common.ObjectID.String()), groupSuffix),
query.Kind(query.Relationship(), ad.LocalToComputer),
query.InIDs(query.EndID(), computer),
)).Query(
func(results graph.Result) error {
if results.Next() {
var node graph.Node
if err := results.Scan(&node); err != nil {
return err
}
groupNode = &node
}
results.Close()
return results.Error()
},
query.Returning(
query.Start(),
),
)
)
if queryErr != nil {
return nil, queryErr
}
if groupNode == nil {
return nil, graph.ErrNoResultsFound
}
return groupNode, nil
}
🧰 Tools
🪛 GitHub Actions: Run Go Unit Tests

[error] Build failed due to compilation error in post.go (too many arguments in call).

Comment on lines 452 to 426
func FetchComputerLocalGroupIDBySIDSuffix(tx graph.Transaction, computer graph.ID, groupSuffix string) (graph.ID, error) {
var (
startID graph.ID
queryErr = tx.Relationships().Filter(query.And(
query.StringEndsWith(query.StartProperty(common.ObjectID.String()), groupSuffix),
query.Kind(query.Relationship(), ad.LocalToComputer),
query.InIDs(query.EndID(), computer),
)).Query(
func(results graph.Result) error {
if results.Next() {
if err := results.Scan(&startID); err != nil {
return err
}
}

results.Close()
return results.Error()
},
query.Returning(
query.StartID(),
),
)
)

return startID, queryErr
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Similar issue: returns zero ID when no results found.

Same pattern as FetchComputerLocalGroupBySIDSuffix - when no results are found, a zero graph.ID is returned without an error. Consider returning graph.ErrNoResultsFound when no matching local group exists.

🧰 Tools
🪛 GitHub Actions: Run Go Unit Tests

[error] Build failed due to compilation error in post.go (too many arguments in call).

🤖 Prompt for AI Agents
In `@packages/go/analysis/ad/post.go` around lines 452 - 477,
FetchComputerLocalGroupIDBySIDSuffix currently returns a zero graph.ID when no
rows match; update the query callback (or post-query handling) to return
graph.ErrNoResultsFound instead of silently returning nil error when
results.Next() is false — e.g., inside the anonymous Query callback for
FetchComputerLocalGroupIDBySIDSuffix, if results.Next() is false return
graph.ErrNoResultsFound (or set queryErr accordingly) so callers receive a clear
no-results error similar to FetchComputerLocalGroupBySIDSuffix.

Copy link
Contributor

@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: 4

Caution

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

⚠️ Outside diff range comments (3)
cmd/api/src/daemons/datapipe/datapipe.go (1)

76-103: Dead code after early return.

All code from line 76 onwards is unreachable due to the unconditional return on line 74. This includes:

  • Timer initialization (lines 76-79)
  • Deferred cleanup (lines 81-82)
  • The pipeline.Start call (line 84)
  • The entire daemon loop handling pruning, deletion, ingestion, and periodic analysis (lines 86-103)

If the early return was intentional for testing, this dead code should be removed. If the early return was accidental, remove lines 73-75 instead to restore normal daemon operation.

packages/go/analysis/ad/esc_shared.go (1)

309-331: Misleading function name: expandNodeSliceToBitmapWithoutGroups includes groups.

The function name suggests groups are excluded, but the group detection logic (lines 316-320) is commented out, causing isGroup to always be false. This means all expanded members—including groups—are added to the bitmap.

While the caller in getVictimBitmap explicitly expects this behavior ("we don't do group shortcutting here"), the function name is misleading. This creates a maintenance risk: future developers may incorrectly assume the function excludes groups based on its name alone.

Either rename the function to reflect that it includes groups, or implement the group filtering logic properly.

packages/go/analysis/ad/post.go (1)

209-226: Use query.NodeProperty() instead of query.StartProperty() and query.EndProperty() for node queries.

FetchAdminGroups queries tx.Nodes() but uses query.StartProperty() and query.EndProperty() in the filter criteria. These property accessors are only for relationship queries, not node queries. Use query.NodeProperty() instead, and remove the unnecessary OR logic since a node has a single ObjectID, not start/end variants. This pattern is used correctly throughout the codebase (e.g., in ad.go and agt.go).

Proposed fix
 func FetchAdminGroups(ctx context.Context, db graph.Database) (cardinality.Duplex[uint64], error) {
 	computerNodeIds := cardinality.NewBitmap64()

 	return computerNodeIds, db.ReadTransaction(ctx, func(tx graph.Transaction) error {
 		return tx.Nodes().Filter(query.And(
-			query.Or(
-				query.StringEndsWith(query.StartProperty(common.ObjectID.String()), wellknown.AdministratorsSIDSuffix.String()),
-				query.StringEndsWith(query.EndProperty(common.ObjectID.String()), wellknown.AdministratorsSIDSuffix.String()),
-			),
+			query.StringEndsWith(query.NodeProperty(common.ObjectID.String()), wellknown.AdministratorsSIDSuffix.String()),
 		)).FetchIDs(func(cursor graph.Cursor[graph.ID]) error {
 			for id := range cursor.Chan() {
 				computerNodeIds.Add(id.Uint64())
 			}

 			return nil
 		})
 	})
 }
🤖 Fix all issues with AI agents
In `@cmd/api/src/analysis/ad/post.go`:
- Around line 32-52: Tests call removed/renamed APIs: remove any invocation of
adAnalysis.ExpandAllRDPLocalGroups and update calls to
adAnalysis.FetchCanRDPEntityBitmapForComputer to the new signature by
constructing a CanRDPComputerData value (populate fields that used to be passed:
tx/computerID/groupExpansions) and calling
FetchCanRDPEntityBitmapForComputer(computerData, enforceURA, citrixEnabled);
adjust test setup/mocks to provide the CanRDPComputerData instance and the two
boolean flags rather than the old parameter set.

In `@cmd/api/src/daemons/datapipe/datapipe.go`:
- Around line 73-75: The early return after calling s.WithDatapipeStatus(ctx,
model.DatapipeStatusAnalyzing, s.pipeline.Analyze) short-circuits the daemon so
the main loop (pruning, ingestion, periodic analysis, context cancellation
handling) never runs; remove that return and restore the surrounding daemon loop
so the call to WithDatapipeStatus/Analyze runs as part of the periodic/looped
workflow (or gate it behind a test flag), ensuring the loop that performs
pruning, ingestion and context.Done handling continues to execute.

In `@packages/go/analysis/ad/local_groups.go`:
- Around line 32-42: Tests use removed/changed RDP APIs: update integration
tests in cmd/api/src/analysis/analysis_integration_test.go to use the new
signatures by removing any calls to ExpandAllRDPLocalGroups and replacing
FetchCanRDPEntityBitmapForComputer(tx, computerID, groupExpansions, enforceURA,
citrixEnabled) with the new call FetchCanRDPEntityBitmapForComputer(computerData
*CanRDPComputerData, enforceURA bool, citrixEnabled bool); construct or obtain a
*CanRDPComputerData for the computer under test (populating fields previously
provided by groupExpansions/tx) and pass enforceURA/citrix flags as before, then
adjust assertions to reflect any difference in returned bitmap semantics after
removing ExpandAllRDPLocalGroups.

In `@packages/go/analysis/ad/post.go`:
- Around line 535-563: The test calls still use the old signature passing (tx,
computerID, groupExpansions, enforceURA, citrixEnabled); update each of the
seven callers in cmd/api/src/analysis/analysis_integration_test.go to build a
CanRDPComputerData instance (populating fields used by
FetchCanRDPEntityBitmapForComputer such as Computer, CanRDPData,
RemoteDesktopUsersLocalGroup, DAUGroup, LocalGroupData,
RemoteInteractiveLogonRightEntities and any helper state like ComputersWithURA)
and then call FetchCanRDPEntityBitmapForComputer(computerData, enforceURA,
citrixEnabled) with the two boolean flags; ensure helper test setup that
previously produced tx/computerID/groupExpansions now returns or is converted
into the matching CanRDPComputerData structure so callers match the new
signature.
♻️ Duplicate comments (2)
packages/go/analysis/ad/post.go (2)

456-485: Similar issue: Returns zero ID when no results found.

Same pattern as FetchComputerLocalGroupBySIDSuffix - when no results are found, a zero graph.ID is returned without an error. Consider returning graph.ErrNoResultsFound when no matching local group exists.

Proposed fix
 func FetchComputerLocalGroupIDBySIDSuffix(tx graph.Transaction, computer graph.ID, groupSuffix string) (graph.ID, error) {
 	var (
-		startID graph.ID
+		startID graph.ID
+		found   bool
 		err     = tx.Relationships().Filter(query.And(
 			// ... query unchanged ...
 		)).Query(
 			func(results graph.Result) error {
 				if results.Next() {
 					if err := results.Scan(&startID); err != nil {
 						return err
 					}
+					found = true
 				}

 				results.Close()
 				return results.Error()
 			},
 			// ... returning unchanged ...
 		)
 	)

 	if err != nil {
 		return 0, err
 	}
+	if !found {
+		return 0, graph.ErrNoResultsFound
+	}

 	return startID, nil
 }

425-454: Bug: Returns zero-value node when no results found.

When the query returns no results, groupNode remains a zero-value graph.Node, and &groupNode is returned with nil error. Callers (e.g., line 751 in ComputerData) don't check for a zero-value node, which could cause incorrect behavior when accessing RemoteDesktopUsersLocalGroup.ID.

Proposed fix - return error when no results found
 func FetchComputerLocalGroupBySIDSuffix(tx graph.Transaction, computer graph.ID, groupSuffix string) (*graph.Node, error) {
 	var (
-		groupNode graph.Node
+		groupNode *graph.Node
 		err       = tx.Relationships().Filter(query.And(
 			query.StringEndsWith(query.StartProperty(common.ObjectID.String()), groupSuffix),
 			query.Kind(query.Relationship(), ad.LocalToComputer),
 			query.InIDs(query.EndID(), computer),
 		)).Query(
 			func(results graph.Result) error {
 				if results.Next() {
-					if err := results.Scan(&groupNode); err != nil {
+					var node graph.Node
+					if err := results.Scan(&node); err != nil {
 						return err
 					}
+					groupNode = &node
 				}

 				results.Close()
 				return results.Error()
 			},
 			query.Returning(
 				query.Start(),
 			),
 		)
 	)

 	if err != nil {
 		return nil, err
 	}
+	if groupNode == nil {
+		return nil, graph.ErrNoResultsFound
+	}

-	return &groupNode, nil
+	return groupNode, nil
 }
🧹 Nitpick comments (6)
packages/go/analysis/ad/local_groups.go (1)

274-308: Consider logging unexpected errors from group lookups.

The pattern of checking err == nil before proceeding is reasonable when the group may not exist. However, this silently ignores all errors, including unexpected ones (e.g., database connection issues).

Consider distinguishing between "group not found" (expected) and other errors (unexpected):

♻️ Suggested improvement
-						if localAdminGroupErr == nil {
+						if localAdminGroupErr == nil {
 							numGroupsProcessed.Add(1)

 							channels.Submit(ctx, reachC, reachJob{
 								targetComputer: computerID,
 								targetGroup:    localAdminGroup.Uint64(),
 								groupSuffix:    adminGroupSuffix,
 							})
+						} else if !graph.IsErrNotFound(localAdminGroupErr) {
+							slog.Warn("Unexpected error fetching admin group", slog.String("err", localAdminGroupErr.Error()))
 						}
cmd/api/src/analysis/post_integration_test.go (1)

88-91: Consider renaming excludedGroups to localGroupData for clarity.

The variable is named excludedGroups, but FetchLocalGroupData returns a *LocalGroupData struct containing multiple fields (Computers, GroupMembershipCache, ExcludedShortcutGroups, LocalGroupMembershipDigraph). The current name only reflects one of those fields.

♻️ Suggested rename
-		excludedGroups, err := ad2.FetchLocalGroupData(context.Background(), graphDB)
+		localGroupData, err := ad2.FetchLocalGroupData(context.Background(), graphDB)
 		require.Nilf(t, err, "%v", err)

-		results := ad2.CalculateCrossProductNodeSets(excludedGroups, firstSet, secondSet)
+		results := ad2.CalculateCrossProductNodeSets(localGroupData, firstSet, secondSet)

This same rename should be applied consistently across all four test functions (Lines 88-91, 105-108, 122-125, 139-142).

packages/go/analysis/ad/ntlm.go (1)

64-83: Consider adding nil check for localGroupData parameter.

If localGroupData is nil, dereferencing localGroupData.GroupMembershipCache on line 75 will cause a panic. While callers should provide a valid pointer, a defensive nil check at the function entry would improve robustness.

Proposed defensive check
 func NewNTLMCache(ctx context.Context, db graph.Database, localGroupData *LocalGroupData) (NTLMCache, error) {
+	if localGroupData == nil {
+		return NTLMCache{}, fmt.Errorf("localGroupData cannot be nil")
+	}
+
 	var (
 		ntlmCache                   = NTLMCache{}
 		unprotectedComputerCache    = make(map[string]cardinality.Duplex[uint64])
packages/go/analysis/ad/post.go (2)

592-627: Minor redundancy in property check.

The query already filters with query.Equals(query.NodeProperty(ad.HasURA.String()), true) at line 601, but line 612 again checks if propertyValue before adding to the bitmap. This double-check is unnecessary since the filter guarantees only true values reach this point.

Simplified version
 	if err := tx.Nodes().Filter(
 		query.And(
 			query.Kind(query.Node(), ad.Computer),
 			query.Equals(query.NodeProperty(ad.HasURA.String()), true),
 		),
 	).Query(func(results graph.Result) error {
 		for results.Next() {
 			var (
-				nodeID        graph.ID
-				propertyValue bool
+				nodeID graph.ID
 			)

-			if err := results.Scan(&nodeID, &propertyValue); err != nil {
+			if err := results.Scan(&nodeID); err != nil {
 				return err
-			} else if propertyValue {
-				nodesWithURA.Add(nodeID.Uint64())
 			}
+			nodesWithURA.Add(nodeID.Uint64())
 		}

 		return results.Error()
 	}, query.Returning(
 		query.NodeID(),
-		query.NodeProperty(ad.HasURA.String()),
 	)); err != nil {

629-670: LGTM! Consider documenting ExcludedShortcutGroups.

The LocalGroupData structure and FetchLocalGroupData implementation are well-designed. The TODO comment on line 632 for ExcludedShortcutGroups should be addressed in a follow-up.

Would you like me to open an issue to track the documentation of ExcludedShortcutGroups?

go.mod (1)

87-87: LGTM: Indirect dependency updates.

The indirect dependency updates appear reasonable and are automatically managed by the Go module system. The golang.org/x package pseudo-versions are recent (early January 2026) and align with typical Go toolchain updates.

For additional assurance, you may optionally verify there are no security advisories for the golang.org/x packages:

#!/bin/bash
# Optional: Check golang.org/x packages for vulnerabilities

echo "=== Checking golang.org/x packages ==="

for pkg in "golang.org/x/net" "golang.org/x/sys" "golang.org/x/exp" "golang.org/x/crypto"; do
  echo "--- $pkg ---"
  gh api graphql -f query="
  {
    securityVulnerabilities(first: 3, ecosystem: GO, package: \"$pkg\") {
      nodes {
        advisory {
          summary
          severity
        }
        vulnerableVersionRange
      }
    }
  }"
  echo
done

Also applies to: 276-276, 278-278, 280-280, 281-281

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b44edf3 and 68f1667.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (22)
  • cmd/api/src/analysis/ad/post.go
  • cmd/api/src/analysis/membership_integration_test.go
  • cmd/api/src/analysis/post_integration_test.go
  • cmd/api/src/daemons/datapipe/datapipe.go
  • cmd/api/src/test/integration/harnesses.go
  • go.mod
  • packages/go/analysis/ad/ad.go
  • packages/go/analysis/ad/adcs.go
  • packages/go/analysis/ad/adcscache.go
  • packages/go/analysis/ad/esc1.go
  • packages/go/analysis/ad/esc10.go
  • packages/go/analysis/ad/esc13.go
  • packages/go/analysis/ad/esc3.go
  • packages/go/analysis/ad/esc4.go
  • packages/go/analysis/ad/esc6.go
  • packages/go/analysis/ad/esc9.go
  • packages/go/analysis/ad/esc_shared.go
  • packages/go/analysis/ad/local_groups.go
  • packages/go/analysis/ad/membership.go
  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/owns.go
  • packages/go/analysis/ad/post.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/go/analysis/ad/esc1.go
  • cmd/api/src/test/integration/harnesses.go
  • packages/go/analysis/ad/esc3.go
🧰 Additional context used
🧠 Learnings (12)
📚 Learning: 2025-07-22T20:30:34.839Z
Learnt from: LawsonWillard
Repo: SpecterOps/BloodHound PR: 1700
File: cmd/api/src/api/v2/saved_queries_test.go:3182-3182
Timestamp: 2025-07-22T20:30:34.839Z
Learning: In Go table-driven tests in cmd/api/src/api/v2/saved_queries_test.go, subtest parallelization with t.Parallel() is acceptable when tests are self-contained, each creating their own mock controller (gomock.NewController(t)) and having isolated mock expectations without shared state between subtests.

Applied to files:

  • cmd/api/src/analysis/post_integration_test.go
📚 Learning: 2025-06-06T23:12:14.181Z
Learnt from: elikmiller
Repo: SpecterOps/BloodHound PR: 1563
File: packages/go/graphschema/azure/azure.go:24-24
Timestamp: 2025-06-06T23:12:14.181Z
Learning: In BloodHound, files in packages/go/graphschema/*/`*.go` are generated from CUE schemas. When `just prepare-for-codereview` is run, it triggers code generation that may automatically add import aliases or other formatting changes. These changes are legitimate outputs of the generation process, not manual edits that would be overwritten.

Applied to files:

  • packages/go/analysis/ad/ad.go
  • packages/go/analysis/ad/esc13.go
  • go.mod
  • packages/go/analysis/ad/esc_shared.go
  • packages/go/analysis/ad/adcs.go
  • packages/go/analysis/ad/esc6.go
  • packages/go/analysis/ad/esc10.go
  • cmd/api/src/analysis/membership_integration_test.go
  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/esc9.go
  • packages/go/analysis/ad/esc4.go
  • packages/go/analysis/ad/owns.go
📚 Learning: 2025-06-11T20:49:35.177Z
Learnt from: kpom-specter
Repo: SpecterOps/BloodHound PR: 1562
File: cmd/api/src/daemons/datapipe/datapipe.go:100-120
Timestamp: 2025-06-11T20:49:35.177Z
Learning: In BloodHound's datapipe architecture, only the datapipe (e.g., `WithDatapipeStatus`) is responsible for mutating the datapipe status; pipeline actions themselves do not alter the status and instead communicate any job-level state elsewhere.

Applied to files:

  • cmd/api/src/daemons/datapipe/datapipe.go
📚 Learning: 2025-07-10T14:31:01.781Z
Learnt from: JonasBK
Repo: SpecterOps/BloodHound PR: 1671
File: packages/go/analysis/ad/esc16.go:65-69
Timestamp: 2025-07-10T14:31:01.781Z
Learning: In BloodHound ADCS ESC analysis functions (PostADCSESC1, PostADCSESC4, PostADCSESC6, PostADCSESC9, PostADCSESC10, PostADCSESC13, PostADCSESC16, etc.), the established pattern for handling cert template validation errors and user filtering errors is to use slog.WarnContext to log warnings and continue processing with the next template, rather than returning errors to the caller. This pattern is consistently applied across all ADCS ESC edge implementations.

Applied to files:

  • packages/go/analysis/ad/esc13.go
  • packages/go/analysis/ad/esc6.go
  • packages/go/analysis/ad/esc10.go
  • packages/go/analysis/ad/esc9.go
  • cmd/api/src/analysis/ad/post.go
  • packages/go/analysis/ad/esc4.go
📚 Learning: 2025-11-05T21:13:08.542Z
Learnt from: ktstrader
Repo: SpecterOps/BloodHound PR: 2020
File: cmd/api/src/queries/graph.go:198-203
Timestamp: 2025-11-05T21:13:08.542Z
Learning: In cmd/api/src/queries/graph.go, when ETAC filtering is enabled in GetAssetGroupComboNode, an empty etacAllowedList (when user.AllEnvironments is false and user.EnvironmentTargetedAccessControl is empty) is intentional and should not be guarded against. The empty list will cause query.In() to filter out all nodes, which is the correct security behavior—users with no environment assignments should see no results.

Applied to files:

  • packages/go/analysis/ad/esc13.go
  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/post.go
📚 Learning: 2025-06-30T22:58:54.834Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1629
File: go.mod:18-18
Timestamp: 2025-06-30T22:58:54.834Z
Learning: Go 1.24 supports patch versions in the go directive of go.mod files. Syntax like "go 1.24.4" is valid and specifies the minimum Go version required to build the module, including the patch level.

Applied to files:

  • go.mod
📚 Learning: 2025-08-26T18:13:36.643Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1820
File: .github/workflows/audit-release.yml:0-0
Timestamp: 2025-08-26T18:13:36.643Z
Learning: The go tool command supports custom/third-party binaries as of Go 1.24, allowing invocation of custom CLI tools like "go tool stbernard" rather than being limited to only built-in Go toolchain binaries.

Applied to files:

  • go.mod
📚 Learning: 2025-06-25T18:24:25.014Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1606
File: cmd/api/src/analysis/azure/post.go:33-35
Timestamp: 2025-06-25T18:24:25.014Z
Learning: In BloodHound Go code, for error types in slog structured logging, prefer using slog.String("key", err.Error()) over slog.Any("key", err). The explicit string conversion with err.Error() is preferred over using slog.Any() for error types.

Applied to files:

  • packages/go/analysis/ad/local_groups.go
  • packages/go/analysis/ad/owns.go
📚 Learning: 2025-06-25T17:52:33.291Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1606
File: cmd/api/src/analysis/azure/post.go:33-35
Timestamp: 2025-06-25T17:52:33.291Z
Learning: In BloodHound Go code, prefer using explicit slog type functions like slog.Any(), slog.String(), slog.Int(), etc. over simple key-value pairs for structured logging. This provides better type safety and makes key-value pairs more visually distinct. For error types, use slog.Any("key", err) or slog.String("key", err.Error()).

Applied to files:

  • packages/go/analysis/ad/local_groups.go
  • packages/go/analysis/ad/adcs.go
  • packages/go/analysis/ad/owns.go
📚 Learning: 2025-05-23T19:56:13.423Z
Learnt from: definitelynotagoblin
Repo: SpecterOps/BloodHound PR: 1440
File: packages/go/analysis/ad/ntlm.go:123-138
Timestamp: 2025-05-23T19:56:13.423Z
Learning: In the BloodHound NTLM analysis code (packages/go/analysis/ad/ntlm.go), the `isProtectedComputer` function intentionally fails open (returns false/unprotected) when the Protected Users group cannot be retrieved, maintaining consistency with the original logic patterns in the codebase.

Applied to files:

  • packages/go/analysis/ad/local_groups.go
  • packages/go/analysis/ad/ntlm.go
  • cmd/api/src/analysis/ad/post.go
  • packages/go/analysis/ad/post.go
📚 Learning: 2025-05-27T16:58:33.295Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1503
File: cmd/api/src/services/job/jobs_test.go:19-143
Timestamp: 2025-05-27T16:58:33.295Z
Learning: Tests in cmd/api/src/services/job/jobs_test.go have been found to be flaky in the past and are due for rewrite. They should be skipped with t.Skip() until they can be properly rewritten.

Applied to files:

  • cmd/api/src/analysis/membership_integration_test.go
📚 Learning: 2025-07-10T14:33:20.317Z
Learnt from: JonasBK
Repo: SpecterOps/BloodHound PR: 1671
File: cmd/api/src/analysis/ad/adcs_integration_test.go:3687-3687
Timestamp: 2025-07-10T14:33:20.317Z
Learning: When reviewing Go code, functions defined in one file within a package are accessible from other files in the same package. Before flagging missing functions as compilation errors, check if they exist in other files within the same package directory.

Applied to files:

  • packages/go/analysis/ad/owns.go
🧬 Code graph analysis (11)
cmd/api/src/analysis/post_integration_test.go (2)
packages/go/analysis/ad/post.go (1)
  • FetchLocalGroupData (636-670)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (477-638)
packages/go/analysis/ad/ad.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (629-634)
packages/go/graphschema/ad/ad.go (3)
  • Group (31-31)
  • LocalGroup (36-36)
  • Contains (54-54)
packages/go/analysis/ad/esc13.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (629-634)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (477-638)
packages/go/analysis/ad/esc_shared.go (1)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (629-634)
packages/go/analysis/ad/local_groups.go (2)
packages/go/analysis/ad/post.go (4)
  • LocalGroupData (629-634)
  • CanRDPComputerData (728-735)
  • FetchCanRDPData (702-708)
  • FetchCanRDPEntityBitmapForComputer (535-563)
packages/go/analysis/analysis.go (1)
  • MaximumDatabaseParallelWorkers (36-36)
packages/go/analysis/ad/esc6.go (3)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (629-634)
packages/go/analysis/ad/adcscache.go (1)
  • ADCSCache (33-55)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (477-638)
packages/go/analysis/ad/esc10.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (629-634)
packages/go/analysis/ad/adcscache.go (1)
  • ADCSCache (33-55)
packages/go/analysis/ad/ntlm.go (1)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (629-634)
packages/go/analysis/ad/esc9.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (629-634)
packages/go/analysis/ad/adcscache.go (1)
  • ADCSCache (33-55)
cmd/api/src/analysis/ad/post.go (1)
packages/go/analysis/ad/post.go (2)
  • FetchLocalGroupData (636-670)
  • PostDCSync (74-104)
packages/go/analysis/ad/owns.go (1)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (629-634)
🪛 golangci-lint (2.5.0)
packages/go/analysis/ad/local_groups.go

[major] 42-42: : # github.com/specterops/bloodhound/cmd/api/src/analysis_test [github.com/specterops/bloodhound/cmd/api/src/analysis.test]
cmd/api/src/analysis/analysis_integration_test.go:42:38: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:46:130: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want ("github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:65:38: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:70:129: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want (
"github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:97:129: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want ("github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:126:37: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:131:129: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want (
"github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:154:38: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:159:146: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want ("github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:175:146: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want (
"github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:175:146: too many errors

(typecheck)

cmd/api/src/analysis/ad/post.go

[major] 42-42: : # github.com/specterops/bloodhound/cmd/api/src/analysis_test [github.com/specterops/bloodhound/cmd/api/src/analysis.test]
cmd/api/src/analysis/analysis_integration_test.go:42:38: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:46:130: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want ("github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:65:38: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:70:129: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want (
"github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:97:129: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want ("github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:126:37: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:131:129: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want (
"github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:154:38: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:159:146: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want ("github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:175:146: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want (
"github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:175:146: too many errors

(typecheck)

packages/go/analysis/ad/owns.go

[major] 42-42: : # github.com/specterops/bloodhound/cmd/api/src/analysis_test [github.com/specterops/bloodhound/cmd/api/src/analysis.test]
cmd/api/src/analysis/analysis_integration_test.go:42:38: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:46:130: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want ("github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:65:38: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:70:129: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want (
"github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:97:129: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want ("github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:126:37: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:131:129: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want (
"github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:154:38: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:159:146: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want ("github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:175:146: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want (
"github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:175:146: too many errors

(typecheck)

packages/go/analysis/ad/post.go

[major] 42-42: : # github.com/specterops/bloodhound/cmd/api/src/analysis_test [github.com/specterops/bloodhound/cmd/api/src/analysis.test]
cmd/api/src/analysis/analysis_integration_test.go:42:38: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:46:130: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want ("github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:65:38: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:70:129: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want (
"github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:97:129: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want ("github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:126:37: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:131:129: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want (
"github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:154:38: undefined: adAnalysis.ExpandAllRDPLocalGroups
cmd/api/src/analysis/analysis_integration_test.go:159:146: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want ("github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:175:146: too many arguments in call to adAnalysis.FetchCanRDPEntityBitmapForComputer
have (graph.Transaction, graph.ID, unknown type, bool, bool)
want (
"github.com/specterops/bloodhound/packages/go/analysis/ad".CanRDPComputerData, bool, bool)
cmd/api/src/analysis/analysis_integration_test.go:175:146: too many errors

(typecheck)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build BloodHound Container Image / Build and Package Container
  • GitHub Check: run-tests
  • GitHub Check: run-analysis
  • GitHub Check: build-ui
  • GitHub Check: run-go-unit-tests
🔇 Additional comments (44)
packages/go/analysis/ad/membership.go (1)

92-94: LGTM!

Clean simplification using direction.Reverse() directly instead of a separate variable. The logic remains equivalent and the code is more concise.

packages/go/analysis/ad/esc_shared.go (1)

394-419: LGTM on signature update.

The function signature change to accept *LocalGroupData aligns with the broader refactor. The internal logic correctly uses localGroupData.GroupMembershipCache for the bitmap expansions.

packages/go/analysis/ad/local_groups.go (1)

32-166: Well-structured concurrent processing pipeline.

The function correctly addresses previous concerns:

  • Worker count uses runtime.NumCPU()/2+1 ensuring at least one worker (line 81)
  • Channel closing is properly sequenced after all producers finish
  • Database transaction errors are now logged (lines 146-148)
  • Error from FetchCanRDPData is checked before starting goroutines, avoiding orphaned channel readers

The three-stage pipeline (work fetchers → computer processors → batch writer) with proper wait group coordination is clean.

packages/go/analysis/ad/adcscache.go (1)

75-77: LGTM!

Good addition of timing instrumentation using util.SLogMeasureFunction. The deferred call pattern correctly measures the entire BuildCache execution time.

cmd/api/src/analysis/membership_integration_test.go (1)

77-92: LGTM!

The test correctly adapts to the new algo.FetchReachabilityCache API:

  • Uses query.KindIn(query.Relationship(), ad.MemberOf, ad.MemberOfLocalGroup) to filter relevant relationship types
  • Assertions updated to use ReachExclusive(..., graph.DirectionInbound).Cardinality() which returns the count of nodes that can reach each group

The expected cardinalities remain unchanged, validating the new implementation maintains behavioral parity.

packages/go/analysis/ad/esc13.go (2)

36-36: LGTM - Function signature updated to use LocalGroupData.

The signature change from groupExpansions impact.PathAggregator to localGroupData *LocalGroupData aligns with the broader refactoring to use a unified data carrier for cross-product calculations.


53-53: LGTM - CalculateCrossProductNodeSets call updated correctly.

The call site properly passes localGroupData as the first argument, consistent with the new function signature in ad.go. The remaining arguments (ecaEnrollers, cache.GetCertTemplateEnrollers(template.ID)) are unchanged, preserving the cross-product logic.

cmd/api/src/analysis/post_integration_test.go (1)

84-93: LGTM - Test properly migrated to new LocalGroupData flow.

The test correctly:

  1. Uses the updated callback signature with graphDB
  2. Fetches local group data before cross-product calculation
  3. Passes the data to CalculateCrossProductNodeSets
  4. Validates results with appropriate assertions
packages/go/analysis/ad/esc4.go (2)

35-35: LGTM - Function signature updated to use LocalGroupData.

The signature change from groupExpansions impact.PathAggregator to localGroupData *LocalGroupData is consistent with the pattern applied across all ESC post-processing functions.


62-117: LGTM - All CalculateCrossProductNodeSets call sites correctly updated.

All five cross-product calculations (2a, 2b, 2d, 2e, 2f) properly pass localGroupData as the first argument while preserving the remaining node set arguments. The logic flow and OR operations with principals bitmap remain unchanged.

cmd/api/src/analysis/ad/post.go (1)

29-67: LGTM - Post-processing pipeline properly refactored to use LocalGroupData.

The orchestration flow is well-structured:

  1. Deletes transit edges first
  2. Fetches localGroupData once and reuses it across all subsequent Post* calls
  3. Sequential execution with proper error propagation at each step
  4. Stats aggregation at the end

This addresses the previous review concern about disabled post-processing - the pipeline now executes all steps correctly.

packages/go/analysis/ad/owns.go (2)

38-45: LGTM - PostOwnsAndWriteOwner properly migrated to LocalGroupData.

The function correctly:

  1. Accepts localGroupData *LocalGroupData parameter
  2. Extracts GroupMembershipCache and passes it to FetchAdminGroupIds

This maintains the separation of concerns while adapting to the new data carrier pattern.


196-213: LGTM - FetchAdminGroupIds refactored to use ReachabilityCache.

The function now:

  1. Accepts *algo.ReachabilityCache instead of the previous PathAggregator
  2. Uses OrReachability to accumulate group members into adminIds

The OrReachability call on line 208 correctly expands each admin group (DA/EA) by adding all principals that can reach that group via inbound membership edges, which is the intended behavior for determining admin group membership.

packages/go/analysis/ad/esc9.go (2)

35-35: Signature and parameter migration look correct for ESC9a.

The function signature update to accept *LocalGroupData and the corresponding getVictimBitmap call are consistent with the broader refactor. The error handling pattern using slog.WarnContext and continue aligns with the established ADCS ESC pattern. Based on learnings, this follows the expected convention.

Also applies to: 53-53


84-84: Signature and parameter migration look correct for ESC9b.

Consistent with ESC9a changes. The localGroupData parameter is correctly propagated to getVictimBitmap.

Also applies to: 102-102

packages/go/analysis/ad/esc10.go (2)

36-36: Signature and parameter migration look correct for ESC10a.

The migration from groupExpansions to localGroupData is correctly implemented. The getVictimBitmap call properly receives the new parameter.

Also applies to: 54-54


84-84: Signature and parameter migration look correct for ESC10b.

Consistent with ESC10a changes and the broader LocalGroupData refactor pattern.

Also applies to: 102-102

packages/go/analysis/ad/ad.go (5)

500-543: Unrolling logic correctly migrated to LocalGroupData.

The refactored code:

  1. Uses localGroupData.ExcludedShortcutGroups.Contains() to check for Auth Users/Everyone directly
  2. Retrieves group membership via localGroupData.GroupMembershipCache.Reach()
  3. Correctly breaks out of entity processing when an excluded group is found in the reachability set

The nested iteration at lines 523-529 efficiently short-circuits when an excluded node is detected.


554-564: Edge case handling simplified and correct.

The conditional structure properly handles:

  • Empty firstDegreeSets (all contain excluded groups) → return all first-degree entities
  • Single firstDegreeSet → return that set directly
  • Two or more sets → proceed with cross-product calculation

579-597: Cardinality mapping uses proper bitmap reuse pattern.

The tempBitmap is correctly cleared after each use (line 590) to prevent cardinality accumulation across iterations. This is a good memory-efficient pattern.


611-627: Group shortcutting logic correctly uses XorReachability.

The use of XorReachability at line 622 properly removes group members from the reference set when a group is matched, preventing redundant checks of already-covered entities.


477-481: All call sites of CalculateCrossProductNodeSets have been correctly updated with the new signature.

The function no longer requires a graph.Transaction parameter. All 22 references across production code (esc1.go, esc3.go, esc4.go, esc6.go, esc13.go, post.go) and tests (post_integration_test.go) properly pass localGroupData as the first parameter, which is fetched via FetchLocalGroupData(). This decouples the cross-product calculation from database access, improving the design.

packages/go/analysis/ad/adcs.go (3)

36-36: PostADCS entry point correctly updated.

The signature change to accept *LocalGroupData instead of impact.PathAggregator aligns with the broader refactor. The function correctly passes this to downstream processing.


109-109: Internal helper signature correctly updated.

The processEnterpriseCAWithValidCertChainToDomain function signature properly includes localGroupData to propagate it to all ESC processors.


119-207: All ESC post-processing calls consistently updated.

All 11 ADCS ESC processors (ESC1, ESC3, ESC4, ESC6a, ESC6b, ESC9a, ESC9b, ESC10a, ESC10b, ESC13) correctly receive localGroupData. The PostGoldenCert call at line 111 appropriately does not receive localGroupData as it doesn't require group expansion data.

packages/go/analysis/ad/esc6.go (2)

37-37: PostADCSESC6a correctly migrated to LocalGroupData.

The signature change and CalculateCrossProductNodeSets call are properly updated. The call now omits the tx parameter (which was removed from the function signature) and correctly passes localGroupData as the first argument.

Also applies to: 59-59


82-82: PostADCSESC6b correctly migrated to LocalGroupData.

Consistent with ESC6a changes. The function signature and CalculateCrossProductNodeSets call are properly aligned with the new LocalGroupData-based API.

Also applies to: 104-104

packages/go/analysis/ad/ntlm.go (6)

27-27: LGTM!

Import addition is appropriate for the new algo.ReachabilityCache type usage.


41-47: LGTM!

The struct field change from GroupExpansions to LocalGroupData aligns with the refactoring objective to centralize group membership data.


128-166: LGTM!

The signature change and internal usage of localGroupData are consistent with the refactoring pattern.


443-444: LGTM!

The bitmap expansion now correctly uses the GroupMembershipCache from LocalGroupData.


586-591: LGTM!

The group membership expansion correctly uses OrReachability from the GroupMembershipCache.


856-871: LGTM!

The function signature and implementation correctly use *algo.ReachabilityCache for group expansion, maintaining consistency with the new data carrier approach.

packages/go/analysis/ad/post.go (10)

29-36: LGTM!

New imports are necessary for the introduced LocalGroupData, CanRDPData structures, and associated helper functions.


39-47: LGTM!

The signature change to accept *LocalGroupData is consistent with the refactoring pattern.


74-83: LGTM!

Consistent with the PostSyncLAPSPassword refactoring pattern.


187-207: LGTM!

Clean implementation with appropriate error handling and timing measurement for observability.


288-303: LGTM!

The signature change and usage of CalculateCrossProductNodeSets with LocalGroupData is consistent with the refactoring.


502-510: Potential nil pointer dereference risk.

This function accesses canRDPData.RemoteDesktopUsersLocalGroup.ID without checking if RemoteDesktopUsersLocalGroup is nil. While ComputerData (line 751) fetches this, if FetchComputerLocalGroupBySIDSuffix returns a zero-value node (as noted in the earlier issue), this could lead to incorrect behavior. This is a downstream effect of the FetchComputerLocalGroupBySIDSuffix issue.


565-590: LGTM!

The query error handling has been fixed - the error from Query() is now properly captured and returned.


737-764: ComputerData doesn't detect missing Remote Desktop Users group.

At line 751-755, if FetchComputerLocalGroupBySIDSuffix returns a zero-value node (when no group is found), the error will be nil and canRDPData.RemoteDesktopUsersLocalGroup will point to an empty node. This could lead to incorrect behavior in downstream processing. Once the FetchComputerLocalGroupBySIDSuffix fix is applied to return graph.ErrNoResultsFound, this code should handle that error appropriately.


780-786: LGTM!

Clean delegation pattern based on the skipURA flag.


788-819: LGTM!

The two-phase RDP entity processing logic is well-structured:

  1. Direct RIL entities with RDP group membership are added immediately
  2. Group members are expanded and checked for the correct permissions

The implementation correctly uses GroupMembershipCache.Reach for group expansion.

go.mod (1)

21-21: Dependency versions are published and security-verified; breaking change concern unfounded for dawgs.

The updates to github.com/specterops/dawgs v0.4.1 and github.com/jackc/pgx/v5 v5.8.0 are both published versions (as of Jan 15 and Dec 26, 2025, respectively). The dawgs v0.4.1 release notes show no breaking changes—only new Reachability tooling and code cleanup. pgx v5.8.0 exists on the Go module proxy but lacks official release notes on its GitHub releases page. No security advisories found for cuelang.org/go v0.15.3 or the golang.org/x updates; crypto v0.47.0 includes fixes for CVE-2025-47914 and CVE-2025-58181 from v0.45.0+.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@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: 2

🤖 Fix all issues with AI agents
In `@cmd/api/src/analysis/analysis_integration_test.go`:
- Line 17: The integration test build tag line is commented out ("// //go:build
integration") so the tests run during normal unit tests; edit
analysis_integration_test.go to remove the extra leading slashes so the line
reads "//go:build integration" (ensure it is the very first non-blank line
before package declaration) so the integration build tag is active and the tests
are excluded from regular `go test`.

In `@packages/go/analysis/ad/ad.go`:
- Around line 559-571: The slog.Info statements that print members of
nodeSlices/firstDegreeSets/unrolledSets/checkSet (calls in the loop and final
slog.Info) should be removed or demoted to debug-level logging to avoid emitting
large data blobs; replace slog.Info(...) with slog.Debug(...) and wrap them in a
debug-enabled guard using slog.Default().Enabled(context.Background(),
slog.LevelDebug) (or remove entirely) so the heavy Slice() outputs are only
produced when debug logging is enabled for functions manipulating nodeSlices,
firstDegreeSets, unrolledSets, and checkSet.
♻️ Duplicate comments (3)
packages/go/analysis/ad/post.go (3)

456-485: Zero ID returned when no results found.

Similar to FetchComputerLocalGroupBySIDSuffix, this function returns 0 for startID when no results are found, without indicating an error. Callers cannot distinguish between "no local group found" and "local group with ID 0".

🔧 Proposed fix - return error when no results found
 func FetchComputerLocalGroupIDBySIDSuffix(tx graph.Transaction, computer graph.ID, groupSuffix string) (graph.ID, error) {
 	var (
-		startID graph.ID
+		startID graph.ID
+		found   bool
 		err     = tx.Relationships().Filter(query.And(
 			query.StringEndsWith(query.StartProperty(common.ObjectID.String()), groupSuffix),
 			query.Kind(query.Relationship(), ad.LocalToComputer),
 			query.InIDs(query.EndID(), computer),
 		)).Query(
 			func(results graph.Result) error {
 				if results.Next() {
 					if err := results.Scan(&startID); err != nil {
 						return err
 					}
+					found = true
 				}

 				results.Close()
 				return results.Error()
 			},
 			query.Returning(
 				query.StartID(),
 			),
 		)
 	)

 	if err != nil {
 		return 0, err
 	}
+	if !found {
+		return 0, graph.ErrNoResultsFound
+	}

 	return startID, nil
 }

425-454: Zero-value node returned when no results found.

When results.Next() returns false on the first call, groupNode remains a zero-value graph.Node and the function returns &groupNode, nil. This can cause issues downstream - for example at line 757 where computerData.RemoteDesktopUsersLocalGroup is assigned from this function and later accessed at line 505 via RemoteDesktopUsersLocalGroup.ID.

🔧 Proposed fix - return error when no results found
 func FetchComputerLocalGroupBySIDSuffix(tx graph.Transaction, computer graph.ID, groupSuffix string) (*graph.Node, error) {
 	var (
-		groupNode graph.Node
+		groupNode *graph.Node
 		err       = tx.Relationships().Filter(query.And(
 			query.StringEndsWith(query.StartProperty(common.ObjectID.String()), groupSuffix),
 			query.Kind(query.Relationship(), ad.LocalToComputer),
 			query.InIDs(query.EndID(), computer),
 		)).Query(
 			func(results graph.Result) error {
 				if results.Next() {
-					if err := results.Scan(&groupNode); err != nil {
+					var node graph.Node
+					if err := results.Scan(&node); err != nil {
 						return err
 					}
+					groupNode = &node
 				}

 				results.Close()
 				return results.Error()
 			},
 			query.Returning(
 				query.Start(),
 			),
 		)
 	)

 	if err != nil {
 		return nil, err
 	}

-	return &groupNode, nil
+	if groupNode == nil {
+		return nil, graph.ErrNoResultsFound
+	}
+	return groupNode, nil
 }

754-758: Error handling for missing RemoteDesktopUsersLocalGroup needs verification.

FetchComputerLocalGroupBySIDSuffix returns a pointer to a zero-value node (not nil) when the Remote Desktop Users local group doesn't exist. The function always returns (&groupNode, nil) at line 453, even when no query results are found. This means computerData.RemoteDesktopUsersLocalGroup will point to a node with an empty/zero ID.

When accessed downstream at line 505 (canRDPData.RemoteDesktopUsersLocalGroup.ID.Uint64()), this will use an invalid (zero) ID value, causing container.AdjacentNodes() to retrieve incorrect membership data. Confirm whether this should instead return nil on missing group or if the callers need explicit nil checks.

🧹 Nitpick comments (2)
cmd/api/src/analysis/ad/ntlm_integration_test.go (1)

154-156: Inconsistent variable naming: grouplocalGroupData appears to be a typo.

The variable is named localGroupData in tests TestPostNTLMRelayADCS and TestNTLMRelayToADCSComposition (lines 49, 96), but grouplocalGroupData here and in several other test functions. This inconsistency suggests a find-replace artifact. Consider standardizing to localGroupData throughout for readability.

♻️ Suggested fix for consistent naming
-			grouplocalGroupData, computers, _, authenticatedUsers, err := fetchNTLMPrereqs(db)
+			localGroupData, computers, _, authenticatedUsers, err := fetchNTLMPrereqs(db)
 			require.NoError(t, err)
-			ntlmCache, err := ad2.NewNTLMCache(context.Background(), db, grouplocalGroupData)
+			ntlmCache, err := ad2.NewNTLMCache(context.Background(), db, localGroupData)
cmd/api/src/analysis/ad/adcs_integration_test.go (1)

66-81: Same variable naming inconsistency as in ntlm_integration_test.go.

The variable grouplocalGroupData is used throughout this file (and appears ~70+ times in the changed lines). Consider a bulk rename to localGroupData for consistency with the function's internal naming and with other test files. The actual data flow is correct.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68f1667 and dd1f27e.

📒 Files selected for processing (7)
  • cmd/api/src/analysis/ad/ad_integration_test.go
  • cmd/api/src/analysis/ad/adcs_integration_test.go
  • cmd/api/src/analysis/ad/ntlm_integration_test.go
  • cmd/api/src/analysis/analysis_integration_test.go
  • packages/go/analysis/ad/ad.go
  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/post.go
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-07-22T20:30:34.839Z
Learnt from: LawsonWillard
Repo: SpecterOps/BloodHound PR: 1700
File: cmd/api/src/api/v2/saved_queries_test.go:3182-3182
Timestamp: 2025-07-22T20:30:34.839Z
Learning: In Go table-driven tests in cmd/api/src/api/v2/saved_queries_test.go, subtest parallelization with t.Parallel() is acceptable when tests are self-contained, each creating their own mock controller (gomock.NewController(t)) and having isolated mock expectations without shared state between subtests.

Applied to files:

  • cmd/api/src/analysis/ad/ad_integration_test.go
📚 Learning: 2025-05-23T19:56:13.423Z
Learnt from: definitelynotagoblin
Repo: SpecterOps/BloodHound PR: 1440
File: packages/go/analysis/ad/ntlm.go:123-138
Timestamp: 2025-05-23T19:56:13.423Z
Learning: In the BloodHound NTLM analysis code (packages/go/analysis/ad/ntlm.go), the `isProtectedComputer` function intentionally fails open (returns false/unprotected) when the Protected Users group cannot be retrieved, maintaining consistency with the original logic patterns in the codebase.

Applied to files:

  • cmd/api/src/analysis/analysis_integration_test.go
  • cmd/api/src/analysis/ad/ntlm_integration_test.go
  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/post.go
📚 Learning: 2025-11-05T21:13:08.542Z
Learnt from: ktstrader
Repo: SpecterOps/BloodHound PR: 2020
File: cmd/api/src/queries/graph.go:198-203
Timestamp: 2025-11-05T21:13:08.542Z
Learning: In cmd/api/src/queries/graph.go, when ETAC filtering is enabled in GetAssetGroupComboNode, an empty etacAllowedList (when user.AllEnvironments is false and user.EnvironmentTargetedAccessControl is empty) is intentional and should not be guarded against. The empty list will cause query.In() to filter out all nodes, which is the correct security behavior—users with no environment assignments should see no results.

Applied to files:

  • cmd/api/src/analysis/analysis_integration_test.go
  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/post.go
📚 Learning: 2025-06-06T23:12:14.181Z
Learnt from: elikmiller
Repo: SpecterOps/BloodHound PR: 1563
File: packages/go/graphschema/azure/azure.go:24-24
Timestamp: 2025-06-06T23:12:14.181Z
Learning: In BloodHound, files in packages/go/graphschema/*/`*.go` are generated from CUE schemas. When `just prepare-for-codereview` is run, it triggers code generation that may automatically add import aliases or other formatting changes. These changes are legitimate outputs of the generation process, not manual edits that would be overwritten.

Applied to files:

  • cmd/api/src/analysis/ad/ntlm_integration_test.go
  • packages/go/analysis/ad/ad.go
  • cmd/api/src/analysis/ad/adcs_integration_test.go
  • packages/go/analysis/ad/ntlm.go
📚 Learning: 2025-08-28T16:43:43.961Z
Learnt from: mvlipka
Repo: SpecterOps/BloodHound PR: 1784
File: packages/go/openapi/doc/openapi.json:18008-18029
Timestamp: 2025-08-28T16:43:43.961Z
Learning: In SpecterOps/BloodHound, packages/go/openapi/doc/openapi.json is generated from YAML under packages/go/openapi/src/schemas; edits must be made to the YAML and then the spec regenerated.

Applied to files:

  • cmd/api/src/analysis/ad/ntlm_integration_test.go
  • cmd/api/src/analysis/ad/adcs_integration_test.go
📚 Learning: 2025-05-27T16:58:33.295Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1503
File: cmd/api/src/services/job/jobs_test.go:19-143
Timestamp: 2025-05-27T16:58:33.295Z
Learning: Tests in cmd/api/src/services/job/jobs_test.go have been found to be flaky in the past and are due for rewrite. They should be skipped with t.Skip() until they can be properly rewritten.

Applied to files:

  • packages/go/analysis/ad/post.go
🧬 Code graph analysis (6)
cmd/api/src/analysis/ad/ad_integration_test.go (2)
packages/go/analysis/ad/post.go (2)
  • PostSyncLAPSPassword (39-72)
  • PostDCSync (74-104)
packages/go/analysis/ad/owns.go (1)
  • PostOwnsAndWriteOwner (38-164)
cmd/api/src/analysis/analysis_integration_test.go (1)
packages/go/analysis/ad/post.go (2)
  • FetchCanRDPData (705-711)
  • FetchCanRDPEntityBitmapForComputer (536-564)
cmd/api/src/analysis/ad/ntlm_integration_test.go (1)
packages/go/analysis/ad/ntlm.go (3)
  • NewNTLMCache (63-124)
  • FetchLDAPSigningCache (892-968)
  • FetchProtectedUsersMappedToDomains (855-880)
packages/go/analysis/ad/ad.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (630-635)
packages/go/graphschema/ad/ad.go (3)
  • Group (31-31)
  • LocalGroup (36-36)
  • Contains (54-54)
cmd/api/src/analysis/ad/adcs_integration_test.go (4)
packages/go/analysis/ad/post.go (2)
  • LocalGroupData (630-635)
  • FetchLocalGroupData (637-671)
packages/go/analysis/ad/esc1.go (1)
  • PostADCSESC1 (35-64)
packages/go/analysis/ad/esc6.go (2)
  • PostADCSESC6a (37-80)
  • PostADCSESC6b (82-127)
packages/go/analysis/ad/esc13.go (1)
  • PostADCSESC13 (36-78)
packages/go/analysis/ad/ntlm.go (1)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (630-635)
🪛 GitHub Actions: Run Go Unit Tests
cmd/api/src/analysis/ad/ad_integration_test.go

[error] 1-1: Test command failed with exit code 1

cmd/api/src/analysis/analysis_integration_test.go

[error] 1-1: Test command failed with exit code 1

cmd/api/src/analysis/ad/ntlm_integration_test.go

[error] 1-1: Test command failed with exit code 1

packages/go/analysis/ad/ad.go

[error] 1-1: Test command failed with exit code 1

cmd/api/src/analysis/ad/adcs_integration_test.go

[error] 1-1: Test command failed with exit code 1

packages/go/analysis/ad/ntlm.go

[error] 1-1: Test command failed with exit code 1

packages/go/analysis/ad/post.go

[error] 1-1: Test command failed with exit code 1

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build BloodHound Container Image / Build and Package Container
  • GitHub Check: build-ui
  • GitHub Check: run-analysis
  • GitHub Check: run-tests
🔇 Additional comments (17)
cmd/api/src/analysis/analysis_integration_test.go (1)

42-57: LGTM - API migration to FetchCanRDPData pattern.

The test correctly uses the new FetchCanRDPData API and retrieves per-computer data via canRDPData.ComputerData(tx, computerID) before calling FetchCanRDPEntityBitmapForComputer. This pattern is consistently applied throughout the file.

packages/go/analysis/ad/ad.go (1)

477-497: LGTM - Clean signature refactor to LocalGroupData.

The function signature change from (tx graph.Transaction, groupExpansions impact.PathAggregator, nodeSlices ...[]*graph.Node) to (localGroupData *LocalGroupData, nodeSlices ...[]*graph.Node) is a sensible simplification that removes the transaction dependency and centralizes group membership data access.

packages/go/analysis/ad/ntlm.go (3)

40-46: LGTM - NTLMCache struct updated to use LocalGroupData.

The struct field change from GroupExpansions impact.PathAggregator to LocalGroupData *LocalGroupData aligns with the PR's goal of centralizing group membership data access through the new LocalGroupData abstraction.


63-82: LGTM - NewNTLMCache correctly wired to LocalGroupData.

The function signature and internal wiring correctly propagates localGroupData to FetchProtectedUsersMappedToDomains and assigns it to ntlmCache.LocalGroupData.


855-879: LGTM - FetchProtectedUsersMappedToDomains updated to use LocalGroupData.

The implementation correctly uses localGroupData.GroupMembershipCache.OrReachability to populate the protected users bitmap, replacing the previous PathAggregator-based approach.

packages/go/analysis/ad/post.go (3)

630-671: LGTM - Well-structured LocalGroupData type and fetching logic.

The LocalGroupData struct cleanly encapsulates the data needed for local group post-processing. The FetchLocalGroupData function properly handles errors and sequentially builds up the required caches.


713-767: LGTM - CanRDPData and CanRDPComputerData provide clean per-computer data access.

The nested struct design with CanRDPData embedding LocalGroupData and CanRDPComputerData embedding CanRDPData provides a clean hierarchy for accessing RDP-related data at different scopes.


791-821: LGTM - ProcessRDPWithUra logic is clear.

The two-pass approach (first checking direct RIL entities, then expanding group memberships) is well-structured and the comments clearly explain the logic.

cmd/api/src/analysis/ad/ad_integration_test.go (4)

1144-1165: LGTM - Test properly updated to use FetchLocalGroupData API.

The test correctly fetches localGroupData and passes it to PostSyncLAPSPassword. The error handling pattern is appropriate for integration tests.


1175-1196: LGTM - TestDCSync properly migrated to new API.

Consistent with the other test updates, using FetchLocalGroupData and passing the result to PostDCSync.


1207-1212: LGTM - TestOwnsWriteOwnerPriorCollectorVersions properly updated.

The test correctly uses the new FetchLocalGroupData and PostOwnsAndWriteOwner signatures.


1410-1415: LGTM - TestOwnsWriteOwner properly updated.

Consistent migration to the new LocalGroupData-based API.

cmd/api/src/analysis/ad/ntlm_integration_test.go (2)

49-51: LGTM!

The refactor correctly retrieves localGroupData via fetchNTLMPrereqs and passes it to NewNTLMCache. The variable naming is consistent here with localGroupData.


613-630: LGTM!

The fetchNTLMPrereqs function has been correctly updated to use FetchLocalGroupData and return *ad2.LocalGroupData. The error handling and return value flow are properly maintained.

cmd/api/src/analysis/ad/adcs_integration_test.go (3)

41-55: LGTM!

The FetchADCSPrereqs function correctly fetches localGroupData using ad2.FetchLocalGroupData and returns it as the first value. The cache building and other data fetching remain unchanged.


231-232: Good pattern for tests that don't need localGroupData.

Correctly discards the unused localGroupData return value since PostGoldenCert doesn't require it.


1-39: Function signatures and imports are correct; no issues found with the code structure shown.

The FetchLocalGroupData and PostADCSESC* function signatures match their usage throughout the test file. All calls pass parameters in the correct order and types:

  • FetchLocalGroupData(context.Background(), db) matches the definition func(context.Context, graph.Database) (*LocalGroupData, error)
  • All PostADCSESC* calls (ESC1, ESC3, ESC4, ESC6a, ESC6b, ESC9a, ESC9b, ESC10a, ESC10b, ESC13) follow the expected signature pattern and are invoked correctly

Imports (lines 21-38) include all necessary packages. The code shown is structurally sound with no compilation errors evident.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

// SPDX-License-Identifier: Apache-2.0

//go:build integration
// //go:build integration
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Build tag is commented out, causing integration tests to run during unit tests.

The build tag // //go:build integration has an extra // prefix that comments it out. This means these integration tests will run during regular unit test execution, which is likely causing the pipeline failure.

🔧 Proposed fix
-// //go:build integration
+//go:build integration
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// //go:build integration
//go:build integration
🤖 Prompt for AI Agents
In `@cmd/api/src/analysis/analysis_integration_test.go` at line 17, The
integration test build tag line is commented out ("// //go:build integration")
so the tests run during normal unit tests; edit analysis_integration_test.go to
remove the extra leading slashes so the line reads "//go:build integration"
(ensure it is the very first non-blank line before package declaration) so the
integration build tag is active and the tests are excluded from regular `go
test`.

@zinic zinic force-pushed the BED-7147 branch 2 times, most recently from 237a7dd to 3283c57 Compare January 15, 2026 21:34
@zinic zinic changed the title feat: BED-7147 - Local Group Post-Processing Rework feat: Local Group Post-Processing Rework - BED-7147 Jan 15, 2026
@zinic zinic force-pushed the BED-7147 branch 2 times, most recently from 221d877 to b328188 Compare January 15, 2026 21:40
Copy link
Contributor

@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

Caution

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

⚠️ Outside diff range comments (2)
cmd/api/src/analysis/ad/adcs_integration_test.go (1)

41-53: Handle BuildCache errors in FetchADCSPrereqs.

BuildCache returns an error but it’s ignored here, which can mask setup failures and yield misleading test results.

🔧 Proposed fix
-			cache.BuildCache(context.Background(), db, enterpriseCertAuthorities, certTemplates)
-			return localGroupData, cache.GetEnterpriseCertAuthorities(), cache.GetCertTemplates(), cache.GetDomains(), cache, nil
+			if err := cache.BuildCache(context.Background(), db, enterpriseCertAuthorities, certTemplates); err != nil {
+				return nil, nil, nil, nil, ad2.ADCSCache{}, err
+			}
+			return localGroupData, cache.GetEnterpriseCertAuthorities(), cache.GetCertTemplates(), cache.GetDomains(), cache, nil
packages/go/analysis/ad/post.go (1)

209-226: Fix incorrect property accessors and misleading variable name in FetchAdminGroups.

This function uses query.StartProperty and query.EndProperty on a node query (tx.Nodes()), but these are relationship-oriented accessors meant for tx.Relationships() filters. For node queries, use query.NodeProperty instead. Additionally, the variable name computerNodeIds is misleading for a function named FetchAdminGroups—it should be adminGroupIds.

Proposed fix
 func FetchAdminGroups(ctx context.Context, db graph.Database) (cardinality.Duplex[uint64], error) {
-	computerNodeIds := cardinality.NewBitmap64()
+	adminGroupIds := cardinality.NewBitmap64()

-	return computerNodeIds, db.ReadTransaction(ctx, func(tx graph.Transaction) error {
+	return adminGroupIds, db.ReadTransaction(ctx, func(tx graph.Transaction) error {
 		return tx.Nodes().Filter(query.And(
-			query.Or(
-				query.StringEndsWith(query.StartProperty(common.ObjectID.String()), wellknown.AdministratorsSIDSuffix.String()),
-				query.StringEndsWith(query.EndProperty(common.ObjectID.String()), wellknown.AdministratorsSIDSuffix.String()),
-			),
+			query.StringEndsWith(query.NodeProperty(common.ObjectID.String()), wellknown.AdministratorsSIDSuffix.String()),
 		)).FetchIDs(func(cursor graph.Cursor[graph.ID]) error {
 			for id := range cursor.Chan() {
-				computerNodeIds.Add(id.Uint64())
+				adminGroupIds.Add(id.Uint64())
 			}

 			return nil
 		})
 	})
 }
🤖 Fix all issues with AI agents
In `@packages/go/analysis/ad/local_groups.go`:
- Around line 275-308: The three calls to FetchComputerLocalGroupIDBySIDSuffix
(localAdminGroup, localPSRemoteGroup, localDCOMGroup) currently ignore their
errors (localAdminGroupErr, localPSRemoteGroupErr, localDCOMGroupErr) and
silently skip processing; change the logic to propagate or surface these errors
instead of swallowing them—e.g., for each fetch, if err != nil wrap/return the
error (or accumulate and return a combined error) with context like which
computerID and suffix failed so callers can see failures; update the surrounding
function to return an error (or accept/return a multi-error) and remove the
silent-nil-only behavior before calling numGroupsProcessed.Add and
channels.Submit for reachJob submissions.
♻️ Duplicate comments (2)
packages/go/analysis/ad/post.go (2)

425-454: Bug: Returns pointer to zero-value node when no results found.

This issue was previously flagged. When results.Next() returns false on the first call, groupNode remains a zero-value graph.Node, and the function returns &groupNode, nil. Callers (e.g., ComputerData at line 754) don't check for a zero-value node, which will cause issues when accessing RemoteDesktopUsersLocalGroup.ID (it will be 0).

Proposed fix - return error when no results found
 func FetchComputerLocalGroupBySIDSuffix(tx graph.Transaction, computer graph.ID, groupSuffix string) (*graph.Node, error) {
 	var (
-		groupNode graph.Node
+		groupNode *graph.Node
 		err       = tx.Relationships().Filter(query.And(
 			query.StringEndsWith(query.StartProperty(common.ObjectID.String()), groupSuffix),
 			query.Kind(query.Relationship(), ad.LocalToComputer),
 			query.InIDs(query.EndID(), computer),
 		)).Query(
 			func(results graph.Result) error {
 				if results.Next() {
-					if err := results.Scan(&groupNode); err != nil {
+					var node graph.Node
+					if err := results.Scan(&node); err != nil {
 						return err
 					}
+					groupNode = &node
 				}

 				results.Close()
 				return results.Error()
 			},
 			query.Returning(
 				query.Start(),
 			),
 		)
 	)

 	if err != nil {
 		return nil, err
 	}

-	return &groupNode, nil
+	if groupNode == nil {
+		return nil, graph.ErrNoResultsFound
+	}
+	return groupNode, nil
 }

456-485: Similar issue: Returns zero ID when no results found.

Same pattern as FetchComputerLocalGroupBySIDSuffix - when no results are found, a zero graph.ID is returned without an error. Callers cannot distinguish between "no group found" and "group found with ID 0" (though ID 0 is typically invalid). Consider returning graph.ErrNoResultsFound.

🧹 Nitpick comments (5)
cmd/api/src/analysis/membership_integration_test.go (1)

77-95: Test refactor looks correct for the new reachability model.

The updated test properly validates the SCC-based reachability behavior, and the explanatory comments (lines 87-89) clearly document why origin members are included in the cardinality counts. The consistent use of graph.DirectionInbound across all assertions aligns with the new LocalGroupData / ReachabilityCache approach.


Consider addressing the deprecated test utility.

Static analysis flags integration.NewGraphTestContext as deprecated (SA1019). The deprecation note references updated integration test standards. This is a minor concern since it affects existing test infrastructure, but consider updating this test to use the newer patterns when feasible.

packages/go/analysis/ad/ntlm.go (1)

63-83: Add defensive checks for nil LocalGroupData.

LocalGroupData (and its GroupMembershipCache) is now a required dependency; a nil pointer would panic in several paths. An early guard yields a clearer error and avoids hard crashes.

💡 Proposed guard
 func NewNTLMCache(ctx context.Context, db graph.Database, localGroupData *LocalGroupData) (NTLMCache, error) {
 	var (
 		ntlmCache                   = NTLMCache{}
 		unprotectedComputerCache    = make(map[string]cardinality.Duplex[uint64])
 		allUnprotectedComputerCache = cardinality.NewBitmap64()
 	)
+	if localGroupData == nil || localGroupData.GroupMembershipCache == nil {
+		return ntlmCache, fmt.Errorf("local group data is required")
+	}
 
 	return ntlmCache, db.ReadTransaction(ctx, func(tx graph.Transaction) error {
 		// Fetch all nodes where the node is a Group and is an Authenticated User
 		if innerAuthenticatedUsersCache, err := FetchAuthUsersMappedToDomains(tx); err != nil {
 			return err
@@
 func FetchProtectedUsersMappedToDomains(ctx context.Context, db graph.Database, localGroupData *LocalGroupData) (map[string]cardinality.Duplex[uint64], error) {
 	protectedUsers := make(map[string]cardinality.Duplex[uint64])
+	if localGroupData == nil || localGroupData.GroupMembershipCache == nil {
+		return protectedUsers, fmt.Errorf("local group data is required")
+	}
 
 	err := db.ReadTransaction(ctx, func(tx graph.Transaction) error {

Also applies to: 855-870

packages/go/analysis/ad/post.go (3)

502-511: Remove commented-out dead code.

Line 510 contains commented-out code that should be removed to keep the codebase clean.

Proposed fix
 func FetchRemoteDesktopUsersLocalGroupMembership(computer graph.ID, canRDPData *CanRDPComputerData) cardinality.Duplex[uint64] {
 	adjacentNodes := container.AdjacentNodes(
 		canRDPData.LocalGroupMembershipDigraph,
 		canRDPData.RemoteDesktopUsersLocalGroup.ID.Uint64(),
 		graph.DirectionInbound,
 	)

 	return cardinality.NewBitmap64With(adjacentNodes...)
-	// return canRDPData.GroupMembershipCache.Reach(canRDPData.RemoteDesktopUsersLocalGroup.ID.Uint64(), graph.DirectionInbound)
 }

630-635: Address the TODO comment for documentation.

Line 633 has a TODO comment // TODO: Document me for ExcludedShortcutGroups. Consider adding documentation explaining what this field represents and how it's used in cross-product calculations.

Would you like me to help draft documentation for this field based on its usage in CalculateCrossProductNodeSets?


685-689: Redundant fetch of ExcludedShortcutGroups.

ExcludedShortcutGroups is already populated in LocalGroupData (line 644 in FetchLocalGroupData), and since CanRDPData embeds LocalGroupData, it inherits this field. Fetching it again here is redundant and performs an unnecessary database query.

Proposed fix - remove redundant fetch
 func (s *LocalGroupData) FetchCanRDPData(ctx context.Context, graphDB graph.Database) (*CanRDPData, error) {
 	components := &CanRDPData{
 		LocalGroupData: *s,
 	}

 	if err := graphDB.ReadTransaction(ctx, func(tx graph.Transaction) error {
 		if computersWithURA, err := FetchComputersWithURA(tx); err != nil {
 			return err
 		} else {
 			components.ComputersWithURA = computersWithURA
 		}

-		if excludedGroups, err := FetchAuthUsersAndEveryoneGroups(tx); err != nil {
-			return err
-		} else {
-			components.ExcludedShortcutGroups = excludedGroups.IDBitmap()
-		}
-
 		return nil
 	}); err != nil {
 		return components, err
 	}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd1f27e and b328188.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (26)
  • cmd/api/src/analysis/ad/ad_integration_test.go
  • cmd/api/src/analysis/ad/adcs_integration_test.go
  • cmd/api/src/analysis/ad/ntlm_integration_test.go
  • cmd/api/src/analysis/ad/post.go
  • cmd/api/src/analysis/analysis_integration_test.go
  • cmd/api/src/analysis/membership_integration_test.go
  • cmd/api/src/analysis/post_integration_test.go
  • cmd/api/src/daemons/datapipe/datapipe.go
  • cmd/api/src/test/integration/harnesses.go
  • go.mod
  • packages/go/analysis/ad/ad.go
  • packages/go/analysis/ad/adcs.go
  • packages/go/analysis/ad/adcscache.go
  • packages/go/analysis/ad/esc1.go
  • packages/go/analysis/ad/esc10.go
  • packages/go/analysis/ad/esc13.go
  • packages/go/analysis/ad/esc3.go
  • packages/go/analysis/ad/esc4.go
  • packages/go/analysis/ad/esc6.go
  • packages/go/analysis/ad/esc9.go
  • packages/go/analysis/ad/esc_shared.go
  • packages/go/analysis/ad/local_groups.go
  • packages/go/analysis/ad/membership.go
  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/owns.go
  • packages/go/analysis/ad/post.go
💤 Files with no reviewable changes (1)
  • cmd/api/src/daemons/datapipe/datapipe.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • cmd/api/src/analysis/ad/post.go
  • packages/go/analysis/ad/esc6.go
  • packages/go/analysis/ad/esc4.go
  • packages/go/analysis/ad/adcscache.go
  • cmd/api/src/test/integration/harnesses.go
  • packages/go/analysis/ad/esc10.go
  • cmd/api/src/analysis/post_integration_test.go
  • packages/go/analysis/ad/esc13.go
🧰 Additional context used
🧠 Learnings (14)
📚 Learning: 2025-06-25T17:52:33.291Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1606
File: cmd/api/src/analysis/azure/post.go:33-35
Timestamp: 2025-06-25T17:52:33.291Z
Learning: In BloodHound Go code, prefer using explicit slog type functions like slog.Any(), slog.String(), slog.Int(), etc. over simple key-value pairs for structured logging. This provides better type safety and makes key-value pairs more visually distinct. For error types, use slog.Any("key", err) or slog.String("key", err.Error()).

Applied to files:

  • packages/go/analysis/ad/adcs.go
  • packages/go/analysis/ad/local_groups.go
  • packages/go/analysis/ad/owns.go
  • packages/go/analysis/ad/ad.go
📚 Learning: 2025-06-06T23:12:14.181Z
Learnt from: elikmiller
Repo: SpecterOps/BloodHound PR: 1563
File: packages/go/graphschema/azure/azure.go:24-24
Timestamp: 2025-06-06T23:12:14.181Z
Learning: In BloodHound, files in packages/go/graphschema/*/`*.go` are generated from CUE schemas. When `just prepare-for-codereview` is run, it triggers code generation that may automatically add import aliases or other formatting changes. These changes are legitimate outputs of the generation process, not manual edits that would be overwritten.

Applied to files:

  • packages/go/analysis/ad/adcs.go
  • packages/go/analysis/ad/esc_shared.go
  • packages/go/analysis/ad/esc9.go
  • packages/go/analysis/ad/esc1.go
  • cmd/api/src/analysis/ad/ntlm_integration_test.go
  • cmd/api/src/analysis/membership_integration_test.go
  • cmd/api/src/analysis/ad/adcs_integration_test.go
  • go.mod
  • packages/go/analysis/ad/owns.go
  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/ad.go
  • packages/go/analysis/ad/esc3.go
📚 Learning: 2025-07-10T14:31:01.781Z
Learnt from: JonasBK
Repo: SpecterOps/BloodHound PR: 1671
File: packages/go/analysis/ad/esc16.go:65-69
Timestamp: 2025-07-10T14:31:01.781Z
Learning: In BloodHound ADCS ESC analysis functions (PostADCSESC1, PostADCSESC4, PostADCSESC6, PostADCSESC9, PostADCSESC10, PostADCSESC13, PostADCSESC16, etc.), the established pattern for handling cert template validation errors and user filtering errors is to use slog.WarnContext to log warnings and continue processing with the next template, rather than returning errors to the caller. This pattern is consistently applied across all ADCS ESC edge implementations.

Applied to files:

  • packages/go/analysis/ad/adcs.go
  • packages/go/analysis/ad/esc9.go
  • packages/go/analysis/ad/esc1.go
  • packages/go/analysis/ad/ad.go
  • packages/go/analysis/ad/esc3.go
📚 Learning: 2025-06-25T18:24:25.014Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1606
File: cmd/api/src/analysis/azure/post.go:33-35
Timestamp: 2025-06-25T18:24:25.014Z
Learning: In BloodHound Go code, for error types in slog structured logging, prefer using slog.String("key", err.Error()) over slog.Any("key", err). The explicit string conversion with err.Error() is preferred over using slog.Any() for error types.

Applied to files:

  • packages/go/analysis/ad/local_groups.go
  • packages/go/analysis/ad/owns.go
📚 Learning: 2025-05-23T19:56:13.423Z
Learnt from: definitelynotagoblin
Repo: SpecterOps/BloodHound PR: 1440
File: packages/go/analysis/ad/ntlm.go:123-138
Timestamp: 2025-05-23T19:56:13.423Z
Learning: In the BloodHound NTLM analysis code (packages/go/analysis/ad/ntlm.go), the `isProtectedComputer` function intentionally fails open (returns false/unprotected) when the Protected Users group cannot be retrieved, maintaining consistency with the original logic patterns in the codebase.

Applied to files:

  • packages/go/analysis/ad/local_groups.go
  • cmd/api/src/analysis/ad/ntlm_integration_test.go
  • packages/go/analysis/ad/ntlm.go
  • cmd/api/src/analysis/analysis_integration_test.go
  • packages/go/analysis/ad/post.go
📚 Learning: 2025-05-27T16:58:33.295Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1503
File: cmd/api/src/services/job/jobs_test.go:19-143
Timestamp: 2025-05-27T16:58:33.295Z
Learning: Tests in cmd/api/src/services/job/jobs_test.go have been found to be flaky in the past and are due for rewrite. They should be skipped with t.Skip() until they can be properly rewritten.

Applied to files:

  • packages/go/analysis/ad/local_groups.go
  • cmd/api/src/analysis/membership_integration_test.go
  • cmd/api/src/analysis/analysis_integration_test.go
  • packages/go/analysis/ad/post.go
📚 Learning: 2025-08-27T21:15:32.207Z
Learnt from: jvacca-specterops
Repo: SpecterOps/BloodHound PR: 1823
File: packages/javascript/bh-shared-ui/src/commonSearchesAGT.ts:184-187
Timestamp: 2025-08-27T21:15:32.207Z
Learning: In the BloodHound codebase, syntax fixes for invalid Cypher patterns (like `*..` → `*1..`) may be considered out of scope for specific PRs, even when flagged during review.

Applied to files:

  • packages/go/analysis/ad/local_groups.go
📚 Learning: 2025-07-22T20:30:34.839Z
Learnt from: LawsonWillard
Repo: SpecterOps/BloodHound PR: 1700
File: cmd/api/src/api/v2/saved_queries_test.go:3182-3182
Timestamp: 2025-07-22T20:30:34.839Z
Learning: In Go table-driven tests in cmd/api/src/api/v2/saved_queries_test.go, subtest parallelization with t.Parallel() is acceptable when tests are self-contained, each creating their own mock controller (gomock.NewController(t)) and having isolated mock expectations without shared state between subtests.

Applied to files:

  • cmd/api/src/analysis/ad/ad_integration_test.go
📚 Learning: 2025-08-28T16:43:43.961Z
Learnt from: mvlipka
Repo: SpecterOps/BloodHound PR: 1784
File: packages/go/openapi/doc/openapi.json:18008-18029
Timestamp: 2025-08-28T16:43:43.961Z
Learning: In SpecterOps/BloodHound, packages/go/openapi/doc/openapi.json is generated from YAML under packages/go/openapi/src/schemas; edits must be made to the YAML and then the spec regenerated.

Applied to files:

  • cmd/api/src/analysis/ad/ntlm_integration_test.go
  • cmd/api/src/analysis/ad/adcs_integration_test.go
📚 Learning: 2025-06-30T22:58:54.834Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1629
File: go.mod:18-18
Timestamp: 2025-06-30T22:58:54.834Z
Learning: Go 1.24 supports patch versions in the go directive of go.mod files. Syntax like "go 1.24.4" is valid and specifies the minimum Go version required to build the module, including the patch level.

Applied to files:

  • go.mod
📚 Learning: 2025-08-26T18:13:36.643Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1820
File: .github/workflows/audit-release.yml:0-0
Timestamp: 2025-08-26T18:13:36.643Z
Learning: The go tool command supports custom/third-party binaries as of Go 1.24, allowing invocation of custom CLI tools like "go tool stbernard" rather than being limited to only built-in Go toolchain binaries.

Applied to files:

  • go.mod
📚 Learning: 2025-07-10T14:33:20.317Z
Learnt from: JonasBK
Repo: SpecterOps/BloodHound PR: 1671
File: cmd/api/src/analysis/ad/adcs_integration_test.go:3687-3687
Timestamp: 2025-07-10T14:33:20.317Z
Learning: When reviewing Go code, functions defined in one file within a package are accessible from other files in the same package. Before flagging missing functions as compilation errors, check if they exist in other files within the same package directory.

Applied to files:

  • packages/go/analysis/ad/owns.go
📚 Learning: 2025-11-05T21:13:08.542Z
Learnt from: ktstrader
Repo: SpecterOps/BloodHound PR: 2020
File: cmd/api/src/queries/graph.go:198-203
Timestamp: 2025-11-05T21:13:08.542Z
Learning: In cmd/api/src/queries/graph.go, when ETAC filtering is enabled in GetAssetGroupComboNode, an empty etacAllowedList (when user.AllEnvironments is false and user.EnvironmentTargetedAccessControl is empty) is intentional and should not be guarded against. The empty list will cause query.In() to filter out all nodes, which is the correct security behavior—users with no environment assignments should see no results.

Applied to files:

  • packages/go/analysis/ad/ntlm.go
  • cmd/api/src/analysis/analysis_integration_test.go
  • packages/go/analysis/ad/post.go
📚 Learning: 2025-05-29T18:24:23.227Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1509
File: packages/go/stbernard/command/license/internal/cmd.go:49-51
Timestamp: 2025-05-29T18:24:23.227Z
Learning: In the stbernard package, use slog for all logging prints instead of fmt.Printf/fmt.Fprintf to maintain consistency with the codebase's logging standards.

Applied to files:

  • packages/go/analysis/ad/ad.go
🧬 Code graph analysis (12)
packages/go/analysis/ad/esc_shared.go (1)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (630-635)
packages/go/analysis/ad/local_groups.go (5)
packages/go/analysis/ad/post.go (4)
  • LocalGroupData (630-635)
  • CanRDPComputerData (731-738)
  • FetchCanRDPEntityBitmapForComputer (536-564)
  • FetchComputerLocalGroupIDBySIDSuffix (456-485)
packages/go/analysis/post_operation.go (3)
  • AtomicPostProcessingStats (86-90)
  • NewAtomicPostProcessingStats (92-98)
  • NewPropertiesWithLastSeen (166-171)
packages/go/analysis/post.go (1)
  • CreatePostRelationshipJob (151-157)
packages/go/graphschema/ad/ad.go (4)
  • CanRDP (68-68)
  • AdminTo (66-66)
  • CanPSRemote (67-67)
  • ExecuteDCOM (69-69)
packages/go/analysis/analysis.go (1)
  • MaximumDatabaseParallelWorkers (36-36)
packages/go/analysis/ad/esc9.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (630-635)
packages/go/analysis/ad/adcscache.go (1)
  • ADCSCache (33-55)
packages/go/analysis/ad/esc1.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (630-635)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (477-638)
cmd/api/src/analysis/ad/ad_integration_test.go (2)
packages/go/analysis/ad/post.go (3)
  • FetchLocalGroupData (637-671)
  • PostSyncLAPSPassword (39-72)
  • PostDCSync (74-104)
packages/go/analysis/ad/owns.go (1)
  • PostOwnsAndWriteOwner (38-164)
cmd/api/src/analysis/ad/ntlm_integration_test.go (2)
packages/go/analysis/ad/ntlm.go (3)
  • NewNTLMCache (63-124)
  • FetchLDAPSigningCache (892-968)
  • FetchProtectedUsersMappedToDomains (855-880)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (630-635)
cmd/api/src/analysis/ad/adcs_integration_test.go (4)
packages/go/analysis/ad/post.go (2)
  • LocalGroupData (630-635)
  • FetchLocalGroupData (637-671)
packages/go/analysis/ad/esc1.go (1)
  • PostADCSESC1 (35-64)
packages/go/analysis/ad/esc9.go (2)
  • PostADCSESC9a (35-82)
  • PostADCSESC9b (84-128)
packages/go/analysis/ad/esc6.go (2)
  • PostADCSESC6a (37-80)
  • PostADCSESC6b (82-127)
packages/go/analysis/ad/owns.go (1)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (630-635)
packages/go/analysis/ad/ntlm.go (1)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (630-635)
packages/go/analysis/ad/esc3.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (630-635)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (477-638)
cmd/api/src/analysis/analysis_integration_test.go (1)
packages/go/analysis/ad/post.go (2)
  • FetchCanRDPData (705-711)
  • FetchCanRDPEntityBitmapForComputer (536-564)
packages/go/analysis/ad/post.go (5)
packages/go/analysis/post.go (1)
  • CreatePostRelationshipJob (151-157)
packages/go/graphschema/common/common.go (2)
  • Nodes (33-35)
  • ObjectID (52-52)
packages/go/analysis/ad/wellknown/suffix.go (2)
  • AdministratorsSIDSuffix (83-83)
  • RemoteDesktopUsersSIDSuffix (86-86)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (477-638)
packages/go/analysis/ad/queries.go (1)
  • FetchAuthUsersAndEveryoneGroups (1892-1902)
🪛 golangci-lint (2.5.0)
cmd/api/src/analysis/membership_integration_test.go

[minor] 78-78: SA1019: integration.NewGraphTestContext is deprecated: this suite of integration utils is deprecated and should be avoided Integration tests should be updated to reflect the latest standards. See commit a6cc430#diff-e6bcb50873ade3cf33cef4e3e0ff566fb8ac1367b4ade36f4511bc2172a760e1 for implementation guidance. Additional detailed information can be found in Confluence.

(staticcheck)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build BloodHound Container Image / Build and Package Container
  • GitHub Check: run-tests
  • GitHub Check: run-analysis
  • GitHub Check: build-ui
🔇 Additional comments (26)
cmd/api/src/analysis/membership_integration_test.go (1)

30-30: LGTM!

Import correctly added to support the new algo.FetchReachabilityCache function used in the refactored test.

cmd/api/src/analysis/analysis_integration_test.go (3)

42-50: Per-computer CanRDP test wiring looks good.

Fetching CanRDPData once and deriving per-computer data inside the read transaction matches the new API flow.


68-76: URA enforced/unenforced paths are updated cleanly.

Also applies to: 100-106, 134-143


165-174: Citrix path adjustments look consistent with the new CanRDP data model.

Also applies to: 189-195, 214-222

go.mod (2)

21-60: No review notes for this hunk.


87-281: No review notes for this hunk.

packages/go/analysis/ad/local_groups.go (1)

32-165: PostCanRDP pipeline sequencing looks solid.

Stages are closed in the right order, and the data flow is easy to follow.

cmd/api/src/analysis/ad/ad_integration_test.go (1)

1144-1147: LocalGroupData wiring in post-processing tests looks consistent.

Also applies to: 1175-1178, 1207-1210, 1410-1413

packages/go/analysis/ad/membership.go (1)

92-94: Simplified direction handling is clear and correct.

packages/go/analysis/ad/esc1.go (1)

35-35: LGTM - Signature updated to use LocalGroupData.

The function signature change from impact.PathAggregator to *LocalGroupData and the corresponding update to CalculateCrossProductNodeSets call align with the broader refactor. The cross-product calculation now correctly passes localGroupData as the first parameter, consistent with the updated CalculateCrossProductNodeSets signature in ad/ad.go.

Also applies to: 48-48

packages/go/analysis/ad/esc3.go (1)

38-38: LGTM - Consistent migration to LocalGroupData.

The signature change and both CalculateCrossProductNodeSets call sites are correctly updated:

  • Line 38: Function now accepts *LocalGroupData instead of impact.PathAggregator
  • Lines 91-97: Delegated agents path correctly passes localGroupData with the additional delegatedAgents.Slice() parameter
  • Lines 109-114: Non-delegated path correctly passes localGroupData

The cross-product computation logic is preserved while switching to the new data carrier.

Also applies to: 91-97, 109-114

packages/go/analysis/ad/owns.go (1)

38-38: LGTM - Clean migration to ReachabilityCache-based expansion.

The changes correctly adapt the admin group resolution:

  1. PostOwnsAndWriteOwner now accepts *LocalGroupData and extracts GroupMembershipCache for the downstream call
  2. FetchAdminGroupIds signature updated to accept *algo.ReachabilityCache directly
  3. The OrReach call (Line 208) replaces the previous Cardinality approach, accumulating all principals with inbound reachability to admin groups into adminIds

The logic correctly identifies Domain Admins/Enterprise Admins and their transitive members by leveraging the reachability cache's graph-based expansion.

Also applies to: 45-45, 196-208

packages/go/analysis/ad/esc9.go (1)

35-35: LGTM - ESC9a/9b updated to use LocalGroupData.

Both functions are correctly updated:

  • PostADCSESC9a and PostADCSESC9b signatures now accept *LocalGroupData
  • The getVictimBitmap calls pass localGroupData as the first argument, matching the updated signature in esc_shared.go

The established error handling pattern (using slog.WarnContext and continuing) is preserved. Based on learnings, this is the correct pattern for ADCS ESC analysis functions.

Also applies to: 53-53, 84-84, 102-102

packages/go/analysis/ad/esc_shared.go (2)

394-399: Signature update looks correct.

The getVictimBitmap function correctly accepts *LocalGroupData and extracts GroupMembershipCache for the downstream expandNodeSliceToBitmapWithoutGroups calls. The logic for combining template and ECA enrollers based on special group enrollment is preserved.

Note: The output depends on expandNodeSliceToBitmapWithoutGroups, which has the commented-out group filtering logic flagged above.


303-334: The design is correct as implemented. The comment "Expand controllers for the eca + template completely because we don't do group shortcutting here" explicitly indicates that full expansion of all group members (including nested groups) is intentional. The function name expandNodeSliceToBitmapWithoutGroups means the result bitmap contains expanded members without the group nodes themselves—not that groups should be filtered from the result.

The commented-out code that checks if members are groups was intentionally disabled because the analysis requires complete expansion without optimization shortcuts. This is a deliberate design choice, not incomplete refactoring.

cmd/api/src/analysis/ad/ntlm_integration_test.go (2)

49-52: LGTM: NTLM integration tests now use LocalGroupData-backed cache.

Call sites consistently pass LocalGroupData into NewNTLMCache and protected user lookups, matching the new API flow.

Also applies to: 96-99, 154-157, 230-233, 282-285, 354-361, 434-441, 510-517, 565-572


613-630: LGTM: fetchNTLMPrereqs now centralizes LocalGroupData retrieval.

Using FetchLocalGroupData in the test helper keeps setup aligned with the production data pipeline.

packages/go/analysis/ad/adcs.go (1)

36-70: LGTM: LocalGroupData is threaded through ADCS post-processing.

Passing LocalGroupData into ESC handlers keeps the ADCS pipeline consistent with the new local-group cache model.

Also applies to: 109-207

cmd/api/src/analysis/ad/adcs_integration_test.go (1)

66-3711: LGTM: ESC integration tests now pass LocalGroupData into handlers.

Call sites are consistently updated to the new LocalGroupData-based signatures across ESC test cases.

packages/go/analysis/ad/ntlm.go (1)

40-46: LGTM: LocalGroupData cache now powers NTLM group expansion.

Using LocalGroupData.GroupMembershipCache for template/ECA and SMB admin expansion keeps NTLM analysis aligned with the new local-group data flow.

Also applies to: 442-444, 588-589

packages/go/analysis/ad/ad.go (1)

475-507: No defensive guard needed—LocalGroupData is always initialized at call sites.

FetchLocalGroupData either returns a fully initialized *LocalGroupData with all fields set (including GroupMembershipCache) or returns (nil, error). All callers consistently check for error before using the result:

if localGroupData, err := adAnalysis.FetchLocalGroupData(ctx, db); err != nil {
    return &aggregateStats, err
} else {
    // localGroupData is guaranteed non-nil here
}

The proposed guard is redundant given this pattern is enforced across the entire codebase.

packages/go/analysis/ad/post.go (5)

39-72: LGTM! Signature updates align with LocalGroupData refactoring.

The PostSyncLAPSPassword and PostDCSync functions correctly propagate the *LocalGroupData parameter to getLAPSSyncers and getDCSyncers respectively. The operation flow remains consistent with the previous implementation.

Also applies to: 74-104


536-564: LGTM! Function signature and logic are correct.

The signature change to accept *CanRDPComputerData is properly implemented. The function correctly handles the citrix-enabled and URA scenarios, using the embedded LocalGroupData for cross-product calculations.


566-591: LGTM! Query error is now properly captured and returned.

The previous issue where the query error was ignored has been addressed. The error is now assigned to the err variable and returned along with the result.


754-758: Dependent on fix for FetchComputerLocalGroupBySIDSuffix.

This code will silently receive a pointer to a zero-value node when no Remote Desktop Users local group exists for the computer, due to the bug in FetchComputerLocalGroupBySIDSuffix. Once that function is fixed to return graph.ErrNoResultsFound, this call site should handle that error appropriately (either as an error or by setting RemoteDesktopUsersLocalGroup to nil).


791-821: LGTM! ProcessRDPWithUra logic is correct.

The refactored implementation properly uses the cached digraphs and reachability cache for RDP permission checking. The two-phase approach (direct RIL entity check, then expanded group membership check) is sound.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +275 to +310
localAdminGroup, localAdminGroupErr = FetchComputerLocalGroupIDBySIDSuffix(tx, graph.ID(computerID), adminGroupSuffix)
localPSRemoteGroup, localPSRemoteGroupErr = FetchComputerLocalGroupIDBySIDSuffix(tx, graph.ID(computerID), psRemoteGroupSuffix)
localDCOMGroup, localDCOMGroupErr = FetchComputerLocalGroupIDBySIDSuffix(tx, graph.ID(computerID), dcomGroupSuffix)
)

if localAdminGroupErr == nil {
numGroupsProcessed.Add(1)

channels.Submit(ctx, reachC, reachJob{
targetComputer: computerID,
targetGroup: localAdminGroup.Uint64(),
groupSuffix: adminGroupSuffix,
})
}

if localPSRemoteGroupErr == nil {
numGroupsProcessed.Add(1)

channels.Submit(ctx, reachC, reachJob{
targetComputer: computerID,
targetGroup: localPSRemoteGroup.Uint64(),
groupSuffix: psRemoteGroupSuffix,
})
}

if localDCOMGroupErr == nil {
numGroupsProcessed.Add(1)

channels.Submit(ctx, reachC, reachJob{
targetComputer: computerID,
targetGroup: localDCOMGroup.Uint64(),
groupSuffix: dcomGroupSuffix,
})
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t swallow local group lookup errors.

If FetchComputerLocalGroupIDBySIDSuffix fails, the error is ignored and edges are silently skipped. Returning the error makes failures visible and avoids partial post-processing.

🔧 Suggested fix
-						if localAdminGroupErr == nil {
+						if localAdminGroupErr == nil {
 							numGroupsProcessed.Add(1)

 							channels.Submit(ctx, reachC, reachJob{
 								targetComputer: computerID,
 								targetGroup:    localAdminGroup.Uint64(),
 								groupSuffix:    adminGroupSuffix,
 							})
+						} else {
+							return localAdminGroupErr
 						}

-						if localPSRemoteGroupErr == nil {
+						if localPSRemoteGroupErr == nil {
 							numGroupsProcessed.Add(1)

 							channels.Submit(ctx, reachC, reachJob{
 								targetComputer: computerID,
 								targetGroup:    localPSRemoteGroup.Uint64(),
 								groupSuffix:    psRemoteGroupSuffix,
 							})
+						} else {
+							return localPSRemoteGroupErr
 						}

-						if localDCOMGroupErr == nil {
+						if localDCOMGroupErr == nil {
 							numGroupsProcessed.Add(1)

 							channels.Submit(ctx, reachC, reachJob{
 								targetComputer: computerID,
 								targetGroup:    localDCOMGroup.Uint64(),
 								groupSuffix:    dcomGroupSuffix,
 							})
+						} else {
+							return localDCOMGroupErr
 						}
🤖 Prompt for AI Agents
In `@packages/go/analysis/ad/local_groups.go` around lines 275 - 308, The three
calls to FetchComputerLocalGroupIDBySIDSuffix (localAdminGroup,
localPSRemoteGroup, localDCOMGroup) currently ignore their errors
(localAdminGroupErr, localPSRemoteGroupErr, localDCOMGroupErr) and silently skip
processing; change the logic to propagate or surface these errors instead of
swallowing them—e.g., for each fetch, if err != nil wrap/return the error (or
accumulate and return a combined error) with context like which computerID and
suffix failed so callers can see failures; update the surrounding function to
return an error (or accept/return a multi-error) and remove the silent-nil-only
behavior before calling numGroupsProcessed.Add and channels.Submit for reachJob
submissions.

Copy link
Contributor

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
packages/go/analysis/ad/post.go (1)

206-223: Use NodeProperty filters when querying nodes.

FetchAdminGroups uses query.StartProperty / query.EndProperty in a tx.Nodes() filter. These accessors are relationship-scoped and don't apply to node queries, causing incorrect filtering. Use query.NodeProperty instead, as shown in similar patterns throughout the codebase (e.g., owns.go).

🔧 Proposed fix
 	return computerNodeIds, db.ReadTransaction(ctx, func(tx graph.Transaction) error {
 		return tx.Nodes().Filter(query.And(
-			query.Or(
-				query.StringEndsWith(query.StartProperty(common.ObjectID.String()), wellknown.AdministratorsSIDSuffix.String()),
-				query.StringEndsWith(query.EndProperty(common.ObjectID.String()), wellknown.AdministratorsSIDSuffix.String()),
-			),
+			query.StringEndsWith(query.NodeProperty(common.ObjectID.String()), wellknown.AdministratorsSIDSuffix.String()),
 		)).FetchIDs(func(cursor graph.Cursor[graph.ID]) error {
 			for id := range cursor.Chan() {
 				computerNodeIds.Add(id.Uint64())
 			}
🤖 Fix all issues with AI agents
In `@packages/go/analysis/ad/esc_shared.go`:
- Around line 303-327: The expandNodeSliceToBitmapWithoutGroups function
currently never filters out group IDs because the group detection block is
commented out; restore the group-detection logic in
expandNodeSliceToBitmapWithoutGroups by using the graph adjacency iteration on
groupExpansions.Digraph (or the appropriate Digraph instance) to check each
memberID for inbound adjacent nodes and set isGroup = true when any inbound
adjacent node exists, then only add non-group memberIDs to bitmap; if the Local
Group Post-Processing rework intentionally removed group filtering, instead
rename the function and update callers (e.g., getVictimBitmap) to reflect that
it includes groups so behavior is explicit.

In `@packages/go/analysis/ad/local_groups.go`:
- Line 182: The deferred call currently defers the ContextMeasure function
itself instead of its returned closure, so change the defer in PostLocalGroups
to invoke the returned function; e.g. call measure.ContextMeasure(ctx,
slog.LevelInfo, "PostLocalGroups") and immediately call the result (defer
measure.ContextMeasure(... )()) or assign the returned func to a variable and
defer that (e.g. end := measure.ContextMeasure(...); defer end()) so the timing
closure actually runs.
♻️ Duplicate comments (3)
packages/go/analysis/ad/local_groups.go (1)

277-309: Still swallowing local group lookup errors.

The error returns from FetchComputerLocalGroupIDBySIDSuffix are ignored, which can silently skip edges and hide collection gaps. This was already raised earlier.

packages/go/analysis/ad/post.go (2)

363-393: Missing not-found handling for local group lookup.

FetchComputerLocalGroupBySIDSuffix returns a zero-value node when no rows match. This was already flagged earlier; please ensure the fix is applied here too.


395-426: Missing not-found handling for local group ID lookup.

FetchComputerLocalGroupIDBySIDSuffix still returns zero graph.ID when no rows match. This was already flagged earlier; please ensure the fix is applied here too.

🧹 Nitpick comments (4)
cmd/api/src/analysis/ad/ntlm_integration_test.go (1)

154-157: Consider renaming grouplocalGroupData to localGroupData for clarity.
This is a minor readability nit across multiple tests.

♻️ Suggested rename (apply similarly in each test)
-			grouplocalGroupData, computers, _, authenticatedUsers, err := fetchNTLMPrereqs(db)
+			localGroupData, computers, _, authenticatedUsers, err := fetchNTLMPrereqs(db)
 			require.NoError(t, err)
-			ntlmCache, err := ad2.NewNTLMCache(context.Background(), db, grouplocalGroupData)
+			ntlmCache, err := ad2.NewNTLMCache(context.Background(), db, localGroupData)

Also applies to: 230-233, 282-285, 354-361, 434-441, 510-517, 565-572

packages/go/analysis/ad/owns.go (1)

196-209: Add a defensive nil guard for groupExpansions to avoid panics.
If a caller passes a nil cache, OrReach will panic; returning an error makes failures explicit.

🛡️ Suggested guard
 func FetchAdminGroupIds(ctx context.Context, db graph.Database, groupExpansions *algo.ReachabilityCache) (cardinality.Duplex[uint64], error) {
 	adminIds := cardinality.NewBitmap64()
+
+	if groupExpansions == nil {
+		return adminIds, errors.New("groupExpansions is nil")
+	}
 
 	return adminIds, db.ReadTransaction(ctx, func(tx graph.Transaction) error {
packages/go/analysis/ad/ntlm.go (1)

63-83: Guard against nil LocalGroupData before dereference.

FetchProtectedUsersMappedToDomains (and the call chain from NewNTLMCache) assumes a non-nil localGroupData and GroupMembershipCache. If a caller passes nil, this will panic. Consider failing fast with a clear error.

✅ Suggested guard
 func FetchProtectedUsersMappedToDomains(ctx context.Context, db graph.Database, localGroupData *LocalGroupData) (map[string]cardinality.Duplex[uint64], error) {
+	if localGroupData == nil || localGroupData.GroupMembershipCache == nil {
+		return nil, fmt.Errorf("localGroupData.GroupMembershipCache is required")
+	}
 	protectedUsers := make(map[string]cardinality.Duplex[uint64])

Also applies to: 855-870

cmd/api/src/analysis/membership_integration_test.go (1)

78-78: Migrate away from deprecated integration.NewGraphTestContext.

Staticcheck flags this helper as deprecated; prefer the newer integration test utilities to avoid future breakage.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b328188 and 0c951fb.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (25)
  • cmd/api/src/analysis/ad/ad_integration_test.go
  • cmd/api/src/analysis/ad/adcs_integration_test.go
  • cmd/api/src/analysis/ad/ntlm_integration_test.go
  • cmd/api/src/analysis/ad/post.go
  • cmd/api/src/analysis/analysis_integration_test.go
  • cmd/api/src/analysis/membership_integration_test.go
  • cmd/api/src/analysis/post_integration_test.go
  • cmd/api/src/daemons/datapipe/datapipe.go
  • go.mod
  • packages/go/analysis/ad/ad.go
  • packages/go/analysis/ad/adcs.go
  • packages/go/analysis/ad/adcscache.go
  • packages/go/analysis/ad/esc1.go
  • packages/go/analysis/ad/esc10.go
  • packages/go/analysis/ad/esc13.go
  • packages/go/analysis/ad/esc3.go
  • packages/go/analysis/ad/esc4.go
  • packages/go/analysis/ad/esc6.go
  • packages/go/analysis/ad/esc9.go
  • packages/go/analysis/ad/esc_shared.go
  • packages/go/analysis/ad/local_groups.go
  • packages/go/analysis/ad/membership.go
  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/owns.go
  • packages/go/analysis/ad/post.go
💤 Files with no reviewable changes (1)
  • cmd/api/src/daemons/datapipe/datapipe.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmd/api/src/analysis/post_integration_test.go
🧰 Additional context used
🧠 Learnings (14)
📚 Learning: 2025-05-27T16:58:33.295Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1503
File: cmd/api/src/services/job/jobs_test.go:19-143
Timestamp: 2025-05-27T16:58:33.295Z
Learning: Tests in cmd/api/src/services/job/jobs_test.go have been found to be flaky in the past and are due for rewrite. They should be skipped with t.Skip() until they can be properly rewritten.

Applied to files:

  • cmd/api/src/analysis/membership_integration_test.go
  • cmd/api/src/analysis/ad/post.go
  • packages/go/analysis/ad/local_groups.go
  • cmd/api/src/analysis/analysis_integration_test.go
  • packages/go/analysis/ad/post.go
📚 Learning: 2025-06-06T23:12:14.181Z
Learnt from: elikmiller
Repo: SpecterOps/BloodHound PR: 1563
File: packages/go/graphschema/azure/azure.go:24-24
Timestamp: 2025-06-06T23:12:14.181Z
Learning: In BloodHound, files in packages/go/graphschema/*/`*.go` are generated from CUE schemas. When `just prepare-for-codereview` is run, it triggers code generation that may automatically add import aliases or other formatting changes. These changes are legitimate outputs of the generation process, not manual edits that would be overwritten.

Applied to files:

  • cmd/api/src/analysis/membership_integration_test.go
  • packages/go/analysis/ad/adcscache.go
  • packages/go/analysis/ad/esc13.go
  • cmd/api/src/analysis/ad/post.go
  • packages/go/analysis/ad/esc1.go
  • packages/go/analysis/ad/owns.go
  • packages/go/analysis/ad/esc_shared.go
  • cmd/api/src/analysis/ad/ntlm_integration_test.go
  • packages/go/analysis/ad/esc6.go
  • packages/go/analysis/ad/esc4.go
  • packages/go/analysis/ad/ntlm.go
  • packages/go/analysis/ad/esc10.go
  • packages/go/analysis/ad/adcs.go
  • cmd/api/src/analysis/ad/adcs_integration_test.go
  • packages/go/analysis/ad/esc9.go
  • packages/go/analysis/ad/esc3.go
  • packages/go/analysis/ad/ad.go
  • go.mod
📚 Learning: 2025-06-25T17:52:33.291Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1606
File: cmd/api/src/analysis/azure/post.go:33-35
Timestamp: 2025-06-25T17:52:33.291Z
Learning: In BloodHound Go code, prefer using explicit slog type functions like slog.Any(), slog.String(), slog.Int(), etc. over simple key-value pairs for structured logging. This provides better type safety and makes key-value pairs more visually distinct. For error types, use slog.Any("key", err) or slog.String("key", err.Error()).

Applied to files:

  • packages/go/analysis/ad/adcscache.go
  • packages/go/analysis/ad/local_groups.go
  • packages/go/analysis/ad/owns.go
  • packages/go/analysis/ad/adcs.go
  • packages/go/analysis/ad/ad.go
📚 Learning: 2025-05-29T18:24:23.227Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1509
File: packages/go/stbernard/command/license/internal/cmd.go:49-51
Timestamp: 2025-05-29T18:24:23.227Z
Learning: In the stbernard package, use slog for all logging prints instead of fmt.Printf/fmt.Fprintf to maintain consistency with the codebase's logging standards.

Applied to files:

  • packages/go/analysis/ad/adcscache.go
  • packages/go/analysis/ad/ad.go
📚 Learning: 2025-06-25T18:24:25.014Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1606
File: cmd/api/src/analysis/azure/post.go:33-35
Timestamp: 2025-06-25T18:24:25.014Z
Learning: In BloodHound Go code, for error types in slog structured logging, prefer using slog.String("key", err.Error()) over slog.Any("key", err). The explicit string conversion with err.Error() is preferred over using slog.Any() for error types.

Applied to files:

  • packages/go/analysis/ad/adcscache.go
  • packages/go/analysis/ad/local_groups.go
  • packages/go/analysis/ad/owns.go
📚 Learning: 2025-07-10T14:31:01.781Z
Learnt from: JonasBK
Repo: SpecterOps/BloodHound PR: 1671
File: packages/go/analysis/ad/esc16.go:65-69
Timestamp: 2025-07-10T14:31:01.781Z
Learning: In BloodHound ADCS ESC analysis functions (PostADCSESC1, PostADCSESC4, PostADCSESC6, PostADCSESC9, PostADCSESC10, PostADCSESC13, PostADCSESC16, etc.), the established pattern for handling cert template validation errors and user filtering errors is to use slog.WarnContext to log warnings and continue processing with the next template, rather than returning errors to the caller. This pattern is consistently applied across all ADCS ESC edge implementations.

Applied to files:

  • packages/go/analysis/ad/esc13.go
  • cmd/api/src/analysis/ad/post.go
  • packages/go/analysis/ad/esc1.go
  • packages/go/analysis/ad/esc6.go
  • packages/go/analysis/ad/esc4.go
  • packages/go/analysis/ad/esc10.go
  • packages/go/analysis/ad/esc9.go
  • packages/go/analysis/ad/esc3.go
  • packages/go/analysis/ad/ad.go
📚 Learning: 2025-11-05T21:13:08.542Z
Learnt from: ktstrader
Repo: SpecterOps/BloodHound PR: 2020
File: cmd/api/src/queries/graph.go:198-203
Timestamp: 2025-11-05T21:13:08.542Z
Learning: In cmd/api/src/queries/graph.go, when ETAC filtering is enabled in GetAssetGroupComboNode, an empty etacAllowedList (when user.AllEnvironments is false and user.EnvironmentTargetedAccessControl is empty) is intentional and should not be guarded against. The empty list will cause query.In() to filter out all nodes, which is the correct security behavior—users with no environment assignments should see no results.

Applied to files:

  • packages/go/analysis/ad/esc13.go
  • packages/go/analysis/ad/ntlm.go
  • cmd/api/src/analysis/analysis_integration_test.go
  • packages/go/analysis/ad/post.go
📚 Learning: 2025-05-23T19:56:13.423Z
Learnt from: definitelynotagoblin
Repo: SpecterOps/BloodHound PR: 1440
File: packages/go/analysis/ad/ntlm.go:123-138
Timestamp: 2025-05-23T19:56:13.423Z
Learning: In the BloodHound NTLM analysis code (packages/go/analysis/ad/ntlm.go), the `isProtectedComputer` function intentionally fails open (returns false/unprotected) when the Protected Users group cannot be retrieved, maintaining consistency with the original logic patterns in the codebase.

Applied to files:

  • cmd/api/src/analysis/ad/post.go
  • packages/go/analysis/ad/local_groups.go
  • cmd/api/src/analysis/ad/ntlm_integration_test.go
  • packages/go/analysis/ad/ntlm.go
  • cmd/api/src/analysis/analysis_integration_test.go
  • packages/go/analysis/ad/post.go
📚 Learning: 2025-08-27T21:15:32.207Z
Learnt from: jvacca-specterops
Repo: SpecterOps/BloodHound PR: 1823
File: packages/javascript/bh-shared-ui/src/commonSearchesAGT.ts:184-187
Timestamp: 2025-08-27T21:15:32.207Z
Learning: In the BloodHound codebase, syntax fixes for invalid Cypher patterns (like `*..` → `*1..`) may be considered out of scope for specific PRs, even when flagged during review.

Applied to files:

  • packages/go/analysis/ad/local_groups.go
📚 Learning: 2025-07-10T14:33:20.317Z
Learnt from: JonasBK
Repo: SpecterOps/BloodHound PR: 1671
File: cmd/api/src/analysis/ad/adcs_integration_test.go:3687-3687
Timestamp: 2025-07-10T14:33:20.317Z
Learning: When reviewing Go code, functions defined in one file within a package are accessible from other files in the same package. Before flagging missing functions as compilation errors, check if they exist in other files within the same package directory.

Applied to files:

  • packages/go/analysis/ad/owns.go
📚 Learning: 2025-08-28T16:43:43.961Z
Learnt from: mvlipka
Repo: SpecterOps/BloodHound PR: 1784
File: packages/go/openapi/doc/openapi.json:18008-18029
Timestamp: 2025-08-28T16:43:43.961Z
Learning: In SpecterOps/BloodHound, packages/go/openapi/doc/openapi.json is generated from YAML under packages/go/openapi/src/schemas; edits must be made to the YAML and then the spec regenerated.

Applied to files:

  • cmd/api/src/analysis/ad/ntlm_integration_test.go
  • cmd/api/src/analysis/ad/adcs_integration_test.go
📚 Learning: 2025-07-22T20:30:34.839Z
Learnt from: LawsonWillard
Repo: SpecterOps/BloodHound PR: 1700
File: cmd/api/src/api/v2/saved_queries_test.go:3182-3182
Timestamp: 2025-07-22T20:30:34.839Z
Learning: In Go table-driven tests in cmd/api/src/api/v2/saved_queries_test.go, subtest parallelization with t.Parallel() is acceptable when tests are self-contained, each creating their own mock controller (gomock.NewController(t)) and having isolated mock expectations without shared state between subtests.

Applied to files:

  • cmd/api/src/analysis/ad/ad_integration_test.go
📚 Learning: 2025-06-30T22:58:54.834Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1629
File: go.mod:18-18
Timestamp: 2025-06-30T22:58:54.834Z
Learning: Go 1.24 supports patch versions in the go directive of go.mod files. Syntax like "go 1.24.4" is valid and specifies the minimum Go version required to build the module, including the patch level.

Applied to files:

  • go.mod
📚 Learning: 2025-08-26T18:13:36.643Z
Learnt from: superlinkx
Repo: SpecterOps/BloodHound PR: 1820
File: .github/workflows/audit-release.yml:0-0
Timestamp: 2025-08-26T18:13:36.643Z
Learning: The go tool command supports custom/third-party binaries as of Go 1.24, allowing invocation of custom CLI tools like "go tool stbernard" rather than being limited to only built-in Go toolchain binaries.

Applied to files:

  • go.mod
🧬 Code graph analysis (15)
cmd/api/src/analysis/membership_integration_test.go (2)
cmd/api/src/test/integration/harnesses.go (1)
  • HarnessDetails (10107-10221)
packages/go/graphschema/ad/ad.go (1)
  • MemberOfLocalGroup (81-81)
packages/go/analysis/ad/adcscache.go (1)
packages/go/bhlog/measure/measure.go (1)
  • ContextMeasure (36-45)
packages/go/analysis/ad/esc13.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (539-551)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (477-638)
cmd/api/src/analysis/ad/post.go (2)
packages/go/analysis/post.go (1)
  • DeleteTransitEdges (164-202)
packages/go/analysis/ad/post.go (2)
  • FetchLocalGroupData (554-588)
  • PostDCSync (73-103)
packages/go/analysis/ad/esc1.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (539-551)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (477-638)
packages/go/analysis/ad/esc_shared.go (2)
packages/go/graphschema/ad/ad.go (1)
  • Group (31-31)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (539-551)
cmd/api/src/analysis/ad/ntlm_integration_test.go (2)
packages/go/analysis/ad/ntlm.go (3)
  • NewNTLMCache (63-124)
  • FetchLDAPSigningCache (892-968)
  • FetchProtectedUsersMappedToDomains (855-880)
packages/go/analysis/ad/post.go (2)
  • LocalGroupData (539-551)
  • FetchLocalGroupData (554-588)
cmd/api/src/analysis/ad/ad_integration_test.go (2)
packages/go/analysis/ad/post.go (3)
  • FetchLocalGroupData (554-588)
  • PostSyncLAPSPassword (38-71)
  • PostDCSync (73-103)
packages/go/analysis/ad/owns.go (1)
  • PostOwnsAndWriteOwner (38-164)
packages/go/analysis/ad/esc4.go (3)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (539-551)
packages/go/analysis/ad/adcscache.go (1)
  • ADCSCache (33-55)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (477-638)
packages/go/analysis/ad/ntlm.go (1)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (539-551)
packages/go/analysis/ad/esc10.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (539-551)
packages/go/analysis/ad/adcscache.go (1)
  • ADCSCache (33-55)
packages/go/analysis/ad/adcs.go (7)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (539-551)
packages/go/analysis/ad/adcscache.go (1)
  • ADCSCache (33-55)
packages/go/analysis/ad/esc1.go (1)
  • PostADCSESC1 (35-64)
packages/go/analysis/ad/esc6.go (2)
  • PostADCSESC6a (37-80)
  • PostADCSESC6b (82-127)
packages/go/analysis/ad/esc9.go (2)
  • PostADCSESC9a (35-82)
  • PostADCSESC9b (84-128)
packages/go/analysis/ad/esc10.go (2)
  • PostADCSESC10a (36-82)
  • PostADCSESC10b (84-127)
packages/go/analysis/ad/esc13.go (1)
  • PostADCSESC13 (36-78)
packages/go/analysis/ad/esc9.go (2)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (539-551)
packages/go/analysis/ad/adcscache.go (1)
  • ADCSCache (33-55)
packages/go/analysis/ad/esc3.go (3)
packages/go/analysis/ad/post.go (1)
  • LocalGroupData (539-551)
packages/go/analysis/ad/adcscache.go (1)
  • ADCSCache (33-55)
packages/go/analysis/ad/ad.go (1)
  • CalculateCrossProductNodeSets (477-638)
cmd/api/src/analysis/analysis_integration_test.go (2)
packages/go/analysis/ad/post.go (3)
  • CanRDPData (627-635)
  • FetchLocalGroupData (554-588)
  • FetchCanRDPEntityBitmapForComputer (470-498)
cmd/api/src/test/integration/harnesses.go (1)
  • RDPHarnessWithCitrix (579-602)
🪛 golangci-lint (2.5.0)
cmd/api/src/analysis/membership_integration_test.go

[minor] 78-78: SA1019: integration.NewGraphTestContext is deprecated: this suite of integration utils is deprecated and should be avoided Integration tests should be updated to reflect the latest standards. See commit a6cc430#diff-e6bcb50873ade3cf33cef4e3e0ff566fb8ac1367b4ade36f4511bc2172a760e1 for implementation guidance. Additional detailed information can be found in Confluence.

(staticcheck)

cmd/api/src/analysis/analysis_integration_test.go

[minor] 45-45: SA1019: integration.NewGraphTestContext is deprecated: this suite of integration utils is deprecated and should be avoided Integration tests should be updated to reflect the latest standards. See commit a6cc430#diff-e6bcb50873ade3cf33cef4e3e0ff566fb8ac1367b4ade36f4511bc2172a760e1 for implementation guidance. Additional detailed information can be found in Confluence.

(staticcheck)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build BloodHound Container Image / Build and Package Container
  • GitHub Check: run-tests
  • GitHub Check: run-go-unit-tests
  • GitHub Check: build-ui
  • GitHub Check: run-analysis
🔇 Additional comments (39)
packages/go/analysis/ad/adcscache.go (1)

75-76: BuildCache timing instrumentation looks good.

Nice, low‑impact observability addition.

packages/go/analysis/ad/membership.go (1)

92-94: Inline reverse direction is fine.

This simplifies the traversal without changing behavior.

cmd/api/src/analysis/ad/ad_integration_test.go (1)

1144-1147: LGTM — tests now align with LocalGroupData-based APIs.

Good update to the new post-processing flow.

Also applies to: 1175-1178, 1207-1210, 1410-1413

go.mod (1)

21-60: Verify compatibility of dependency bumps via integration tests rather than release notes.

The versions github.com/specterops/dawgs v0.4.2 and github.com/jackc/pgx/v5 v5.8.0 are valid Go module entries (confirmed in go.sum), but public release notes are not available. Since LocalGroupData underwent a recent refactor (BED-7147), run the full integration test suite to confirm no behavioral regressions with these upgraded dependencies, particularly for graph schema operations and database access patterns.

Also applies to: 87-87, 276-281

packages/go/analysis/ad/esc1.go (1)

35-35: LGTM — LocalGroupData threaded into ESC1 cross-product.

Signature and call-site are aligned with the new LocalGroupData-based cross-product API.

Also applies to: 48-48

packages/go/analysis/ad/esc13.go (1)

36-36: LGTM — ESC13 cross-product now uses LocalGroupData.

The signature update and call-site wiring match the updated CalculateCrossProductNodeSets API.

Also applies to: 53-53

packages/go/analysis/ad/esc3.go (1)

38-38: LGTM — ESC3 paths updated consistently for LocalGroupData.

Both cross-product call sites and the function signature are aligned with the new API.

Also applies to: 91-97, 109-114

cmd/api/src/analysis/ad/post.go (1)

31-65: LGTM — centralized LocalGroupData reuse in AD post-processing.

Fetching LocalGroupData once and reusing it across downstream steps keeps the data flow consistent.

packages/go/analysis/ad/esc4.go (1)

35-35: LGTM — ESC4 cross-product calls aligned with LocalGroupData contract.

Signature and all affected call sites now match the updated CalculateCrossProductNodeSets API.

Also applies to: 63-67, 71-76, 88-94, 99-104, 111-116

cmd/api/src/analysis/ad/ntlm_integration_test.go (2)

49-52: LocalGroupData-based NTLM cache setup looks consistent.

Also applies to: 96-99


613-630: fetchNTLMPrereqs now centralizes LocalGroupData retrieval — LGTM.

packages/go/analysis/ad/owns.go (1)

38-45: LocalGroupData is now wired into Owns/WriteOwner processing as expected.

packages/go/analysis/ad/esc9.go (1)

35-54: Victim bitmap now consistently derives from LocalGroupData.

Also applies to: 84-103

packages/go/analysis/ad/ad.go (1)

477-623: Cross-product computation now consistently uses LocalGroupData — looks good.

cmd/api/src/analysis/analysis_integration_test.go (2)

36-41: Helper to build CanRDPData keeps the tests DRY.


50-58: Per-computer CanRDP data flow updates look consistent.

Also applies to: 76-84, 110-114, 142-151, 173-182, 197-204, 222-230

packages/go/analysis/ad/esc_shared.go (2)

27-31: Import update only.


394-399: LocalGroupData integration looks consistent.

The victim bitmap now flows through LocalGroupData’s membership cache as expected.

packages/go/analysis/ad/adcs.go (1)

36-71: LocalGroupData propagation is consistent.

Passing LocalGroupData through PostADCS into all ESC handlers matches the refactor direction.

Also applies to: 109-207

cmd/api/src/analysis/ad/adcs_integration_test.go (2)

41-53: Prereq helper updated cleanly.

Fetching LocalGroupData in the shared helper aligns with the new API surface.


66-3692: Test call sites updated consistently.

All ESC post-processing invocations now thread LocalGroupData through; the refactor looks uniform across the integration suite.

packages/go/analysis/ad/esc6.go (1)

37-59: ESC6 LocalGroupData wiring looks good.

Signature updates and cross-product calls are consistent with the refactor.

Also applies to: 82-104

packages/go/analysis/ad/esc10.go (1)

36-55: ESC10 LocalGroupData integration looks consistent.

Victim bitmap computation now correctly flows through LocalGroupData.

Also applies to: 84-103

packages/go/analysis/ad/ntlm.go (3)

40-46: LGTM: LocalGroupData cached on NTLMCache.

The new cache field wiring is straightforward and aligns with the refactor.


126-166: LGTM: PostNTLM now threads LocalGroupData through cache initialization.


442-444: LGTM: Group expansion switched to LocalGroupData reachability cache.

Also applies to: 588-589

packages/go/analysis/ad/post.go (11)

26-103: LGTM: PostSyncLAPSPassword/PostDCSync now consume LocalGroupData.


186-204: LGTM: FetchComputerIDs added with timing instrumentation.


285-316: LGTM: LAPS/DCSync cross-product now uses LocalGroupData.


428-442: LGTM: local group lookup by name.


444-498: LGTM: RDP local group membership + CanRDP bitmap computation.


500-535: LGTM: FetchComputersWithURA bitmap caching.


537-588: LGTM: LocalGroupData structure and fetcher wiring.


590-635: LGTM: CanRDPData aggregation from LocalGroupData.


637-666: LGTM: RemoteInteractiveLogonRight cache lookup + per-computer data struct.


668-697: LGTM: FetchCanRDPComputerData flow is consistent.


699-738: LGTM: RDP bitmap selection and URA processing.

cmd/api/src/analysis/membership_integration_test.go (2)

30-30: No issues to note for this import change.


83-94: Verify that ReachOfComponentContainingMember is available in DAWGS v0.4.2 and includes the origin member with DirectionInbound.

The method ReachOfComponentContainingMember could not be located in DAWGS v0.4.1 (the latest public release) or in any publicly available source code. Additionally, go.mod specifies v0.4.2, which does not have a public release or tag on GitHub—only v0.4.1 exists as of Jan 15, 2026. Confirm:

  1. Whether v0.4.2 is an internal pre-release or if a different version should be used
  2. The actual method name on algo.ReachabilityCache if it differs from ReachOfComponentContainingMember
  3. Whether DirectionInbound includes the origin member as the code comment claims

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +303 to 327
func expandNodeSliceToBitmapWithoutGroups(nodes []*graph.Node, groupExpansions *algo.ReachabilityCache) cardinality.Duplex[uint64] {
var (
bitmap = cardinality.NewBitmap64()
scratch = cardinality.NewBitmap64()
)

for _, controller := range nodes {
if controller.Kinds.ContainsOneOf(ad.Group) {
groupExpansions.Cardinality(controller.ID.Uint64()).(cardinality.Duplex[uint64]).Each(func(id uint64) bool {
//Check group expansions against each id, if cardinality is 0 than its not a group
if groupExpansions.Cardinality(id).Cardinality() == 0 {
bitmap.Add(id)
groupExpansions.OrReach(controller.ID.Uint64(), graph.DirectionInbound, scratch)

scratch.Each(func(memberID uint64) bool {
isGroup := false

// groupExpansions.Digraph.EachAdjacentNode(memberID, graph.DirectionInbound, func(adjacent uint64) bool {
// // If the expanded member of the controller group has an adjacent inbound node then it must be a group
// isGroup = true
// return false
// })

if !isGroup {
bitmap.Add(memberID)
}

return true
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat packages/go/analysis/ad/esc_shared.go

Repository: SpecterOps/BloodHound

Length of output: 15054


🏁 Script executed:

rg "expandNodeSliceToBitmapWithoutGroups" -B 5 -A 5

Repository: SpecterOps/BloodHound

Length of output: 3164


🏁 Script executed:

rg "WithoutGroups" --type go -B 2 -A 2

Repository: SpecterOps/BloodHound

Length of output: 1823


🏁 Script executed:

git log --all --oneline -S "expandNodeSliceToBitmapWithoutGroups" -- packages/go/analysis/ad/

Repository: SpecterOps/BloodHound

Length of output: 195


🏁 Script executed:

git log --all -p --follow -S "isGroup := false" -- packages/go/analysis/ad/esc_shared.go | head -100

Repository: SpecterOps/BloodHound

Length of output: 3786


Group filtering is disabled due to commented-out detection logic.

isGroup is never set because the group detection block is commented out, so group IDs are added to the "WithoutGroups" bitmap. This contradicts the function name and can inflate ESC victim expansion results when used in getVictimBitmap() and similar calculations. Confirm whether this is intentional from the recent Local Group Post-Processing rework, and either restore the group filtering or update the function name and callers to reflect the new behavior.

🤖 Prompt for AI Agents
In `@packages/go/analysis/ad/esc_shared.go` around lines 303 - 327, The
expandNodeSliceToBitmapWithoutGroups function currently never filters out group
IDs because the group detection block is commented out; restore the
group-detection logic in expandNodeSliceToBitmapWithoutGroups by using the graph
adjacency iteration on groupExpansions.Digraph (or the appropriate Digraph
instance) to check each memberID for inbound adjacent nodes and set isGroup =
true when any inbound adjacent node exists, then only add non-group memberIDs to
bitmap; if the Local Group Post-Processing rework intentionally removed group
filtering, instead rename the function and update callers (e.g.,
getVictimBitmap) to reflect that it includes groups so behavior is explicit.

groupSuffix string
}

defer measure.ContextMeasure(ctx, slog.LevelInfo, "PostLocalGroups")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix deferred measure invocation so timing actually runs.

Right now the deferred call schedules ContextMeasure itself instead of the returned closure, so no duration is logged.

🔧 Proposed fix
-	defer measure.ContextMeasure(ctx, slog.LevelInfo, "PostLocalGroups")
+	defer measure.ContextMeasure(ctx, slog.LevelInfo, "PostLocalGroups")()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
defer measure.ContextMeasure(ctx, slog.LevelInfo, "PostLocalGroups")
defer measure.ContextMeasure(ctx, slog.LevelInfo, "PostLocalGroups")()
🤖 Prompt for AI Agents
In `@packages/go/analysis/ad/local_groups.go` at line 182, The deferred call
currently defers the ContextMeasure function itself instead of its returned
closure, so change the defer in PostLocalGroups to invoke the returned function;
e.g. call measure.ContextMeasure(ctx, slog.LevelInfo, "PostLocalGroups") and
immediately call the result (defer measure.ContextMeasure(... )()) or assign the
returned func to a variable and defer that (e.g. end :=
measure.ContextMeasure(...); defer end()) so the timing closure actually runs.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants