Skip to content

Added support for fetching delegated admin partner information#199

Open
Paradoxis wants to merge 1 commit into
SpecterOps:mainfrom
Paradoxis:main
Open

Added support for fetching delegated admin partner information#199
Paradoxis wants to merge 1 commit into
SpecterOps:mainfrom
Paradoxis:main

Conversation

@Paradoxis
Copy link
Copy Markdown

@Paradoxis Paradoxis commented May 19, 2026

This PR adds support for collection of tenants, users, groups and service principals created by delegated admin partners (DAP/GDAP) in Entra ID.

I ran into an issue at a client where AzureHound failed to list information linked to role attachments and thought I'd share my solution with you guys.

I'm not sure if this really is the way to go since it's quite hacky, as you can't just easily go and fetch information from other tenants without going through some $expand hoops (see the big comment in the code itself). This implementation is quite slow and could break on large environments.

I was originally planning on adding a Partner node but I think it would be better to add a custom relationship (i.e.: AZPartnerOf / AZManagedBy) in Bloodhound itself that shows the tenant has a trust relationship with the other tenants.

If you guys have any other suggestions how I could better implement this I'd be happy to change stuff up. Feel free to edit anything if you want

Summary by CodeRabbit

  • New Features

    • Added command to list delegated Azure AD partners with full tenant and role assignment details.
    • Extended tenant enumeration to automatically include partner tenants.
    • Added tenant information lookup by ID capability.
  • Updates

    • Updated role assignments queries to use the beta Graph API version for enhanced functionality.

Review Change Stack

@github-actions
Copy link
Copy Markdown


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

Walkthrough

This PR adds Azure AD partner enumeration support to AzureHound. It introduces a new partners subcommand that streams delegated partner tenants and their associated principals from Entra ID, backed by extended client methods and new data models to capture partner and principal metadata.

Changes

Partner Enumeration Feature

Layer / File(s) Summary
Data models for partner and external tenant support
models/azure/partner.go, models/tenant.go, models/azure/unified_role_assignment.go
New Partner struct captures delegated partner attributes (tenant ID, company info, URLs, support contacts, contract type, role IDs). Tenant model gains External boolean flag. UnifiedRoleAssignment gains PrincipalOrganizationId field for filtering and principal expansion.
Client interface and Graph API methods
client/client.go, client/tenants.go, client/partners.go, client/role_assignments.go
AzureGraphClient interface extended with GetAzureADTenantInfoById and ListAzureADPartners method signatures. Implementations fetch tenant details by ID and stream partner objects from /directory/partners endpoint. ListAzureADRoleAssignments switched to beta Graph API version to enable principal expansion.
Partners command and integration
cmd/list-partners.go, cmd/list-azure-ad.go, cmd/list-tenants.go
New list-partners subcommand streams external partner tenants and their role-assigned principals via two-phase enumeration: collect and emit tenants, then enumerate and deduplicate principals with type coercion and tenant metadata. Integrated into listAllAD multiplexed output. Tenant command marks emitted tenants with External: false for consistency.

Sequence Diagram

sequenceDiagram
  participant partnersCmd as partners command
  participant client as AzureGraphClient
  participant output as output channel
  participant caller as caller
  partnersCmd->>client: ListAzureADPartners(ctx, params)
  client->>client: getAzureObjectList goroutine
  client->>output: stream Partner items
  partnersCmd->>client: GetAzureADTenantInfoById(ctx, tenantId)
  client->>output: emit normalized AZTenant
  partnersCmd->>client: ListAzureADRoleDefinitions
  client->>output: stream role definitions
  partnersCmd->>client: ListAzureADRoleAssignments(ctx, filter, expand)
  client->>output: stream principal assignments
  partnersCmd->>output: close channel on completion
  output->>caller: stream AZTenant and AzureWrapper events
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A partnership unfolds, new paths arise,
Partners dance through Azure's enterprise skies,
Enumerating tenants, principals aligned,
Graph API calls in harmony combined,
External flags and beta versioning bright,
AzureHound's capabilities take flight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely summarizes the main objective of the changeset: adding support for fetching delegated admin partner information.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@Paradoxis Paradoxis changed the title Added support for fetching delegated partner admin information Added support for fetching delegated admin partner information May 19, 2026
@Paradoxis
Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

Copy link
Copy Markdown

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

🧹 Nitpick comments (1)
models/tenant.go (1)

25-25: ⚡ Quick win

Keep external consistently present in tenant JSON.

On Line 25, omitempty drops external for internal tenants (false), creating mixed shapes between internal and delegated tenant objects.

♻️ Proposed change
-	External  bool `json:"external,omitempty"`
+	External  bool `json:"external"`
🤖 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 `@models/tenant.go` at line 25, The JSON tag for the Tenant struct's External
field (External bool) currently uses `omitempty`, causing the key to be omitted
when false; remove `omitempty` from the `json` tag on the External field in the
Tenant struct so the `external` key is always emitted (i.e., change `External 
bool `json:"external,omitempty"` to `External  bool `json:"external"`), then run
gofmt/build to ensure no other code relies on the missing key.
🤖 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 `@client/client.go`:
- Around line 195-196: The AzureClient interface was expanded (e.g.,
GetAzureADTenantInfoById and other AzureGraphClient-related additions) and
existing mocks no longer implement client.AzureClient; regenerate the mocks so
mocks.MockAzureClient matches the updated interface. Run your mock-generation
tool (e.g., mockgen or the project's generator) targeting the client.AzureClient
interface (including methods like GetAzureADTenantInfoById and any
AzureGraphClient methods) to overwrite/update mocks.MockAzureClient, then run
tests to ensure the mock satisfies the new interface.

In `@cmd/list-partners.go`:
- Around line 87-89: The tenant lookup error handler logs the failure but then
continues and adds a zero-value tenant into partnerTenants; change the error
branch in the loop that calls the external tenant retrieval (the block that logs
using log.Error with "failed to retrieve tenant information for external
partner" and references partner.Ok.CompanyName / partner.Ok.PartnerTenantId) to
return early from that iteration (e.g., skip inserting into partnerTenants) by
continuing the loop after logging. Apply the same fix to the similar
error-handling blocks later in the same loop (the region covering lines ~91-105)
so that when err != nil you do not populate partnerTenants with an empty tenant
entry.
- Around line 212-214: The goroutine currently uses break when
pipeline.SendAny(ctx.Done(), out, AzureWrapper{Kind: kind, Data: data}) returns
false, which only exits the inner role-assignment loop and allows the outer loop
to continue; change this to stop the goroutine immediately (e.g., return from
the goroutine) when SendAny indicates cancellation so no further iterations or
sends occur. Locate the call to pipeline.SendAny and replace the inner-loop-only
break with an immediate function return (or otherwise break out of all loops) to
ensure that on ctx cancellation no more AzureWrapper sends or outer-loop work is
performed. Ensure any necessary deferred cleanup still runs after returning.

---

Nitpick comments:
In `@models/tenant.go`:
- Line 25: The JSON tag for the Tenant struct's External field (External bool)
currently uses `omitempty`, causing the key to be omitted when false; remove
`omitempty` from the `json` tag on the External field in the Tenant struct so
the `external` key is always emitted (i.e., change `External  bool
`json:"external,omitempty"` to `External  bool `json:"external"`), then run
gofmt/build to ensure no other code relies on the missing key.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f25eb27b-af43-4995-8c4b-2c5d49836a3b

📥 Commits

Reviewing files that changed from the base of the PR and between a161a8a and 15f36b7.

📒 Files selected for processing (10)
  • client/client.go
  • client/partners.go
  • client/role_assignments.go
  • client/tenants.go
  • cmd/list-azure-ad.go
  • cmd/list-partners.go
  • cmd/list-tenants.go
  • models/azure/partner.go
  • models/azure/unified_role_assignment.go
  • models/tenant.go

Comment thread client/client.go
Comment on lines +195 to 196
GetAzureADTenantInfoById(ctx context.Context, tenantId string) (azure.Tenant, error)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Regenerate mocks after expanding AzureGraphClient.

The interface changes at Line 195 and Line 206 currently break type-checking in tests because mocks.MockAzureClient no longer implements client.AzureClient.

Also applies to: 206-206

🤖 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 `@client/client.go` around lines 195 - 196, The AzureClient interface was
expanded (e.g., GetAzureADTenantInfoById and other AzureGraphClient-related
additions) and existing mocks no longer implement client.AzureClient; regenerate
the mocks so mocks.MockAzureClient matches the updated interface. Run your
mock-generation tool (e.g., mockgen or the project's generator) targeting the
client.AzureClient interface (including methods like GetAzureADTenantInfoById
and any AzureGraphClient methods) to overwrite/update mocks.MockAzureClient,
then run tests to ensure the mock satisfies the new interface.

Comment thread cmd/list-partners.go
Comment on lines +87 to +89
if err != nil {
log.Error(err, "failed to retrieve tenant information for external partner", "companyName", partner.Ok.CompanyName, "partnerTenantId", partner.Ok.PartnerTenantId)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Skip emission when external tenant lookup fails.

After the error at Line 87, execution continues and emits a zero-value tenant (e.g., Id becomes /tenants/) and inserts an empty-key entry into partnerTenants.

🐛 Proposed fix
 			externalTenant, err := client.GetAzureADTenantInfoById(ctx, partner.Ok.PartnerTenantId)
 			if err != nil {
 				log.Error(err, "failed to retrieve tenant information for external partner", "companyName", partner.Ok.CompanyName, "partnerTenantId", partner.Ok.PartnerTenantId)
+				continue
 			}

Also applies to: 91-105

🤖 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/list-partners.go` around lines 87 - 89, The tenant lookup error handler
logs the failure but then continues and adds a zero-value tenant into
partnerTenants; change the error branch in the loop that calls the external
tenant retrieval (the block that logs using log.Error with "failed to retrieve
tenant information for external partner" and references partner.Ok.CompanyName /
partner.Ok.PartnerTenantId) to return early from that iteration (e.g., skip
inserting into partnerTenants) by continuing the loop after logging. Apply the
same fix to the similar error-handling blocks later in the same loop (the region
covering lines ~91-105) so that when err != nil you do not populate
partnerTenants with an empty tenant entry.

Comment thread cmd/list-partners.go
Comment on lines +212 to +214
if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{Kind: kind, Data: data}); !ok {
break
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Return on canceled output instead of only breaking inner loop.

At Line 212, if pipeline.SendAny fails, break only exits the current role-assignment loop. The goroutine can continue with outer iterations and extra calls despite cancellation.

🐛 Proposed fix
 				if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{Kind: kind, Data: data}); !ok {
-					break
+					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
if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{Kind: kind, Data: data}); !ok {
break
}
if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{Kind: kind, Data: data}); !ok {
return
}
🤖 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/list-partners.go` around lines 212 - 214, The goroutine currently uses
break when pipeline.SendAny(ctx.Done(), out, AzureWrapper{Kind: kind, Data:
data}) returns false, which only exits the inner role-assignment loop and allows
the outer loop to continue; change this to stop the goroutine immediately (e.g.,
return from the goroutine) when SendAny indicates cancellation so no further
iterations or sends occur. Locate the call to pipeline.SendAny and replace the
inner-loop-only break with an immediate function return (or otherwise break out
of all loops) to ensure that on ctx cancellation no more AzureWrapper sends or
outer-loop work is performed. Ensure any necessary deferred cleanup still runs
after returning.

@Paradoxis
Copy link
Copy Markdown
Author

recheck

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant