Skip to content

feat: Add bounded graph expansion API BP-2366 - #3089

Open
justin-prime1 wants to merge 2 commits into
mainfrom
bp-2366-bounded-graph-expansion-api
Open

feat: Add bounded graph expansion API BP-2366#3089
justin-prime1 wants to merge 2 commits into
mainfrom
bp-2366-bounded-graph-expansion-api

Conversation

@justin-prime1

@justin-prime1 justin-prime1 commented Aug 3, 2026

Copy link
Copy Markdown

Summary

  • add POST /api/v2/graphs/expand for bounded one-hop graph expansion
  • include inbound/outbound direction, limit validation, truncation metadata, and optional property keys
  • expose the endpoint in OpenAPI and the JS client library

Motivation

This supports the BHE Explore directional expansion work while also giving API consumers a bounded one-hop expansion endpoint without relying on UI-generated Cypher.

Testing

  • go test ./cmd/api/src/api/v2 -run 'TestBuildGraphExpansionQuery|TestGraphExpansionLimit|TestPruneGraphExpansionResponse|TestResources_ExpandGraph'
  • yarn workspace js-client-library check-types
  • git diff --check
  • just prepare-for-codereview

Companion

Resolves: BP-2366

Summary by CodeRabbit

  • New Features

    • Added a graph expansion API for retrieving bounded, one-hop relationships from a specified node.
    • Supports inbound or outbound traversal, configurable result limits, optional properties, and truncation metadata.
    • Added JavaScript client support with request and response types.
  • Documentation

    • Documented the endpoint, request parameters, response format, and error responses in the API specification.
  • Tests

    • Added coverage for validation, limits, query generation, filtering, and successful API responses.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 92e3d5f4-e434-41e5-bb2e-39bde8e08ef1

📥 Commits

Reviewing files that changed from the base of the PR and between 9fb0070 and aa3b471.

⛔ Files ignored due to path filters (4)
  • .yarn/cache/brace-expansion-npm-5.0.8-08bf144160-75d2d2ebc8.zip is excluded by !**/.yarn/**, !**/*.zip
  • .yarn/cache/brace-expansion-npm-5.0.9-2717df6b49-d8683d6129.zip is excluded by !**/.yarn/**, !**/*.zip
  • .yarn/cache/fast-uri-npm-3.1.5-39a8d368d6-784bef687c.zip is excluded by !**/.yarn/**, !**/*.zip
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (1)
  • package.json

📝 Walkthrough

Walkthrough

Changes

The PR adds POST /api/v2/graphs/expand. The endpoint validates traversal inputs, executes bounded graph queries, filters and prunes results, and returns truncation metadata. OpenAPI definitions, Go tests, JavaScript client types, and dependency versions support the endpoint.

Graph expansion API

Layer / File(s) Summary
API contract
packages/go/openapi/src/openapi.yaml, packages/go/openapi/src/paths/graph.graphs.expand.yaml, packages/go/openapi/src/schemas/model.unified-graph.expansion.yaml, packages/go/openapi/doc/openapi.json
Defines the expansion request, response, validation rules, limit, truncation metadata, and standard error responses.
Server expansion flow
cmd/api/src/api/registration/v2.go, cmd/api/src/api/v2/graphexpansion.go, cmd/api/src/api/v2/graphexpansion_internal_test.go, cmd/api/src/api/v2/graphexpansion_test.go
Registers and implements the authenticated endpoint. It validates inputs, builds and executes bounded Cypher queries, applies ETAC filtering, prunes results, and serializes responses. Tests cover query construction, limits, pruning, success, and validation errors.
JavaScript client integration
packages/javascript/js-client-library/src/requests.ts, packages/javascript/js-client-library/src/types.ts, packages/javascript/js-client-library/src/responses.ts, packages/javascript/js-client-library/src/client.ts
Adds typed expansion requests and responses and exposes BHEAPIClient.expandGraph.
Dependency version updates
go.mod, package.json
Updates the Chi module and JavaScript package resolutions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ExpandGraph
  participant GraphDatabase
  participant ETACFiltering
  Client->>ExpandGraph: POST graph expansion payload
  ExpandGraph->>GraphDatabase: Execute bounded Cypher query
  GraphDatabase-->>ExpandGraph: Return graph results
  ExpandGraph->>ETACFiltering: Filter graph results
  ETACFiltering-->>ExpandGraph: Return filtered results
  ExpandGraph-->>Client: Return nodes, edges, limit, and truncation
Loading

Possibly related PRs

Suggested labels: api, go, javascript, enhancement, dependencies

Suggested reviewers: urangel

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the new bounded graph expansion API and references the associated ticket.
Description check ✅ Passed The description explains the feature, motivation, testing, client and OpenAPI changes, and resolves BP-2366.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bp-2366-bounded-graph-expansion-api

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

@justin-prime1
justin-prime1 marked this pull request as ready for review August 3, 2026 17:59
@coderabbitai coderabbitai Bot added api A pull request containing changes affecting the API code. enhancement New feature or request go Pull requests that update go code javascript Pull requests that update javascript code labels Aug 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (3)
cmd/api/src/api/v2/graphexpansion.go (2)

80-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Group declarations into a var (...) block.

filteredRelationshipKinds, seenRelationshipKinds, and relationshipMatch are declared at different points in the function instead of being grouped near the top.

♻️ Proposed refactor to hoist declarations
 func buildGraphExpansionQuery(nodeID int64, direction string, relationshipKinds []string, limit int) (string, error) {
-	filteredRelationshipKinds := make([]string, 0, len(relationshipKinds)+1)
-	seenRelationshipKinds := map[string]struct{}{}
+	var (
+		filteredRelationshipKinds = make([]string, 0, len(relationshipKinds)+1)
+		seenRelationshipKinds     = map[string]struct{}{}
+		relationshipMatch         string
+	)
 
 	for _, relationshipKind := range append([]string{graphExpansionBuiltInKinds}, relationshipKinds...) {
 		if !graphExpansionRelationshipKindPattern.MatchString(relationshipKind) {
 			return "", fmt.Errorf("invalid relationship kind: %s", relationshipKind)
 		}
 
 		if _, seen := seenRelationshipKinds[relationshipKind]; seen {
 			continue
 		}
 
 		seenRelationshipKinds[relationshipKind] = struct{}{}
 		filteredRelationshipKinds = append(filteredRelationshipKinds, relationshipKind)
 	}
 
-	relationshipMatch := ""
 	relationshipFilter := strings.Join(filteredRelationshipKinds, "|")

As per coding guidelines, "When possible, group variable initializations in a var (...) block and hoist them to the top of the function."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/api/src/api/v2/graphexpansion.go` around lines 80 - 98, Update
buildGraphExpansionQuery to group the initial declarations of
filteredRelationshipKinds, seenRelationshipKinds, and relationshipMatch in a
single var block near the top of the function, while preserving their existing
initialization values and subsequent behavior.

Source: Coding guidelines


259-259: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Preserve properties required by ETAC filtering. filterETACGraph reads node.Properties to determine environment access. If RawCypherQuery receives false while ETAC filtering is active, all nodes become hidden. If optimization is required, request properties when payload.IncludeProperties || filterForETAC and strip them from the response when payload.IncludeProperties is false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/api/src/api/v2/graphexpansion.go` at line 259, Update the RawCypherQuery
call in the graph expansion handler to request properties whenever
payload.IncludeProperties or filterForETAC is true, so filterETACGraph can
evaluate node environment access; when properties were requested only for ETAC
filtering and payload.IncludeProperties is false, remove them before returning
the response.
cmd/api/src/api/v2/graphexpansion_test.go (1)

45-122: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the ETAC-filtered path.

This test uses model.User{AllEnvironments: true}, so ShouldFilterForETAC takes the non-filtering branch and filterETACGraph is never exercised. Add a test case with a user that doesn't have AllEnvironments to confirm filterETACGraph correctly restricts nodes and edges before the response is pruned and returned.

Do you want me to generate this additional test case?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/api/src/api/v2/graphexpansion_test.go` around lines 45 - 122, Add a
separate ExpandGraph test case using a user without AllEnvironments so
ShouldFilterForETAC enters the filtering branch and filterETACGraph is
exercised. Configure the graph fixture with ETAC-relevant nodes and edges, then
assert the response contains only permitted nodes and their corresponding edges
before the existing limit-based pruning. Keep the current all-environments test
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/go/openapi/src/paths/graph.graphs.expand.yaml`:
- Around line 59-62: Require data in the 200 response schema by adding it to the
response object's required fields beside properties in
packages/go/openapi/src/paths/graph.graphs.expand.yaml:59-62. Regenerate
packages/go/openapi/doc/openapi.json:7653-7659 from the corrected YAML source;
do not edit the generated artifact directly.
- Around line 37-39: Update node_id in
packages/go/openapi/src/paths/graph.graphs.expand.yaml (lines 37-39) to use a
JavaScript-safe representation by constraining it to the safe integer range or
changing it to a decimal string, while preserving compatibility with the Go
int64 handler. Regenerate packages/go/openapi/doc/openapi.json (lines 7621-7624)
so the generated schema reflects the same representation.

---

Nitpick comments:
In `@cmd/api/src/api/v2/graphexpansion_test.go`:
- Around line 45-122: Add a separate ExpandGraph test case using a user without
AllEnvironments so ShouldFilterForETAC enters the filtering branch and
filterETACGraph is exercised. Configure the graph fixture with ETAC-relevant
nodes and edges, then assert the response contains only permitted nodes and
their corresponding edges before the existing limit-based pruning. Keep the
current all-environments test unchanged.

In `@cmd/api/src/api/v2/graphexpansion.go`:
- Around line 80-98: Update buildGraphExpansionQuery to group the initial
declarations of filteredRelationshipKinds, seenRelationshipKinds, and
relationshipMatch in a single var block near the top of the function, while
preserving their existing initialization values and subsequent behavior.
- Line 259: Update the RawCypherQuery call in the graph expansion handler to
request properties whenever payload.IncludeProperties or filterForETAC is true,
so filterETACGraph can evaluate node environment access; when properties were
requested only for ETAC filtering and payload.IncludeProperties is false, remove
them before returning the response.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: fc183c0a-f6d2-4db7-8132-a08d953f835d

📥 Commits

Reviewing files that changed from the base of the PR and between 39e9977 and af47d75.

📒 Files selected for processing (12)
  • cmd/api/src/api/registration/v2.go
  • cmd/api/src/api/v2/graphexpansion.go
  • cmd/api/src/api/v2/graphexpansion_internal_test.go
  • cmd/api/src/api/v2/graphexpansion_test.go
  • packages/go/openapi/doc/openapi.json
  • packages/go/openapi/src/openapi.yaml
  • packages/go/openapi/src/paths/graph.graphs.expand.yaml
  • packages/go/openapi/src/schemas/model.unified-graph.expansion.yaml
  • packages/javascript/js-client-library/src/client.ts
  • packages/javascript/js-client-library/src/requests.ts
  • packages/javascript/js-client-library/src/responses.ts
  • packages/javascript/js-client-library/src/types.ts

Comment on lines +37 to +39
node_id:
type: integer
format: int64

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'export interface GraphExpansionRequest|node_id' \
  packages/javascript/js-client-library/src/requests.ts

rg -n -C 6 '\bExpandGraph\b|node_id|int64|uint64|MaxSafeInteger|9007199254740991' \
  cmd/api/src packages/go/openapi/src

Repository: SpecterOps/BloodHound

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate request definitions ---'
rg -n -C 5 'GraphExpansionRequest|node_id' packages/javascript packages/go/openapi/src/paths/graph.graphs.expand.yaml 2>/dev/null | head -n 240

printf '%s\n' '--- graph expansion implementation and models ---'
rg -n -C 6 'GraphExpansionPayload|NodeID|ExpandGraph|graphs/expand' cmd/api/src packages/go/openapi/src --glob '*.go' --glob '*.yaml' | head -n 320

printf '%s\n' '--- contract snippets ---'
sed -n '25,75p' packages/go/openapi/src/paths/graph.graphs.expand.yaml
sed -n '7610,7670p' packages/go/openapi/doc/openapi.json

Repository: SpecterOps/BloodHound

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- expansion resource and payload declarations ---'
find cmd/api/src -type f -name '*.go' -print0 |
  xargs -0 rg -l 'func .*ExpandGraph|type GraphExpansionPayload|GraphExpansionPayload' |
  sort

printf '%s\n' '--- relevant declarations and handlers ---'
find cmd/api/src -type f -name '*.go' -print0 |
  xargs -0 rg -n -C 12 'func .*ExpandGraph|type GraphExpansionPayload|NodeID.*int64|node_id is required' |
  head -n 360

printf '%s\n' '--- graph ID type definitions ---'
find . -type f \( -name '*.go' -o -name '*.ts' \) -print0 |
  xargs -0 rg -n -C 3 'type ID (uint|int)|type ID =' |
  head -n 160

Repository: SpecterOps/BloodHound

Length of output: 12374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const safe = 9007199254740991;
const firstUnsafe = 9007199254740993;
const parsed = JSON.parse(`{"node_id":${firstUnsafe}}`).node_id;
console.log(JSON.stringify({
  maxSafeInteger: Number.MAX_SAFE_INTEGER,
  firstUnsafe,
  parsed,
  rounded: parsed !== firstUnsafe,
  adjacentIntegersEqual: Number(firstUnsafe) === Number(firstUnsafe + 1),
}));
JS

printf '%s\n' '--- storage and sequence declarations for node_id ---'
rg -n -C 4 'node_id_seq|node_id.*(bigserial|bigint)|bigserial.*node_id|CREATE TABLE.*nodes' . \
  --glob '*.sql' --glob '*.go' --glob '*.yaml' --glob '*.yml' |
  head -n 240

printf '%s\n' '--- source and generated node_id schemas ---'
python3 - <<'PY'
import json
from pathlib import Path

doc = json.loads(Path("packages/go/openapi/doc/openapi.json").read_text())
hits = []

def walk(value, path=()):
    if isinstance(value, dict):
        if "node_id" in value and isinstance(value["node_id"], dict):
            hits.append((path + ("node_id",), value["node_id"]))
        for key, child in value.items():
            walk(child, path + (key,))
    elif isinstance(value, list):
        for i, child in enumerate(value):
            walk(child, path + (str(i),))

walk(doc)
for path, schema in hits:
    if schema.get("type") == "integer" and schema.get("format") == "int64":
        print(".".join(path), schema)
PY

Repository: SpecterOps/BloodHound

Length of output: 6029


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const jsonValue = "9007199254740993";
const parsed = JSON.parse(`{"node_id":${jsonValue}}`).node_id;
const exact = BigInt(jsonValue);
console.log(JSON.stringify({
  maxSafeInteger: Number.MAX_SAFE_INTEGER,
  wireValue: jsonValue,
  parsedAsNumber: parsed,
  parsedAsBigInt: BigInt(parsed),
  losesPrecision: BigInt(parsed) !== exact,
  adjacentNumbersEqual: Number("9007199254740992") === Number(jsonValue),
}));
JS

sed -n '42,52p' cmd/api/src/api/v2/graphexpansion.go
sed -n '34,40p' packages/javascript/js-client-library/src/requests.ts

Repository: SpecterOps/BloodHound

Length of output: 769


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const jsonValue = "9007199254740993";
const parsed = JSON.parse(`{"node_id":${jsonValue}}`).node_id;
const exact = BigInt(jsonValue);
console.log({
  maxSafeInteger: Number.MAX_SAFE_INTEGER,
  wireValue: jsonValue,
  parsedAsNumber: String(parsed),
  parsedAsBigInt: String(BigInt(parsed)),
  losesPrecision: BigInt(parsed) !== exact,
  adjacentNumbersEqual: Number("9007199254740992") === Number(jsonValue),
});
JS

sed -n '42,52p' cmd/api/src/api/v2/graphexpansion.go
sed -n '34,40p' packages/javascript/js-client-library/src/requests.ts

Repository: SpecterOps/BloodHound

Length of output: 970


Use a JavaScript-safe representation for node_id. The Go handler accepts int64, while GraphExpansionRequest.node_id uses number; values above 9007199254740991 can be rounded and expand the wrong node. Constrain the OpenAPI schema to the safe range or use a decimal string, then regenerate packages/go/openapi/doc/openapi.json.

📍 Affects 2 files
  • packages/go/openapi/src/paths/graph.graphs.expand.yaml#L37-L39 (this comment)
  • packages/go/openapi/doc/openapi.json#L7621-L7624
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/go/openapi/src/paths/graph.graphs.expand.yaml` around lines 37 - 39,
Update node_id in packages/go/openapi/src/paths/graph.graphs.expand.yaml (lines
37-39) to use a JavaScript-safe representation by constraining it to the safe
integer range or changing it to a decimal string, while preserving compatibility
with the Go int64 handler. Regenerate packages/go/openapi/doc/openapi.json
(lines 7621-7624) so the generated schema reflects the same representation.

Comment on lines +59 to +62
type: object
properties:
data:
$ref: './../schemas/model.unified-graph.expansion.yaml'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Require data in the 200 response.

The response schema permits {} because it does not list data as required. The required limit and truncated fields do not apply when data is absent. Require data at the response-object level.

  • packages/go/openapi/src/paths/graph.graphs.expand.yaml#L59-L62: add required: [data] beside properties.
  • packages/go/openapi/doc/openapi.json#L7653-L7659: regenerate this artifact from the corrected YAML.

Based on learnings, regenerate packages/go/openapi/doc/openapi.json from the YAML source rather than editing it directly.

Proposed fix
 schema:
   type: object
+  required:
+    - data
   properties:
     data:
       $ref: './../schemas/model.unified-graph.expansion.yaml'
📝 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
type: object
properties:
data:
$ref: './../schemas/model.unified-graph.expansion.yaml'
type: object
required:
- data
properties:
data:
$ref: './../schemas/model.unified-graph.expansion.yaml'
📍 Affects 2 files
  • packages/go/openapi/src/paths/graph.graphs.expand.yaml#L59-L62 (this comment)
  • packages/go/openapi/doc/openapi.json#L7653-L7659
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/go/openapi/src/paths/graph.graphs.expand.yaml` around lines 59 - 62,
Require data in the 200 response schema by adding it to the response object's
required fields beside properties in
packages/go/openapi/src/paths/graph.graphs.expand.yaml:59-62. Regenerate
packages/go/openapi/doc/openapi.json:7653-7659 from the corrected YAML source;
do not edit the generated artifact directly.

Source: Learnings

@justin-prime1
justin-prime1 force-pushed the bp-2366-bounded-graph-expansion-api branch from af47d75 to 9fb0070 Compare August 3, 2026 18:19
@justin-prime1
justin-prime1 requested review from a team as code owners August 3, 2026 21:20
@coderabbitai coderabbitai Bot added the dependencies Pull requests that update a dependency file label Aug 3, 2026
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. dependencies Pull requests that update a dependency file enhancement New feature or request go Pull requests that update go code javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant