Skip to content

Conversation

@stephanieslamb
Copy link
Contributor

@stephanieslamb stephanieslamb commented Dec 3, 2025

Description

This change is to implement ETAC changes to the explore page cypher query endpoint.

Motivation and Context

Resolves BED-6714

Why is this change required? What problem does it solve?
This change is needed for ETAC filtering of nodes and edges in the explore page.

How Has This Been Tested?

Added additional unit tests for new functionality changes. Updated existing unit tests. Screenshots below on how to test locally.

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):

admin user - hidden in response only on node property as false
admin - hidden only found as false property on node
single env(phantom) user - hidden node
single-env - hidden node
single env(phantom) user - no ghost corp
single-env - no ghost
single env(phantom) user - no wraith corp
single-env - no wraith
no envs user - all hidden nodes
user no-envs - all hidden
no envs user - no ghost corp
user no-envs  ghost
no envs user - no phantom corp
user no-envs phantom
no envs user - no wraith corp
user no-envs wraith

Types of changes

  • New feature (non-breaking change which adds functionality)

Checklist:

Summary by CodeRabbit

Release Notes

  • New Features
    • Graph query responses now support environment-based access control filtering, with nodes marked as hidden for users with restricted permissions.
    • Enhanced authentication error handling for graph queries.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 3, 2025

Walkthrough

The PR introduces ETAC (Environment-based Targeted Access Control) filtering into the CypherQuery flow. It adds a new ShouldFilterForETAC utility function, implements filterETACGraph filtering logic, updates the CypherQuery handler to operate on filtered graphs, adds a Hidden field to UnifiedNode, and expands tests with comprehensive ETAC scenarios and mock database wiring.

Changes

Cohort / File(s) Summary
ETAC Filtering Core Implementation
cmd/api/src/api/v2/cypherquery.go, cmd/api/src/api/v2/etac.go
Adds filterETACGraph method (+120/-5) to apply ETAC filtering to graph responses; introduces ShouldFilterForETAC utility function to determine if filtering is required based on feature flag and user permissions; modifies CypherQuery handler to retrieve user early, operate on filtered response instead of raw graph, and strip properties conditionally from filtered graph.
Data Model Enhancement
cmd/api/src/model/unified_graph.go
Adds Hidden bool field to UnifiedNode struct with JSON tagging (json:"hidden,omitempty") to support visibility control in ETAC filtering.
Test Expansion & Mocking
cmd/api/src/api/v2/cypherquery_test.go
Expands test coverage with ETAC-enabled/disabled scenarios; introduces mock database wiring (new exported DB field in Resources); adds user context injection via WithContext; adds appcfg import for feature flag mocking; tests now cover hidden nodes/edges, error paths, and targeted access control variants.
Configuration Alignment
cmd/api/src/config/config.go, cmd/api/src/config/default.go
Minor whitespace/alignment adjustments to DisableTimeoutLimit field without logic changes.
Debug Logging
cmd/api/src/queries/graph.go
Adds debug print statement in PrepareCypherQuery to output MultiPartQuery parts; no control-flow changes.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/Request
    participant Handler as CypherQuery Handler
    participant DB as Database
    participant ETAC as ETAC Check
    participant Filter as filterETACGraph
    participant Model as Response Model

    Client->>Handler: HTTP Request (with user context)
    Handler->>Handler: Extract user from auth context
    alt User extraction fails
        Handler->>Client: 500 Internal Server Error
    else User extracted successfully
        Handler->>DB: GetFlagByKey(ETAC feature flag)
        DB->>ETAC: Feature flag status
        ETAC->>Handler: Should filter? (bool)
        Handler->>Filter: filterETACGraph(ctx, graph, user)
        alt ETAC filtering enabled
            Filter->>Model: Apply filtering (mark hidden nodes/edges)
            Model->>Filter: Filtered graph
        else No filtering required
            Filter->>Handler: Original graph
        end
        Handler->>Handler: Process properties on filtered graph
        alt IncludeProperties is false
            Handler->>Model: Strip properties from filtered graph
        end
        Handler->>Client: JSON response with filtered/processed graph
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • mvlipka
  • bsheth711

Poem

🐰 Whisker-twitched through graphs we hop,
ETAC filters make secrets drop,
Hidden nodes in burrows stay,
While filtered data leads the way! 🐇

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature: ETAC-based filtering to hide nodes/edges based on user ETAC list, directly aligned with the changeset's primary functionality.
Description check ✅ Passed The description covers all required sections: changes explained (ETAC filtering), motivation (Resolves BED-6714), testing (unit tests added/updated with screenshots), type (new feature), and completed checklist items.

✏️ 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: 2

🧹 Nitpick comments (4)
cmd/api/src/api/v2/cypherquery.go (2)

180-180: Redundant condition check.

The check if !user.AllEnvironments at line 180 is redundant. This code path is only reachable when line 174's condition !etacFlag.Enabled || user.AllEnvironments is false, meaning user.AllEnvironments is already guaranteed to be false.

Apply this diff to remove the redundant check:

-		if !user.AllEnvironments {
-			for id, node := range graphResponse.Nodes {
+		for id, node := range graphResponse.Nodes {
 				include := false
 				for _, key := range environmentKeys {
 					if val, ok := node.Properties[key]; ok {
@@ -200,8 +199,7 @@
 					filteredNodes[id] = model.UnifiedNode{
 						...
 					}
 				}
-			}
 		}

192-204: Minor: Double space in hidden node label when Kind is empty.

When node.Kind is an empty string, the label becomes "** Hidden **" with a double space. Consider trimming or providing a fallback.

-					Label:         fmt.Sprintf("** Hidden %s **", node.Kind),
+					label := "** Hidden Node **"
+					if node.Kind != "" {
+						label = fmt.Sprintf("** Hidden %s **", node.Kind)
+					}
cmd/api/src/api/v2/cypherquery_test.go (2)

467-471: Misleading mock data: Hidden: true in mock input is ignored.

The Hidden: true on node "2" in the mock return is misleading. The filterETACGraph function determines hidden status based on environment property matching, not the input Hidden field. Consider setting Hidden: false in the mock to accurately represent what the database returns, letting the filter set it.

 						"2": {
 							Label:      "label2",
 							Properties: map[string]any{"domainsid": "value"},
-							Hidden:     true,
+							Hidden:     false,
 						},

551-583: Consider adding tests for filterETACGraph error paths.

The test suite doesn't cover error scenarios in filterETACGraph:

  1. When GetFlagByKey returns an error (line 172-173 in cypherquery.go)
  2. When user cannot be extracted from auth context (line 169-171)

These paths would return 500 errors and should be verified.

Would you like me to generate test cases for these error scenarios?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0c6baf and d0c20ba.

📒 Files selected for processing (6)
  • cmd/api/src/api/v2/cypherquery.go (3 hunks)
  • cmd/api/src/api/v2/cypherquery_test.go (10 hunks)
  • cmd/api/src/database/mocks/db.go (2 hunks)
  • cmd/api/src/model/unified_graph.go (1 hunks)
  • packages/javascript/bh-shared-ui/src/components/EntityInfo/EntityInfoPanel.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/hooks/usePZParams/usePZPathParams.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
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.
Learnt from: jvacca-specterops
Repo: SpecterOps/BloodHound PR: 1823
File: packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx:108-148
Timestamp: 2025-09-08T19:01:53.112Z
Learning: In BloodHound's CypherSearch component (packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx), the sharing state reset for sharedIds and isPublic after the two-step permissions update is handled elsewhere in the codebase, so additional state reset callbacks in the updateQueryPermissions function are not needed.
📚 Learning: 2025-08-28T19:26:03.304Z
Learnt from: benwaples
Repo: SpecterOps/BloodHound PR: 1829
File: packages/javascript/bh-shared-ui/src/views/ZoneManagement/ZoneAnalysisIcon.tsx:26-26
Timestamp: 2025-08-28T19:26:03.304Z
Learning: In packages/javascript/bh-shared-ui/src/hooks/, useZonePathParams is exported through the useZoneParams barrel (useZoneParams/index.ts exports it via wildcard from useZonePathParams.tsx), and usePrivilegeZoneAnalysis is exported through useConfiguration.ts. Both are available via the main hooks barrel import.

Applied to files:

  • packages/javascript/bh-shared-ui/src/hooks/usePZParams/usePZPathParams.tsx
  • packages/javascript/bh-shared-ui/src/components/EntityInfo/EntityInfoPanel.tsx
📚 Learning: 2025-08-25T20:12:35.629Z
Learnt from: mistahj67
Repo: SpecterOps/BloodHound PR: 1803
File: packages/javascript/bh-shared-ui/src/views/ZoneManagement/Summary/SummaryCard.tsx:24-24
Timestamp: 2025-08-25T20:12:35.629Z
Learning: The useHighestPrivilegeTagId hook is available through the hooks barrel export in packages/javascript/bh-shared-ui/src/hooks/index.ts via the wildcard export `export * from './useAssetGroupTags'`. The import `import { useHighestPrivilegeTagId } from '../../../hooks'` works correctly and doesn't cause build failures.

Applied to files:

  • packages/javascript/bh-shared-ui/src/components/EntityInfo/EntityInfoPanel.tsx
📚 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/api/v2/cypherquery_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/api/v2/cypherquery_test.go
  • cmd/api/src/database/mocks/db.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/api/v2/cypherquery_test.go
  • cmd/api/src/api/v2/cypherquery.go
📚 Learning: 2025-11-25T22:11:53.518Z
Learnt from: LawsonWillard
Repo: SpecterOps/BloodHound PR: 2107
File: cmd/api/src/database/graphschema.go:86-100
Timestamp: 2025-11-25T22:11:53.518Z
Learning: In cmd/api/src/database/graphschema.go, the CreateSchemaEdgeKind method intentionally does not use AuditableTransaction or audit logging because it would create too much noise in the audit log, unlike CreateGraphSchemaExtension which does use auditing.

Applied to files:

  • cmd/api/src/database/mocks/db.go
🧬 Code graph analysis (3)
cmd/api/src/api/v2/cypherquery_test.go (4)
cmd/api/src/model/appcfg/flag.go (2)
  • FeatureETAC (43-43)
  • FeatureFlag (50-70)
cmd/api/src/queries/graph.go (1)
  • DefaultQueryFitnessLowerBoundExplore (67-67)
cmd/api/src/model/unified_graph.go (3)
  • UnifiedGraph (37-40)
  • UnifiedEdge (64-71)
  • UnifiedNode (51-61)
cmd/api/src/api/v2/cypherquery.go (1)
  • CypherQueryPayload (41-44)
cmd/api/src/api/v2/cypherquery.go (4)
cmd/api/src/auth/model.go (2)
  • Context (174-178)
  • GetUserFromAuthCtx (184-191)
cmd/api/src/model/unified_graph.go (3)
  • UnifiedGraph (37-40)
  • UnifiedNode (51-61)
  • UnifiedEdge (64-71)
cmd/api/src/model/appcfg/flag.go (1)
  • FeatureETAC (43-43)
cmd/api/src/api/v2/etac.go (1)
  • ExtractEnvironmentIDsFromUser (63-71)
cmd/api/src/database/mocks/db.go (1)
cmd/api/src/model/graphschema.go (4)
  • SchemaEdgeKind (75-81)
  • SchemaEdgeKind (83-85)
  • SchemaNodeKind (43-53)
  • SchemaNodeKind (56-58)
⏰ 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: build-ui
  • GitHub Check: run-analysis
🔇 Additional comments (7)
packages/javascript/bh-shared-ui/src/hooks/usePZParams/usePZPathParams.tsx (1)

65-65: LGTM!

Formatting-only change adding trailing newline.

packages/javascript/bh-shared-ui/src/components/EntityInfo/EntityInfoPanel.tsx (1)

17-17: LGTM!

Import placement cleanup. No behavioral changes.

cmd/api/src/model/unified_graph.go (1)

60-60: LGTM!

The Hidden field appropriately defaults to false and is explicitly set to true during ETAC filtering in filterETACGraph. The lack of omitempty ensures consistent JSON serialization across all nodes.

cmd/api/src/api/v2/cypherquery.go (1)

166-228: ETAC filtering logic is well-structured.

The filtering correctly:

  • Bypasses filtering when ETAC is disabled or user has AllEnvironments access
  • Checks both domainsid and tenantid properties for environment matching
  • Replaces inaccessible nodes with hidden placeholders preserving graph structure
  • Hides edges connected to hidden nodes

Based on learnings, the empty access list behavior (filtering all nodes) is intentional security behavior.

cmd/api/src/api/v2/cypherquery_test.go (1)

552-583: Test structure follows best practices.

Each subtest correctly creates its own mock controller, enabling safe parallel execution without shared state issues.

cmd/api/src/database/mocks/db.go (2)

528-556: CreateSchemaEdgeKind/CreateSchemaNodeKind mocks look consistent with Database interface patterns

These create-method mocks (and their recorders) follow the same Helper/Call/type-assert pattern as the rest of the file, and their parameter lists line up with the SchemaEdgeKind/SchemaNodeKind fields and other CreateGraphSchema* methods. Given this file is mockgen‑generated, this block looks correct and requires no manual changes.


1878-1906: GetSchemaEdgeKindById/GetSchemaNodeKindByID mocks correctly mirror other getters

The new getter mocks and their recorders match the established pattern for Get* methods in this file (ctx + ID in, model type + error out). Argument ordering and type assertions look correct and aligned with the underlying model types.

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

🧹 Nitpick comments (2)
cmd/api/src/api/v2/cypherquery.go (2)

106-114: Improve observability and messaging around ETAC filtering errors.

Right now, any error from filterETACGraph results in a 500 with the hard-coded message "error", and the underlying failure (e.g., flag lookup, unexpected state) isn’t logged here:

filteredResponse, err := s.filterETACGraph(graphResponse, request)
if err != nil {
    api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, "error", request), response)
    return
}

Consider either:

  • Logging the actual error (e.g., slog.ErrorContext(request.Context(), "ETAC graph filter failed", "err", err)), and/or
  • Returning a more specific message while still being safe for clients.

This will make ETAC-related failures far easier to debug in production without changing behavior for callers.


167-229: ETAC graph filtering semantics look sound; consider minor cleanups and confirm empty-access behavior.

The core logic—building an access list from the user, hiding non-matching nodes with placeholders, and converting edges touching hidden nodes into hidden-edge placeholders—looks correct and matches the ETAC feature goals.

A few focused suggestions:

  • Redundant !user.AllEnvironments check. Inside the else of !etacFlag.Enabled || user.AllEnvironments, the inner if !user.AllEnvironments is always true and only adds an extra nesting level:

    } else {
        accessList := ExtractEnvironmentIDsFromUser(&user)
        environmentKeys := []string{"domainsid", "tenantid"}
  • if !user.AllEnvironments {
    
  •     for id, node := range graphResponse.Nodes {
    
  • for id, node := range graphResponse.Nodes {
        ...
    
  •     }
    
  • }
    

    }

    
    Dropping the inner condition simplifies the flow without changing behavior.
    
    
  • Membership check performance. slices.Contains(accessList, envStr) is O(len(accessList)) per environment-bearing node. If accessList or the graph is large, consider precomputing a map[string]struct{} set:

    envSet := make(map[string]struct{}, len(accessList))
    for _, id := range accessList {
        envSet[id] = struct{}{}
    }
    
    ...
    
    if envStr, ok := val.(string); ok {
        if _, ok := envSet[envStr]; ok {
            include = true
        }
    }

    This keeps the rest of the logic intact while making filtering scale better.

  • Empty access list semantics. When ETAC is enabled and the user has no environment assignments, accessList will be empty, so all nodes become hidden placeholders and all edges become hidden edges rather than the graph being fully filtered out. In other ETAC code paths (e.g., GetAssetGroupComboNode), an empty allowed list is intentionally used to filter out all nodes. Please double-check that this differing behavior is deliberate for Cypher queries (showing a “fully hidden” graph vs. “no results”), for consistency of user experience and expectations. Based on learnings, this difference is worth confirming with product/UX.

Overall the function is correct; the above are targeted cleanups and a semantics check.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d0c20ba and 57c4cef.

📒 Files selected for processing (1)
  • cmd/api/src/api/v2/cypherquery.go (3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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.
Learnt from: jvacca-specterops
Repo: SpecterOps/BloodHound PR: 1823
File: packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx:108-148
Timestamp: 2025-09-08T19:01:53.112Z
Learning: In BloodHound's CypherSearch component (packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx), the sharing state reset for sharedIds and isPublic after the two-step permissions update is handled elsewhere in the codebase, so additional state reset callbacks in the updateQueryPermissions function are not needed.
📚 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/api/v2/cypherquery.go
⏰ 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: build-ui
  • GitHub Check: run-analysis
🔇 Additional comments (1)
cmd/api/src/api/v2/cypherquery.go (1)

19-35: Imports for ETAC filtering look correct.

Adding time and appcfg is consistent with the new ETAC filtering logic and hidden-node/edge timestamps; no issues here.

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: 0

🧹 Nitpick comments (1)
cmd/api/src/api/v2/cypherquery.go (1)

26-26: Strengthen ETAC graph filtering defensiveness and tighten a few implementation details

The overall ETAC masking behavior (hidden nodes with Kind: "HIDDEN" / Hidden: true and corresponding hidden edges) is aligned with the rest of the ETAC design and the fail-closed approach from the query layer. A couple of non-blocking improvements could make this more robust and efficient:

  1. Defensive handling for edges whose endpoints are missing from filteredNodes

    The edge pass currently assumes every edge.Source / edge.Target exists in filteredNodes:

    for _, edge := range graphResponse.Edges {
        if filteredNodes[edge.Target].Hidden || filteredNodes[edge.Source].Hidden {
            // ...
        } else {
            filteredEdges = append(filteredEdges, edge)
        }
    }

    If the DB ever returns an edge referencing a node ID that’s not present in graphResponse.Nodes (or future refactors change the node filtering semantics), the zero-value lookup will treat that endpoint as not hidden, and you’ll emit an edge whose endpoint doesn’t exist in filteredResponse.Nodes.

    A more defensive pattern would be to treat missing endpoints as hidden (fail-closed) and avoid relying on implicit zero values:

  • for _, edge := range graphResponse.Edges {
  •   if filteredNodes[edge.Target].Hidden || filteredNodes[edge.Source].Hidden {
    
  • for _, edge := range graphResponse.Edges {
  •   src, okSrc := filteredNodes[edge.Source]
    
  •   tgt, okTgt := filteredNodes[edge.Target]
    
  •   if !okSrc || !okTgt || src.Hidden || tgt.Hidden {
      	filteredEdges = append(filteredEdges, model.UnifiedEdge{
      		Source:     edge.Source,
      		Target:     edge.Target,
      		Label:      "** Hidden Edge **",
      		Kind:       "HIDDEN",
      		LastSeen:   time.Time{},
      		Properties: nil,
      	})
      } else {
      	filteredEdges = append(filteredEdges, edge)
      }
    
    }
    
    This keeps the semantics fail-closed even if graph invariants are ever violated, which is desirable for ETAC. Based on learnings, this matches the “empty allowed list → no usable data” behavior used elsewhere in ETAC paths.
    
    
  1. Micro-optimizations for node filtering

    Not required, but easy wins given this is on the hot path for explore queries:

    • Pre-size the node map to avoid rehashing:

  • filteredNodes := make(map[string]model.UnifiedNode)
  • filteredNodes := make(map[string]model.UnifiedNode, len(graphResponse.Nodes))
    
    
  • If accessList can grow beyond a handful of entries, consider building a set once instead of calling slices.Contains inside the inner loop:

  •   accessList := ExtractEnvironmentIDsFromUser(&user)
    
  •   accessList := ExtractEnvironmentIDsFromUser(&user)
    
  •   accessSet := make(map[string]struct{}, len(accessList))
    
  •   for _, id := range accessList {
    
  •   	accessSet[id] = struct{}{}
    
  •   }
    

@@

  •   			if val, ok := node.Properties[key]; ok {
    
  •   				if envStr, ok := val.(string); ok && slices.Contains(accessList, envStr) {
    
  •   					include = true
    
  •   				}
    
  •   			}
    
  •   			if val, ok := node.Properties[key]; ok {
    
  •   				if envStr, ok := val.(string); ok {
    
  •   					if _, ok := accessSet[envStr]; ok {
    
  •   						include = true
    
  •   					}
    
  •   				}
    
  •   			}
    
    
    
  1. Minor readability nit

    Inside the else branch guarded by !etacFlag.Enabled || user.AllEnvironments, the inner if !user.AllEnvironments around the node loop is redundant (that condition is already guaranteed by the enclosing else). Dropping that extra if would slightly simplify the control flow without changing behavior.

None of the above are blockers; they’re primarily about making the ETAC filter more defensive and a bit more efficient.

Can you confirm that RawCypherQuery always returns a graph where every edge’s Source and Target appears in the Nodes map? If there are known exceptions, I’d strongly recommend adopting the defensive edge handling in point (1).

Also applies to: 32-32, 173-235

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57c4cef and d7d09d5.

📒 Files selected for processing (1)
  • cmd/api/src/api/v2/cypherquery.go (3 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
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.
Learnt from: jvacca-specterops
Repo: SpecterOps/BloodHound PR: 1823
File: packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx:108-148
Timestamp: 2025-09-08T19:01:53.112Z
Learning: In BloodHound's CypherSearch component (packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx), the sharing state reset for sharedIds and isPublic after the two-step permissions update is handled elsewhere in the codebase, so additional state reset callbacks in the updateQueryPermissions function are not needed.
📚 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/api/v2/cypherquery.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:

  • cmd/api/src/api/v2/cypherquery.go
📚 Learning: 2025-09-08T19:01:53.112Z
Learnt from: jvacca-specterops
Repo: SpecterOps/BloodHound PR: 1823
File: packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx:108-148
Timestamp: 2025-09-08T19:01:53.112Z
Learning: In BloodHound's CypherSearch component (packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx), the sharing state reset for sharedIds and isPublic after the two-step permissions update is handled elsewhere in the codebase, so additional state reset callbacks in the updateQueryPermissions function are not needed.

Applied to files:

  • cmd/api/src/api/v2/cypherquery.go
⏰ 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: build-ui
  • GitHub Check: run-tests
🔇 Additional comments (1)
cmd/api/src/api/v2/cypherquery.go (1)

94-133: CypherQuery ETAC integration and property-stripping behavior look correct

The updated flow makes sense:

  • For both mutation and non-mutation queries you always call the DB with includeProperties=true, which is required so ETAC can inspect node properties (env IDs).
  • You then run the unified graph through filterETACGraph and base the 404 check on the filtered result, so users with constrained/no environments see masked graphs instead of a misleading 404.
  • When payload.IncludeProperties is false, you now clear both node and edge properties before returning, which preserves the external API contract even though the DB always returned properties.

Given the previous fixes to the missing return and the node-map key usage, the control flow and response-shaping here look solid to me. No changes requested.

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: 0

🧹 Nitpick comments (3)
cmd/api/src/api/v2/cypherquery.go (2)

187-221: Redundant condition: !user.AllEnvironments is always true here.

The check on line 187 is unnecessary. At this point in the code, we're in the else branch of line 181 where !etacFlag.Enabled || user.AllEnvironments is false, meaning ETAC is enabled AND user.AllEnvironments is false.

Apply this diff to simplify:

 	} else {
 		accessList := ExtractEnvironmentIDsFromUser(&user)
 		environmentKeys := []string{"domainsid", "tenantid"}
 
-		if !user.AllEnvironments {
-			for id, node := range graphResponse.Nodes {
-				include := false
-				for _, key := range environmentKeys {
-					if val, ok := node.Properties[key]; ok {
-						if envStr, ok := val.(string); ok && slices.Contains(accessList, envStr) {
-							include = true
-						}
+		for id, node := range graphResponse.Nodes {
+			include := false
+			for _, key := range environmentKeys {
+				if val, ok := node.Properties[key]; ok {
+					if envStr, ok := val.(string); ok && slices.Contains(accessList, envStr) {
+						include = true
 					}
 				}
-				if include {
-					filteredNodes[id] = node
-				} else {
-					var kind string
-					if len(node.Kinds) > 0 && node.Kinds[0] != "" {
-						kind = node.Kinds[0]
-					} else {
-						kind = "Unknown" // unknown if no kind
-					}
-
-					label := fmt.Sprintf("** Hidden %s Object **", kind)
-					filteredNodes[id] = model.UnifiedNode{
-						Label:         label,
-						Kind:          "HIDDEN",
-						Kinds:         []string{},
-						ObjectId:      "HIDDEN",
-						IsTierZero:    false,
-						IsOwnedObject: false,
-						LastSeen:      time.Time{},
-						Properties:    nil,
-						Hidden:        true,
-					}
+			}
+			if include {
+				filteredNodes[id] = node
+			} else {
+				var kind string
+				if len(node.Kinds) > 0 && node.Kinds[0] != "" {
+					kind = node.Kinds[0]
+				} else {
+					kind = "Unknown"
+				}
+
+				label := fmt.Sprintf("** Hidden %s Object **", kind)
+				filteredNodes[id] = model.UnifiedNode{
+					Label:         label,
+					Kind:          "HIDDEN",
+					Kinds:         []string{},
+					ObjectId:      "HIDDEN",
+					IsTierZero:    false,
+					IsOwnedObject: false,
+					LastSeen:      time.Time{},
+					Properties:    nil,
+					Hidden:        true,
 				}
 			}
 		}

225-238: Consider defensive check for edges referencing non-existent nodes.

If edge.Source or edge.Target references a node ID not present in graphResponse.Nodes (malformed data), accessing filteredNodes[edge.Source] returns a zero-value struct with Hidden = false, causing the edge to be kept as visible rather than hidden.

If you want defensive handling:

 		for _, edge := range graphResponse.Edges {
-			if filteredNodes[edge.Target].Hidden || filteredNodes[edge.Source].Hidden {
+			sourceNode, sourceExists := filteredNodes[edge.Source]
+			targetNode, targetExists := filteredNodes[edge.Target]
+			if !sourceExists || !targetExists || sourceNode.Hidden || targetNode.Hidden {
 				filteredEdges = append(filteredEdges, model.UnifiedEdge{

This would also hide edges that reference nodes not in the original graph response. However, if you trust data integrity from the database, the current implementation is acceptable.

cmd/api/src/api/v2/cypherquery_test.go (1)

540-555: Edge test data references non-existent node "source".

In the mock graph response (lines 528-546), nodes "1" and "2" are defined, but the edge references Source: "source" which isn't a node in the graph. The filtering logic will look up filteredNodes["source"] and get a zero-value with Hidden = false.

This means the edge visibility check filteredNodes[edge.Source].Hidden || filteredNodes[edge.Target].Hidden evaluates to false || <node 1 hidden state>. Since node "1" becomes hidden, the edge correctly becomes hidden, but for the wrong reason.

Consider adding "source" as a node in the mock to properly test the filtering logic, or acknowledge this is testing the edge case of orphan edges.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7d09d5 and f28004d.

📒 Files selected for processing (2)
  • cmd/api/src/api/v2/cypherquery.go (3 hunks)
  • cmd/api/src/api/v2/cypherquery_test.go (10 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
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.
📚 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/api/v2/cypherquery.go
  • cmd/api/src/api/v2/cypherquery_test.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:

  • cmd/api/src/api/v2/cypherquery.go
📚 Learning: 2025-09-08T19:01:53.112Z
Learnt from: jvacca-specterops
Repo: SpecterOps/BloodHound PR: 1823
File: packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx:108-148
Timestamp: 2025-09-08T19:01:53.112Z
Learning: In BloodHound's CypherSearch component (packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx), the sharing state reset for sharedIds and isPublic after the two-step permissions update is handled elsewhere in the codebase, so additional state reset callbacks in the updateQueryPermissions function are not needed.

Applied to files:

  • cmd/api/src/api/v2/cypherquery.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/api/v2/cypherquery_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/api/v2/cypherquery_test.go
🧬 Code graph analysis (1)
cmd/api/src/api/v2/cypherquery_test.go (4)
cmd/api/src/model/appcfg/flag.go (2)
  • FeatureETAC (43-43)
  • FeatureFlag (50-70)
cmd/api/src/queries/graph.go (1)
  • DefaultQueryFitnessLowerBoundExplore (67-67)
cmd/api/src/model/unified_graph.go (3)
  • UnifiedGraph (37-40)
  • UnifiedEdge (64-71)
  • UnifiedNode (51-61)
cmd/api/src/api/v2/cypherquery.go (1)
  • CypherQueryPayload (41-44)
🔇 Additional comments (6)
cmd/api/src/api/v2/cypherquery.go (3)

26-26: LGTM on new imports.

The time import is used for time.Time{} in hidden node/edge creation, and appcfg is used for feature flag lookup. Both are necessary.

Also applies to: 32-32


94-110: LGTM on ETAC filtering integration.

The approach of always fetching properties for ETAC filtering and stripping them later when IncludeProperties is false is correct. The error handling now properly returns after writing the error response.


116-132: LGTM on property stripping.

The iteration now correctly uses the map key id instead of node.ObjectId, and edge properties are properly stripped when IncludeProperties is false.

cmd/api/src/api/v2/cypherquery_test.go (3)

76-94: LGTM on test request construction with user context.

The test correctly creates a user, sets up the user context, and attaches it to the request using WithContext. This pattern is consistent across the test cases.


570-574: LGTM on test resource wiring.

The DB field is correctly wired to the mock database, enabling the ETAC feature flag lookup in tests.


351-355: The review comment is incorrect; the struct already has omitempty tags.

The UnifiedGraphWPropertyKeys struct defines EdgeKeys as []string json:"edge_keys,omitempty" (not without omitempty as claimed). With the omitempty tag, JSON marshaling will omit the field when the slice is empty, which is exactly why edge_keys is absent from the expected response bodies in the test cases.

The test expectations at lines 353, 412, 487, and 552 are correct—edge_keys should be omitted when edges have no properties, and the same applies to node_keys when nodes have no properties.

Likely an incorrect or invalid review comment.

@stephanieslamb stephanieslamb marked this pull request as draft December 9, 2025 19:34
@stephanieslamb stephanieslamb marked this pull request as ready for review December 16, 2025 16:19
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: 0

🧹 Nitpick comments (1)
cmd/api/src/api/v2/cypherquery.go (1)

174-176: Include the error value in the log message.

The error is propagated correctly but the log message doesn't include the actual error, making debugging harder.

 	if err != nil {
-		slog.Error("Unable to check ETAC filtering")
+		slog.Error("Unable to check ETAC filtering", "error", err)
 		return model.UnifiedGraph{}, err
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f28004d and c999093.

📒 Files selected for processing (2)
  • cmd/api/src/api/v2/cypherquery.go (3 hunks)
  • cmd/api/src/api/v2/etac.go (2 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
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.
📚 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/api/v2/etac.go
  • cmd/api/src/api/v2/cypherquery.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:

  • cmd/api/src/api/v2/cypherquery.go
📚 Learning: 2025-09-08T19:01:53.112Z
Learnt from: jvacca-specterops
Repo: SpecterOps/BloodHound PR: 1823
File: packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx:108-148
Timestamp: 2025-09-08T19:01:53.112Z
Learning: In BloodHound's CypherSearch component (packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx), the sharing state reset for sharedIds and isPublic after the two-step permissions update is handled elsewhere in the codebase, so additional state reset callbacks in the updateQueryPermissions function are not needed.

Applied to files:

  • cmd/api/src/api/v2/cypherquery.go
🔇 Additional comments (6)
cmd/api/src/api/v2/etac.go (1)

78-94: LGTM! Clean ETAC filtering decision logic.

The function correctly handles:

  • User extraction from auth context with proper error handling
  • Feature flag check with error propagation
  • Short-circuit for disabled ETAC or admin users with AllEnvironments

Based on learnings, the empty accessList when user.EnvironmentTargetedAccessControl is empty is intentional for security—users with no environment assignments correctly see no results.

cmd/api/src/api/v2/cypherquery.go (5)

93-98: Good approach: Always fetch properties for ETAC filtering.

The comments clearly explain why includeProperties is always true at the database level—ETAC filtering needs access to domainsid/tenantid properties. Properties are correctly stripped at response-level when payload.IncludeProperties is false.


105-131: LGTM! Error handling and property stripping are correctly implemented.

Previous review issues have been addressed:

  • Return statement after error response (line 108-109)
  • Using map id key instead of node.ObjectId (line 117)
  • Stripping both node and edge properties (lines 117-125)

199-221: Clean implementation of hidden node placeholders.

The hidden node structure appropriately:

  • Preserves the map key (original node ID) for graph traversal
  • Uses descriptive label with the original kind for user context
  • Sets the Hidden flag for downstream filtering
  • Clears sensitive data (properties, ObjectId, IsOwnedObject, IsTierZero)

186-197: The property key casing in the code is correct. BloodHound consistently uses lowercase property keys: "domainsid" and uses similar lowercase naming patterns across its collectors. While Neo4j property keys are case-sensitive, the code correctly matches the actual BloodHound data model where these properties are stored in lowercase. This is not a correctness issue.

Likely an incorrect or invalid review comment.


227-240: No action needed. The edge case described cannot occur with the current code flow because Neo4j maintains referential integrity—all relationships returned from Cypher queries include their source and target nodes in the result set. The AddPathSet() method populates the graph with nodes and edges from the same paths, ensuring all edge references exist in filteredNodes.

Likely an incorrect or invalid review comment.

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: 0

🧹 Nitpick comments (3)
cmd/api/src/api/v2/cypherquery.go (3)

108-111: Provide a more descriptive error message.

The generic "error" message provides no context for debugging. Consider including what operation failed.

 	if err != nil {
-		api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, "error", request), response)
+		api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, "error filtering graph for access control", request), response)
 		return
 	}

180-183: Include the error in the log message for debugging.

The error value is discarded in the log, making troubleshooting difficult.

 	if err != nil {
-		slog.Error("Unable to check ETAC filtering")
+		slog.Error("Unable to check ETAC filtering", "error", err)
 		return model.UnifiedGraph{}, err
 	}

193-205: Consider extracting environment keys as package-level constants.

Hardcoding "domainsid" and "tenantid" inline may make future updates error-prone if these keys are used elsewhere.

+const (
+	envKeyDomainSID = "domainsid"
+	envKeyTenantID  = "tenantid"
+)
+
 func (s Resources) filterETACGraph(...) {
     ...
-	environmentKeys := []string{"domainsid", "tenantid"}
+	environmentKeys := []string{envKeyDomainSID, envKeyTenantID}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c999093 and 6e9584c.

📒 Files selected for processing (2)
  • cmd/api/src/api/v2/cypherquery.go (3 hunks)
  • cmd/api/src/api/v2/etac.go (2 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
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.
📚 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/api/v2/cypherquery.go
  • cmd/api/src/api/v2/etac.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:

  • cmd/api/src/api/v2/cypherquery.go
📚 Learning: 2025-09-08T19:01:53.112Z
Learnt from: jvacca-specterops
Repo: SpecterOps/BloodHound PR: 1823
File: packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx:108-148
Timestamp: 2025-09-08T19:01:53.112Z
Learning: In BloodHound's CypherSearch component (packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx), the sharing state reset for sharedIds and isPublic after the two-step permissions update is handled elsewhere in the codebase, so additional state reset callbacks in the updateQueryPermissions function are not needed.

Applied to files:

  • cmd/api/src/api/v2/cypherquery.go
🧬 Code graph analysis (2)
cmd/api/src/api/v2/cypherquery.go (3)
cmd/api/src/api/error.go (1)
  • BuildErrorResponse (134-145)
cmd/api/src/model/unified_graph.go (3)
  • UnifiedGraph (37-40)
  • UnifiedNode (51-61)
  • UnifiedEdge (64-71)
cmd/api/src/api/v2/etac.go (1)
  • ShouldFilterForETAC (81-97)
cmd/api/src/api/v2/etac.go (3)
cmd/api/src/auth/model.go (1)
  • GetUserFromAuthCtx (184-191)
cmd/api/src/ctx/ctx.go (1)
  • FromRequest (70-72)
cmd/api/src/model/appcfg/flag.go (1)
  • FeatureETAC (43-43)
⏰ 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: build-ui
  • GitHub Check: run-tests
🔇 Additional comments (4)
cmd/api/src/api/v2/etac.go (1)

78-96: LGTM! Clear and secure ETAC filtering determination.

The function correctly implements the security model:

  • Returns error for unknown users (line 84)
  • Propagates DB errors (line 89)
  • Bypasses filtering when ETAC is disabled or user has full access (line 92-93)
  • Returns environment list for restricted users

Based on learnings, the behavior where an empty accessList (when user has no environments) causes all nodes to be filtered out is the correct security behavior.

cmd/api/src/api/v2/cypherquery.go (3)

117-131: LGTM!

Property stripping correctly handles both nodes and edges using the proper map key and slice index for in-place modification.


237-253: LGTM!

Edge filtering correctly marks edges as hidden when connected to hidden nodes. The use of Kind: "HIDDEN" and a descriptive label provides a consistent marker for client-side filtering, matching the hidden node pattern.


207-231: LGTM!

Hidden node placeholder correctly:

  • Extracts the node kind for informative labeling
  • Sanitizes all identifying information (ObjectId, Properties, LastSeen)
  • Sets Hidden: true for explicit client-side detection

@stephanieslamb stephanieslamb self-assigned this Dec 16, 2025
@stephanieslamb stephanieslamb added enhancement New feature or request api A pull request containing changes affecting the API code. labels Dec 16, 2025
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

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e9584c and be159ce.

📒 Files selected for processing (2)
  • cmd/api/src/api/v2/cypherquery_test.go (10 hunks)
  • cmd/api/src/model/unified_graph.go (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
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.
📚 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/api/v2/cypherquery_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/api/v2/cypherquery_test.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/api/v2/cypherquery_test.go
🧬 Code graph analysis (1)
cmd/api/src/api/v2/cypherquery_test.go (3)
cmd/api/src/model/appcfg/flag.go (2)
  • FeatureETAC (43-43)
  • FeatureFlag (50-70)
cmd/api/src/queries/graph.go (1)
  • DefaultQueryFitnessLowerBoundExplore (67-67)
cmd/api/src/api/v2/cypherquery.go (1)
  • CypherQueryPayload (40-43)
⏰ 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 (7)
cmd/api/src/model/unified_graph.go (1)

60-60: LGTM! Clean addition of ETAC visibility metadata.

The Hidden field with omitempty tag is well-designed: it maintains backward compatibility (omitted when false), supports the ETAC filtering feature, and correctly defaults to false for nodes created via FromDAWGSNode. The filtering logic that sets this field to true for inaccessible nodes is handled elsewhere in the request flow.

cmd/api/src/api/v2/cypherquery_test.go (6)

39-39: LGTM! Import required for ETAC feature flag testing.

The appcfg package import is necessary for accessing appcfg.FeatureETAC in feature flag mocks throughout the updated tests.


76-95: LGTM! User context setup enables ETAC-aware testing.

Adding user context with AllEnvironments: true to existing tests ensures the handler can access user permissions for ETAC filtering. This is necessary infrastructure for ETAC support without changing the expected behavior of these tests (since users with all environments access should see unfiltered results).

Also applies to: 263-282, 309-327


115-116: LGTM! Feature flag mocking maintains existing test semantics.

Mocking the ETAC feature flag as disabled in existing tests ensures they continue to verify the original (non-filtered) behavior while supporting the new handler logic that checks the flag.

Also applies to: 289-290, 348-349


357-415: LGTM! Test verifies ETAC bypass for users with all environments access.

This test correctly validates that when ETAC is enabled but a user has AllEnvironments: true, no filtering occurs and the response remains identical to the non-ETAC case. This is the expected security behavior for privileged users.


417-490: LGTM! Test validates selective ETAC filtering and placeholder rendering.

This test comprehensively verifies the ETAC filtering behavior for users with limited environment access:

  • Accessible nodes (matching user's ETAC list) are rendered with full details
  • Inaccessible nodes are marked with hidden:true and replaced with placeholder values (kind: "HIDDEN", label: "** Hidden kinds Object **", etc.)
  • Edges connected to hidden nodes are also marked as hidden
  • Graph structure is preserved while sensitive details are obscured

This is the correct security and UX behavior.


570-574: LGTM! Proper database wiring for ETAC feature flag checks.

Setting the DB field in the Resources struct enables the handler to call GetFlagByKey for ETAC feature detection. The Authorizer initialization with the mock database is also necessary for permission checks. This is the correct test setup pattern.

Comment on lines +492 to 555
name: "Success: ETAC enabled, user has no access, hidden graph - 200",
buildRequest: func() *http.Request {
payload := &v2.CypherQueryPayload{
Query: "query",
IncludeProperties: true,
}
jsonPayload, err := json.Marshal(payload)
if err != nil {
t.Fatalf("error occurred while marshaling payload necessary for test: %v", err)
}
user := model.User{
AllEnvironments: false,
}
userCtx := setupUserCtx(user)

req := &http.Request{
URL: &url.URL{
Path: "/api/v2/graphs/cypher",
},
Body: io.NopCloser(bytes.NewReader(jsonPayload)),
Header: http.Header{
headers.ContentType.String(): []string{
"application/json",
},
},
Method: http.MethodPost,
}
req = req.WithContext(userCtx)
return req
},
setupMocks: func(t *testing.T, mocks *mock) {
t.Helper()
mocks.mockGraphQuery.EXPECT().PrepareCypherQuery("query", int64(queries.DefaultQueryFitnessLowerBoundExplore)).Return(queries.PreparedQuery{
HasMutation: false,
}, nil)
mocks.mockGraphQuery.EXPECT().RawCypherQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(model.UnifiedGraph{
Nodes: map[string]model.UnifiedNode{
"1": {
Label: "label",
Properties: map[string]any{"domainsid": "testenv"},
Kinds: []string{"kinds"},
},
"2": {
Label: "label2",
Properties: map[string]any{"domainsid": "value"},
Kinds: []string{"kinds"},
},
},
Edges: []model.UnifiedEdge{
{
Source: "source",
Target: "1",
},
},
}, nil)
mocks.mockDatabase.EXPECT().GetFlagByKey(gomock.Any(), appcfg.FeatureETAC).
Return(appcfg.FeatureFlag{Enabled: true}, nil)
},
expected: expected{
responseCode: http.StatusOK,
responseBody: `{"data":{"node_keys": ["key"], "nodes":{"1":{"label":"label","properties": {"key": "value"},"kind":"","objectId":"","kinds":null, "isTierZero":false,"isOwnedObject":false,"lastSeen":"0001-01-01T00:00:00Z"}},"edges":[{"source":"source","target":"","label":"","kind":"","lastSeen":"0001-01-01T00:00:00Z"}]}}`,
responseBody: `{"data":{"nodes":{"1":{"hidden":true,"isOwnedObject":false,"isTierZero":false,"kind":"HIDDEN","kinds":[],"label":"** Hidden kinds Object **","lastSeen":"0001-01-01T00:00:00Z","objectId":"HIDDEN"},"2":{"hidden":true,"isOwnedObject":false,"isTierZero":false,"kind":"HIDDEN","kinds":[],"label":"** Hidden kinds Object **","lastSeen":"0001-01-01T00:00:00Z","objectId":"HIDDEN"}},"edges":[{"source":"source","target":"1","label":"** Hidden Edge **","kind":"HIDDEN","lastSeen":"0001-01-01T00:00:00Z"}]}}`,
responseHeader: http.Header{"Content-Type": []string{"application/json"}},
},
},
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find where response structure is built, particularly for hidden graphs
rg -n "node_keys|edge_keys" cmd/api/src/api/v2/cypherquery_test.go | head -20

Repository: SpecterOps/BloodHound

Length of output: 2192


🏁 Script executed:

#!/bin/bash
# Search for response building logic in the handler
fd -e go -path "*/api/v2/*" ! -path "*/test.go" | xargs rg -l "CypherQuery" | head -5

Repository: SpecterOps/BloodHound

Length of output: 626


🏁 Script executed:

#!/bin/bash
# Extract all test case names to understand the test scenarios
rg -n "name:.*ETAC" cmd/api/src/api/v2/cypherquery_test.go | head -10

Repository: SpecterOps/BloodHound

Length of output: 272


🏁 Script executed:

#!/bin/bash
# Find the handler that processes cypher queries
fd -type f -name "*.go" cmd/api/src/api/v2/ ! -name "*test*" | xargs rg -l "CypherQuery" | head -5

Repository: SpecterOps/BloodHound

Length of output: 623


🏁 Script executed:

#!/bin/bash
# Search for the response building logic for cypher queries
rg -n "\"data\"" cmd/api/src/api/v2/*.go | grep -i cypher | head -10

Repository: SpecterOps/BloodHound

Length of output: 2912


🏁 Script executed:

#!/bin/bash
# Look for where node_keys and edge_keys are populated
rg -n "node_keys|edge_keys" cmd/api/src/ --type go | grep -v test | head -20

Repository: SpecterOps/BloodHound

Length of output: 264


🏁 Script executed:

#!/bin/bash
# Find the cypher query handler function
rg -n "func.*CypherQuery" cmd/api/src/api/v2/*.go | grep -v test

Repository: SpecterOps/BloodHound

Length of output: 286


🏁 Script executed:

#!/bin/bash
# Look for where NodeKeys are populated in the response building
rg -n "NodeKeys\s*=" cmd/api/src -A 3 -B 3 | head -40

Repository: SpecterOps/BloodHound

Length of output: 47


🏁 Script executed:

#!/bin/bash
# Check the full handler implementation for the cypher query endpoint
rg -n "RawCypherQuery\|PostCypherQuery" cmd/api/src/api/v2/*.go | grep -v test

Repository: SpecterOps/BloodHound

Length of output: 47


🏁 Script executed:

#!/bin/bash
# Get the CypherQuery handler function implementation
sed -n '75,300p' cmd/api/src/api/v2/cypherquery.go

Repository: SpecterOps/BloodHound

Length of output: 6365


The response is missing node_keys and edge_keys fields—this indicates a bug in the ETAC filtering logic.

When ETAC filtering is applied in filterETACGraph, the new UnifiedGraph response object never gets NodeKeys and EdgeKeys populated from the original query result. Since these fields have omitempty tags and are nil, they're excluded from the JSON response. The BloodHound API specification shows these fields as part of the standard response structure, and all other test cases (lines 353, 412, 487) include at least node_keys. The filtered response should preserve or regenerate these field keys. This is not a test oversight but a code issue that needs correction in the filterETACGraph function to properly copy NodeKeys and EdgeKeys from the original response.

🤖 Prompt for AI Agents
In cmd/api/src/api/v2/cypherquery_test.go around lines 492-555, the
ETAC-filtered response is missing node_keys and edge_keys because
filterETACGraph builds a new UnifiedGraph but does not copy the original's
NodeKeys and EdgeKeys; update filterETACGraph to copy (or regenerate) the
original graph's NodeKeys and EdgeKeys into the filtered UnifiedGraph before
returning (ensuring they are non-nil and preserve order/semantics) so the JSON
response includes these fields.

Copy link
Contributor

@mvlipka mvlipka left a comment

Choose a reason for hiding this comment

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

This looks great! Just one comment. Will pull down for testing

// ShouldFilterForETAC is a helper function that determines whether ETAC(Environment-based Access Control)
// filtering should be applied for the current user. It checks if the ETAC feature flag is enabled and
// returns the users level of access(AllEnvironments or a list of environments).
func ShouldFilterForETAC(request *http.Request, db database.Database) (accessList []string, shouldFilter bool, err error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure the intent of this function is evident as the verbiage, to me, indicates that a boolean would be returned. Without reading the returns or documentation, I wouldn't know that this could be used to pull a user's environment list

The name may need some rework, but I think I would remove the Extract functionality from here and keep it as two explicit calls

Aside from that: you're pre-declaring variable names in the return but aren't using them in scope.
Named returns, generally, should be used scarcely when writing a function with a small amount of LOC

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated this function. let me know if this is more what you are thinking!

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

♻️ Duplicate comments (1)
cmd/api/src/api/v2/cypherquery_test.go (1)

583-586: Verify: response missing node_keys and edge_keys fields.

The expected response at line 585 is missing node_keys and edge_keys fields, while other successful test cases (lines 120, 386, 445, 520) include at least node_keys. This was flagged in a prior review as a potential bug in the filterETACGraph function rather than a test issue.

If the handler has been fixed to include these keys for filtered graphs, this expected response should be updated accordingly. If not, this test correctly documents the current (potentially buggy) behavior.

#!/bin/bash
# Check if node_keys/edge_keys are populated in the filtered graph response path
rg -n "NodeKeys|EdgeKeys" cmd/api/src/api/v2/cypherquery.go -A 3 -B 3
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be159ce and ab4cddd.

📒 Files selected for processing (3)
  • cmd/api/src/api/v2/cypherquery.go (4 hunks)
  • cmd/api/src/api/v2/cypherquery_test.go (17 hunks)
  • cmd/api/src/api/v2/etac.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • cmd/api/src/api/v2/etac.go
  • cmd/api/src/api/v2/cypherquery.go
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
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.
Learnt from: mvlipka
Repo: SpecterOps/BloodHound PR: 1784
File: cmd/api/src/api/v2/auth/auth.go:559-573
Timestamp: 2025-09-02T16:46:30.895Z
Learning: In the BloodHound codebase, when updating user ETAC (Environment Access Control) lists in the UpdateUser function, the approach is to let GORM handle the creation/persistence of the environment access records through model associations rather than using explicit database helper methods like UpdateEnvironmentListForUser.
Learnt from: jvacca-specterops
Repo: SpecterOps/BloodHound PR: 1823
File: packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx:108-148
Timestamp: 2025-09-08T19:01:53.112Z
Learning: In BloodHound's CypherSearch component (packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx), the sharing state reset for sharedIds and isPublic after the two-step permissions update is handled elsewhere in the codebase, so additional state reset callbacks in the updateQueryPermissions function are not needed.
📚 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/api/v2/cypherquery_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/api/v2/cypherquery_test.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/api/v2/cypherquery_test.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:

  • cmd/api/src/api/v2/cypherquery_test.go
🧬 Code graph analysis (1)
cmd/api/src/api/v2/cypherquery_test.go (4)
cmd/api/src/model/appcfg/flag.go (2)
  • FeatureETAC (43-43)
  • FeatureFlag (50-70)
cmd/api/src/queries/graph.go (1)
  • DefaultQueryFitnessLowerBoundExplore (67-67)
cmd/api/src/model/unified_graph.go (3)
  • UnifiedGraph (37-40)
  • UnifiedNode (51-61)
  • UnifiedEdge (64-71)
cmd/api/src/api/v2/cypherquery.go (1)
  • CypherQueryPayload (40-43)
⏰ 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-tests
  • GitHub Check: run-analysis
🔇 Additional comments (4)
cmd/api/src/api/v2/cypherquery_test.go (4)

35-42: LGTM!

The appcfg import is correctly added to support ETAC feature flag lookups in the new test cases.


124-284: LGTM!

The early error path test cases correctly omit the GetFlagByKey mock since these scenarios fail before reaching the ETAC filtering logic. The user context setup is consistently applied.


449-523: LGTM!

This test case thoroughly verifies ETAC filtering behavior with a user who has access to specific environments. The mock data correctly sets up:

  • Accessible nodes matching the user's environment ("testenv")
  • A hidden node with a different domain SID
  • Edges that should be hidden when connected to hidden nodes

The expected response properly reflects hidden placeholders with kind: "HIDDEN" labels.


603-607: LGTM!

The DB field is correctly wired into the Resources struct to enable ETAC feature flag lookups via the mock database. The Authorizer initialization with the same mock maintains consistency. Based on learnings, subtest parallelization is acceptable here since each creates its own mock controller.

Comment on lines +560 to 579
mocks.mockGraphQuery.EXPECT().RawCypherQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(model.UnifiedGraph{
Nodes: map[string]model.UnifiedNode{
"1": {
Label: "label",
Properties: map[string]any{"domainsid": "testenv"},
Kinds: []string{"kinds"},
},
"2": {
Label: "label2",
Properties: map[string]any{"domainsid": "value"},
Kinds: []string{"kinds"},
},
},
Edges: []model.UnifiedEdge{
{
Source: "source",
Target: "1",
},
},
}, nil)
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

Test data inconsistency: edge references non-existent node.

The edge at lines 573-577 has Source: "source", but there's no node with ID "source" in the Nodes map (only "1" and "2" exist). This differs from the similar test case at lines 488-514 which correctly includes a "source" node.

This inconsistency could mask potential edge-case bugs in the filtering logic when edges reference nodes outside the graph.

🔎 Suggested fix

Either add the missing "source" node to align with realistic graph data:

 				mocks.mockGraphQuery.EXPECT().RawCypherQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(model.UnifiedGraph{
 					Nodes: map[string]model.UnifiedNode{
 						"1": {
 							Label:      "label",
 							Properties: map[string]any{"domainsid": "testenv"},
 							Kinds:      []string{"kinds"},
 						},
 						"2": {
 							Label:      "label2",
 							Properties: map[string]any{"domainsid": "value"},
 							Kinds:      []string{"kinds"},
 						},
+						"source": {
+							Label:      "labelSource",
+							Properties: map[string]any{"domainsid": "otherenv"},
+							Kinds:      []string{"kinds"},
+						},
 					},

Or update the edge to reference existing nodes:

 					Edges: []model.UnifiedEdge{
 						{
-							Source: "source",
+							Source: "2",
 							Target: "1",
 						},
 					},
🤖 Prompt for AI Agents
In cmd/api/src/api/v2/cypherquery_test.go around lines 560-579 the test graph
contains an edge with Source: "source" but the Nodes map only contains "1" and
"2", causing an inconsistent test fixture; fix by either adding a "source" node
entry to the Nodes map (matching the shape used in the similar test at lines
488-514) with appropriate Label/Properties/Kinds, or change the edge Source to
reference an existing node ID ("1" or "2") so the edge points to a valid node.

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

🧹 Nitpick comments (1)
cmd/api/src/api/v2/cypherquery.go (1)

202-202: Consider documenting the environment key list.

The hardcoded list ["domainsid", "tenantid"] determines which node properties are checked for environment access. If additional environment identifier properties exist (or may be added in the future), this could silently fail to filter correctly.

Consider:

  • Adding a comment explaining why only these two keys are checked
  • Or centralizing this list as a package-level constant if used elsewhere
  • Or verifying against a schema/configuration to ensure completeness
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab4cddd and 1c5241e.

📒 Files selected for processing (1)
  • cmd/api/src/api/v2/cypherquery.go
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
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.
Learnt from: mvlipka
Repo: SpecterOps/BloodHound PR: 1784
File: cmd/api/src/api/v2/auth/auth.go:559-573
Timestamp: 2025-09-02T16:46:30.895Z
Learning: In the BloodHound codebase, when updating user ETAC (Environment Access Control) lists in the UpdateUser function, the approach is to let GORM handle the creation/persistence of the environment access records through model associations rather than using explicit database helper methods like UpdateEnvironmentListForUser.
Learnt from: jvacca-specterops
Repo: SpecterOps/BloodHound PR: 1823
File: packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx:108-148
Timestamp: 2025-09-08T19:01:53.112Z
Learning: In BloodHound's CypherSearch component (packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx), the sharing state reset for sharedIds and isPublic after the two-step permissions update is handled elsewhere in the codebase, so additional state reset callbacks in the updateQueryPermissions function are not needed.
📚 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/api/v2/cypherquery.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:

  • cmd/api/src/api/v2/cypherquery.go
📚 Learning: 2025-09-08T19:01:53.112Z
Learnt from: jvacca-specterops
Repo: SpecterOps/BloodHound PR: 1823
File: packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx:108-148
Timestamp: 2025-09-08T19:01:53.112Z
Learning: In BloodHound's CypherSearch component (packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx), the sharing state reset for sharedIds and isPublic after the two-step permissions update is handled elsewhere in the codebase, so additional state reset callbacks in the updateQueryPermissions function are not needed.

Applied to files:

  • cmd/api/src/api/v2/cypherquery.go
⏰ 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: build-ui
  • GitHub Check: run-tests
🔇 Additional comments (6)
cmd/api/src/api/v2/cypherquery.go (6)

100-106: LGTM: Properties always fetched for ETAC filtering.

The hardcoded true for includeProperties ensures ETAC filtering logic can access node properties for environment checks. The original payload.IncludeProperties flag is correctly honored later during response construction.


113-122: LGTM: ETAC filtering integration.

The filtering is applied correctly with proper error handling and appropriate 404 response for non-mutation queries that return no accessible resources.


124-138: LGTM: Complete property stripping.

Both node and edge properties are correctly removed from the filtered response when payload.IncludeProperties is false, maintaining the original API contract.


229-240: Verify: All hidden nodes share ObjectId="HIDDEN".

All hidden placeholder nodes are assigned the same ObjectId="HIDDEN". While this appears intentional for ETAC display purposes, please confirm that downstream code (graph rendering, caching, etc.) doesn't assume unique ObjectIds within a response.

Based on learnings, the empty accessList behavior (when a user has no environment assignments) is correct and will properly filter all nodes.


205-214: LGTM: Environment-based node filtering logic.

The filtering correctly checks node properties against the user's allowed environment list. The type assertion and contains check properly handle missing properties and non-string values.


247-261: LGTM: Edge filtering preserves graph consistency.

Edges are correctly marked as hidden when either endpoint is inaccessible, maintaining graph structure while respecting ETAC boundaries.

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

🤖 Fix all issues with AI Agents
In @cmd/api/src/queries/graph.go:
- Line 448: The stray debug print in PrepareCypherQuery (fmt.Printf("multi
query: %v", queryModel.SingleQuery.MultiPartQuery.Parts)) should be removed;
replace any required debugging with the structured logger used in the codebase
(e.g., use slog.DebugContext with a context and slog.Any("parts", ...) if you
change PrepareCypherQuery to accept a context) or simply delete the print to
avoid unstructured stdout, potential sensitive output, and test pipeline
failures.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c5241e and 8739e84.

📒 Files selected for processing (4)
  • cmd/api/src/config/config.go
  • cmd/api/src/config/default.go
  • cmd/api/src/queries/graph.go
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/SelectedDetailsTabs/SelectedDetailsTabs.test.tsx
✅ Files skipped from review due to trivial changes (3)
  • cmd/api/src/config/default.go
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/SelectedDetailsTabs/SelectedDetailsTabs.test.tsx
  • cmd/api/src/config/config.go
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
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.
Learnt from: mvlipka
Repo: SpecterOps/BloodHound PR: 1784
File: cmd/api/src/api/v2/auth/auth.go:559-573
Timestamp: 2025-09-02T16:46:30.895Z
Learning: In the BloodHound codebase, when updating user ETAC (Environment Access Control) lists in the UpdateUser function, the approach is to let GORM handle the creation/persistence of the environment access records through model associations rather than using explicit database helper methods like UpdateEnvironmentListForUser.
Learnt from: jvacca-specterops
Repo: SpecterOps/BloodHound PR: 1823
File: packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx:108-148
Timestamp: 2025-09-08T19:01:53.112Z
Learning: In BloodHound's CypherSearch component (packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx), the sharing state reset for sharedIds and isPublic after the two-step permissions update is handled elsewhere in the codebase, so additional state reset callbacks in the updateQueryPermissions function are not needed.
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.
🪛 GitHub Actions: Run Go Unit Tests
cmd/api/src/queries/graph.go

[error] 1-1: Test run 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: run-analysis
  • GitHub Check: build-ui
  • GitHub Check: run-tests

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

🤖 Fix all issues with AI agents
In @cmd/api/src/api/v2/cypherquery.go:
- Around line 114-119: The error response for the ETAC filtering call is too
generic; when s.filterETACGraph(request.Context(), graphResponse, user) returns
an error, include the actual error text and context in the response and logs by
passing err.Error() (or a wrapped message) into api.BuildErrorResponse and/or
logging it before calling api.WriteErrorResponse; update the handling around
s.filterETACGraph to construct a descriptive message like "ETAC filter failed:
<err>" and use that with api.BuildErrorResponse and any available logger so both
the response and logs contain the specific failure details.
🧹 Nitpick comments (1)
cmd/api/src/api/v2/cypherquery.go (1)

203-203: Consider extracting environment keys to a constant.

The hardcoded environmentKeys slice could become a package-level constant for better maintainability. If new environment types are added in the future, having these keys in one centralized location would make updates easier.

♻️ Suggested refactor

At the package level:

var (
	errUnauthorizedGraphMutation = errors.New("unauthorized graph mutation")
	// ETACEnvironmentKeys are the property keys used to determine node environment ownership
	ETACEnvironmentKeys = []string{"domainsid", "tenantid"}
)

Then in the function:

-	environmentKeys := []string{"domainsid", "tenantid"}
-
 	// filter nodes based on environment access
 	for id, node := range graphResponse.Nodes {
 		include := false
-		for _, key := range environmentKeys {
+		for _, key := range ETACEnvironmentKeys {
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8739e84 and ae15124.

📒 Files selected for processing (1)
  • cmd/api/src/api/v2/cypherquery.go
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
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.
📚 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/api/v2/cypherquery.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:

  • cmd/api/src/api/v2/cypherquery.go
📚 Learning: 2025-09-08T19:01:53.112Z
Learnt from: jvacca-specterops
Repo: SpecterOps/BloodHound PR: 1823
File: packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx:108-148
Timestamp: 2025-09-08T19:01:53.112Z
Learning: In BloodHound's CypherSearch component (packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx), the sharing state reset for sharedIds and isPublic after the two-step permissions update is handled elsewhere in the codebase, so additional state reset callbacks in the updateQueryPermissions function are not needed.

Applied to files:

  • cmd/api/src/api/v2/cypherquery.go
⏰ 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 (11)
cmd/api/src/api/v2/cypherquery.go (11)

84-89: Good security practice: early user extraction with fail-safe behavior.

Retrieving the user early and failing with a 500 error if extraction fails is appropriate for this security-critical ETAC filtering path.


102-106: LGTM: Properties fetched internally for ETAC filtering.

Defaulting includeProperties to true ensures ETAC filtering has access to environment properties (domainsid, tenantid) for access control decisions. The user's preference is correctly honored later by stripping properties when payload.IncludeProperties is false.


120-123: LGTM: Zero-result check operates on filtered graph.

Checking the filtered response for empty results is correct. Users whose queries return results that are all filtered out by ETAC should receive a 404, not see hidden placeholders for everything.


125-139: LGTM: Property stripping operates on filtered graph.

Correctly strips properties from the filtered response when IncludeProperties is false, preventing information leakage and honoring user preferences.


141-141: LGTM: Property processing uses filtered graph.

Correctly processes properties from the filtered response, ensuring property keys reflect only the accessible data.


186-196: LGTM: Early exit when ETAC filtering not needed.

Efficiently checks if filtering is required and returns the original graph when filtering is disabled, avoiding unnecessary processing.


198-198: LGTM: Empty access list behavior is intentional.

Based on learnings, when ExtractEnvironmentIDsFromUser returns an empty list (for users with no environment assignments), hiding all nodes is the correct security behavior—users with no environment assignments should see no results.


206-242: LGTM: Node filtering logic correctly implements ETAC.

The logic correctly:

  • Checks each node for environment property matches against the user's access list
  • Preserves nodes the user can access
  • Replaces inaccessible nodes with hidden placeholders that have all sensitive data (properties, ObjectId) removed

Note: Nodes without domainsid or tenantid properties will be hidden. This appears intentional for security—if a node lacks environment identifiers, users without full access shouldn't see it.


223-227: Good defensive programming: safe kind extraction.

Safely handles empty or missing Kinds by defaulting to "Unknown", preventing potential panics or display issues.


230-240: LGTM: Hidden node structure prevents information leakage.

The hidden placeholder correctly:

  • Sets all sensitive fields (ObjectId, Properties) to safe values (nil or "HIDDEN")
  • Preserves only the kind for UX clarity ("** Hidden {Kind} Object **")
  • Marks the node with Hidden: true for downstream processing

248-262: LGTM: Edge filtering correctly handles hidden nodes.

Edges connected to hidden nodes are properly replaced with hidden placeholders, preventing information leakage about relationship existence between inaccessible objects. Properties are correctly set to nil.

Comment on lines +114 to +119
// etac filtering
filteredResponse, err := s.filterETACGraph(request.Context(), graphResponse, user)
if err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, "error", request), response)
return
}
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

Improve error message for ETAC filtering failure.

The error message on Line 117 is too generic. Consider providing more context for debugging and user support.

💬 Suggested improvement
 filteredResponse, err := s.filterETACGraph(request.Context(), graphResponse, user)
 if err != nil {
-	api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, "error", request), response)
+	api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, "failed to apply access control filtering", request), response)
 	return
 }
📝 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
// etac filtering
filteredResponse, err := s.filterETACGraph(request.Context(), graphResponse, user)
if err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, "error", request), response)
return
}
// etac filtering
filteredResponse, err := s.filterETACGraph(request.Context(), graphResponse, user)
if err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, "failed to apply access control filtering", request), response)
return
}
🤖 Prompt for AI Agents
In @cmd/api/src/api/v2/cypherquery.go around lines 114 - 119, The error response
for the ETAC filtering call is too generic; when
s.filterETACGraph(request.Context(), graphResponse, user) returns an error,
include the actual error text and context in the response and logs by passing
err.Error() (or a wrapped message) into api.BuildErrorResponse and/or logging it
before calling api.WriteErrorResponse; update the handling around
s.filterETACGraph to construct a descriptive message like "ETAC filter failed:
<err>" and use that with api.BuildErrorResponse and any available logger so both
the response and logs contain the specific failure details.

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

Labels

api A pull request containing changes affecting the API code. enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants