Skip to content

feat: implementing AZAddOwner relationship BED-4754#2649

Merged
bsheth711 merged 5 commits intomainfrom
BED-4754-azaddowner-relationships
Apr 20, 2026
Merged

feat: implementing AZAddOwner relationship BED-4754#2649
bsheth711 merged 5 commits intomainfrom
BED-4754-azaddowner-relationships

Conversation

@bsheth711
Copy link
Copy Markdown
Contributor

@bsheth711 bsheth711 commented Apr 14, 2026

Description

This PR implements the AZAddOwner relationship in Azure post processing. Previously this relationship was unimplemented. The PR also includes an integration test for the new functionality.

The HybridIdentityAdministratorRole literal was found from: https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference.

Motivation and Context

Resolves BED-4754

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

How Has This Been Tested?

I created a new integration test for post processing to test the creation of the AZAddOwner relationship. All new and existing tests compile and pass locally. Additionally, creation of relationships with sample data has been verified.

Screenshots (optional):

image

Types of changes

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

Checklist:

Summary by CodeRabbit

  • New Features

    • Detects Azure role assignments that grant owner capabilities and emits corresponding post-processed relationships.
    • Adds the Hybrid Identity Administrator role to tracked Azure owner-capable roles.
  • Tests

    • Added an integration test validating owner-relationship creation across tenants, applications, service principals, and roles.
    • Added integration test helpers and a unit test to verify the tracked role-ID list and integration setup.

@bsheth711 bsheth711 self-assigned this Apr 14, 2026
@bsheth711 bsheth711 added the go Pull requests that update go code label Apr 14, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Azure integration test helpers and node constructors, introduces AddOwner role IDs and a new post-processing flow that clears existing edges, discovers tenant roles and targets, enqueues azure.AddOwner relationship jobs, and adds unit and integration tests validating AddOwner edge creation.

Changes

Cohort / File(s) Summary
Integration test suite
packages/go/analysis/azure/azure_integration_suite_test.go
New integration test helper IntegrationTestSuite with setup/teardown for a pgtestdb-backed graph.Database, migrations/schema assertions, and helpers NewNode, NewRelationship, plus Azure-typed constructors (NewAzureTenant, NewAzureApplication, NewAzureServicePrincipal, NewAzureRole).
Integration test
packages/go/analysis/azure/azure_integration_test.go
Added TestAZAddOwner which seeds tenant, application, service principal, and role nodes, invokes CreateAZAddOwnerEdge(...), and asserts exactly 8 azure.AddOwner relationships for the expected role→target pairs.
Post-processing logic
packages/go/analysis/azure/post.go
Added exported AddOwnerRoleIDs() returning four role template IDs and new CreateAZAddOwnerEdge / postAzureAddOwner flow that clears existing azure.AddOwner edges, discovers tenant roles and tenant apps/SPs, and enqueues post.EnsureRelationshipJob jobs for azure.AddOwner; wired into Post() aggregation.
Unit test
packages/go/analysis/azure/post_test.go
Added TestAddOwnerRoleIDs asserting AddOwnerRoleIDs() returns the four expected role template IDs.
Graph schema constants
packages/go/graphschema/azure/roles.go
Added constant HybridIdentityAdministratorRole = "8ac3fc64-6eca-42ea-9e69-59f4c7b60eb2".

Sequence Diagram(s)

sequenceDiagram
    actor Test
    participant CreateAZAddOwnerEdge as CreateAZAddOwnerEdge()
    participant GraphDB as GraphDB
    participant JobQueue as EnsureRelationshipJobQueue

    Test->>CreateAZAddOwnerEdge: Invoke CreateAZAddOwnerEdge(ctx, db)
    CreateAZAddOwnerEdge->>GraphDB: Clear existing azure.AddOwner edges
    GraphDB-->>CreateAZAddOwnerEdge: Acknowledgement
    CreateAZAddOwnerEdge->>GraphDB: Query tenants
    GraphDB-->>CreateAZAddOwnerEdge: tenant list
    loop per tenant
        CreateAZAddOwnerEdge->>GraphDB: Query roles by AddOwnerRoleIDs()
        GraphDB-->>CreateAZAddOwnerEdge: role nodes
        CreateAZAddOwnerEdge->>GraphDB: Query tenant applications & service principals
        GraphDB-->>CreateAZAddOwnerEdge: app/SP nodes
        loop for each role × target
            CreateAZAddOwnerEdge->>JobQueue: Enqueue EnsureRelationshipJob(kind=azure.AddOwner, start=role, end=target)
            JobQueue-->>CreateAZAddOwnerEdge: Enqueue ack
        end
    end
    CreateAZAddOwnerEdge-->>Test: Return stats / error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% 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 summarizes the main change: implementing the AZAddOwner relationship with the associated ticket reference.
Description check ✅ Passed The description follows the template structure with all required sections completed: description of changes, motivation/context with ticket resolution, testing details, change type, and completed checklist items.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch BED-4754-azaddowner-relationships

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

Copy link
Copy Markdown
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)
packages/go/analysis/azure/azure_integration_test.go (1)

43-44: Defer suite teardown immediately after setup.

If a require.* fails before Line 108, cleanup is skipped and DB resources can leak for the test run.

♻️ Proposed change
 func TestAZAddOwner(t *testing.T) {
 	t.Parallel()

 	//#region Setup for test
 	var (
 		suite = setupIntegrationTestSuite(t)
@@
 	)
+	defer teardownIntegrationTestSuite(t, &suite)
@@
-	teardownIntegrationTestSuite(t, &suite)
 }

Also applies to: 108-109

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/go/analysis/azure/azure_integration_test.go` around lines 43 - 44,
After calling setupIntegrationTestSuite(t) assign its result to suite and
immediately defer suite teardown to avoid leaking DB resources if a require.*
assertion fails; specifically, right after setupIntegrationTestSuite(t) add a
deferred call to the suite's teardown method (e.g., defer suite.TearDownSuite()
or defer suite.TearDown()) so teardown always runs, and apply the same change to
the other setup call around lines 108-109 that also instantiates a suite.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/go/analysis/azure/azure_integration_test.go`:
- Around line 83-95: The test builds ambiguous keys by concatenating IDs into
expected map entries (see expected map population) and when iterating
addOwnerEdges using edge.StartID and edge.EndID; replace the fragile
concatenation with a safe composite key (e.g., use a tuple-like struct key or a
delimiter/format that cannot collide such as fmt.Sprintf("%s|%s", start, end))
so each pair is uniquely represented; update both the expected map keys (where
HybridIdentityAdminRole.ID, AZApp.ID, etc. are inserted) and the lookup used for
addOwnerEdges to use the new composite key type/format.

---

Nitpick comments:
In `@packages/go/analysis/azure/azure_integration_test.go`:
- Around line 43-44: After calling setupIntegrationTestSuite(t) assign its
result to suite and immediately defer suite teardown to avoid leaking DB
resources if a require.* assertion fails; specifically, right after
setupIntegrationTestSuite(t) add a deferred call to the suite's teardown method
(e.g., defer suite.TearDownSuite() or defer suite.TearDown()) so teardown always
runs, and apply the same change to the other setup call around lines 108-109
that also instantiates a suite.
🪄 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: b7de2860-a302-4f30-92b1-b99f42d129db

📥 Commits

Reviewing files that changed from the base of the PR and between 10b21bb and d1a0caa.

📒 Files selected for processing (5)
  • packages/go/analysis/azure/azure_integration_suite_test.go
  • packages/go/analysis/azure/azure_integration_test.go
  • packages/go/analysis/azure/post.go
  • packages/go/analysis/azure/post_test.go
  • packages/go/graphschema/azure/roles.go

Comment thread packages/go/analysis/azure/azure_integration_test.go Outdated
Comment thread packages/go/analysis/azure/post.go
Copy link
Copy Markdown
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)
packages/go/analysis/azure/azure_integration_test.go (1)

77-82: Return transaction fetch errors instead of asserting and continuing.

At Line 81, assert.Nil(t, err) allows the callback to continue with invalid state and returns nil, which can blur the root cause.

🔧 Fail-fast transaction callback
 		addOwnerEdges, err := ops.FetchRelationships(tx.Relationships().Filterf(func() graph.Criteria {
 			return query.Kind(query.Relationship(), graphAzure.AddOwner)
 		}))
-		assert.Nil(t, err)
+		if err != nil {
+			return err
+		}
 		assert.Len(t, addOwnerEdges, 8)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/go/analysis/azure/azure_integration_test.go` around lines 77 - 82,
The transaction callback passed to GraphDB.ReadTransaction uses assert.Nil(t,
err) after calling ops.FetchRelationships which lets the callback continue on
failure; change the callback to fail fast by returning the encountered error (or
wrapping it) instead of asserting and continuing. Specifically, in the function
passed to GraphDB.ReadTransaction, replace the assert.Nil(t, err) after
ops.FetchRelationships(tx.Relationships().Filterf(...)) with a direct return of
err (or use require on the test caller and return err), so the ReadTransaction
call sees and propagates the fetch error related to graphAzure.AddOwner.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/go/analysis/azure/azure_integration_test.go`:
- Around line 73-75: The test risks a panic when dereferencing
postProcessingStats.RelationshipsCreated[graphAzure.AddOwner]; change the
assertion flow so you first ensure the map entry exists and is non-nil before
dereferencing—either replace assert.NotNil with require.NotNil for
postProcessingStats.RelationshipsCreated[graphAzure.AddOwner] or explicitly
fetch the entry (v, ok :=
postProcessingStats.RelationshipsCreated[graphAzure.AddOwner]) and
assert.True(ok) and assert.NotNil(v) before calling assert.Equal on int(*v);
reference the symbols postProcessingStats.RelationshipsCreated and
graphAzure.AddOwner to locate the check in azure_integration_test.go.

---

Nitpick comments:
In `@packages/go/analysis/azure/azure_integration_test.go`:
- Around line 77-82: The transaction callback passed to GraphDB.ReadTransaction
uses assert.Nil(t, err) after calling ops.FetchRelationships which lets the
callback continue on failure; change the callback to fail fast by returning the
encountered error (or wrapping it) instead of asserting and continuing.
Specifically, in the function passed to GraphDB.ReadTransaction, replace the
assert.Nil(t, err) after ops.FetchRelationships(tx.Relationships().Filterf(...))
with a direct return of err (or use require on the test caller and return err),
so the ReadTransaction call sees and propagates the fetch error related to
graphAzure.AddOwner.
🪄 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: cf0a1a1f-f679-43b6-8454-9ca4aa0ad2a1

📥 Commits

Reviewing files that changed from the base of the PR and between d1a0caa and 69f3b8d.

📒 Files selected for processing (1)
  • packages/go/analysis/azure/azure_integration_test.go

Comment thread packages/go/analysis/azure/azure_integration_test.go Outdated
Copy link
Copy Markdown
Contributor

@StephenHinck StephenHinck left a comment

Choose a reason for hiding this comment

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

This appears to utilize our legacy "delete -> recreate" pattern for post-processed edges. We now have the delta-change-analysis pattern utilized by the the user role assignment post processing package. See https://github.com/SpecterOps/BloodHound/blob/main/packages/go/analysis/azure/post.go#L927 for reference.

Copy link
Copy Markdown
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.

🧹 Nitpick comments (1)
packages/go/analysis/azure/post.go (1)

751-753: Consider graceful termination on Submit failure for consistency.

The error handling here differs from addSecret (lines 680-681) which returns nil on Submit failure, allowing graceful termination when context is cancelled. This explicit error approach is more defensive but creates slightly inconsistent behavior.

♻️ Optional: Match the graceful termination pattern
 				if !sink.Submit(ctx, nextJob) {
-					return fmt.Errorf("unable to submit to channel in postAzureAddOwner")
+					return nil
 				}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/go/analysis/azure/post.go` around lines 751 - 753, In
postAzureAddOwner, make Submit failure mirror the graceful termination used in
addSecret: when sink.Submit(ctx, nextJob) returns false, return nil (allowing
clean exit when ctx is cancelled) instead of returning a formatted error; update
the failure branch in postAzureAddOwner to match addSecret's behavior so
cancellation/closed-channel shutdowns are handled consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/go/analysis/azure/post.go`:
- Around line 751-753: In postAzureAddOwner, make Submit failure mirror the
graceful termination used in addSecret: when sink.Submit(ctx, nextJob) returns
false, return nil (allowing clean exit when ctx is cancelled) instead of
returning a formatted error; update the failure branch in postAzureAddOwner to
match addSecret's behavior so cancellation/closed-channel shutdowns are handled
consistently.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro

Run ID: cfb87f74-ee8c-42bc-a4e9-b401ef1fca60

📥 Commits

Reviewing files that changed from the base of the PR and between 1deaf8e and e3bea8d.

📒 Files selected for processing (2)
  • packages/go/analysis/azure/azure_integration_test.go
  • packages/go/analysis/azure/post.go

@bsheth711 bsheth711 force-pushed the BED-4754-azaddowner-relationships branch from e3bea8d to 4e17c6b Compare April 15, 2026 18:03
Copy link
Copy Markdown
Contributor

@StephenHinck StephenHinck left a comment

Choose a reason for hiding this comment

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

Thank you very much for that refactor to use DCA. I did pull it down and validate it runs as expected.

@bsheth711 bsheth711 force-pushed the BED-4754-azaddowner-relationships branch from 4e17c6b to 48eed3b Compare April 20, 2026 17:00
@bsheth711 bsheth711 merged commit 99b761f into main Apr 20, 2026
12 checks passed
@bsheth711 bsheth711 deleted the BED-4754-azaddowner-relationships branch April 20, 2026 17:29
@github-actions github-actions bot locked and limited conversation to collaborators Apr 20, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

go Pull requests that update go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants