Skip to main content

Documentation Index

Fetch the complete documentation index at: https://bloodhound.specterops.io/llms.txt

Use this file to discover all available pages before exploring further.

Applies to BloodHound Enterprise only
You can complete the steps on this page manually following the instructions below, or use the PowerShell script in the Scripted Configuration section to automate the entire process (except certificate upload).

Create the AzureHound Enterprise app

  1. Log into the Microsoft Entra admin center as a user with the Global Administrator role, or the following less privileged roles:
  1. In the left menu, select App registrations.
  2. Click New registration.
  3. In the Name field, give the application an identifying name in your organization. Make sure the supported account type is set to the “Accounts in this organizational directory only (Single tenant)” option. A URI is not required. Then click Register.
  4. In the Overview menu, copy the Application (client) ID and Directory (tenant) ID to be used later in AzureHound Enterprise Local Configuration.
  5. Continue to the next section: “Grant Microsoft Graph Permissions”.

Grant Microsoft Graph Permissions

  1. In the AzureHound application, select API Permissions.
  2. Remove the default User.Read delegated permission from the application, as it is not needed for data collection. Confirm the operation in the dialog window that appears after selecting the Remove permission option from the context menu.
  3. Select Add a permission.
  4. Click on Microsoft Graph.
  5. Select Application permissions.
  6. Search for and check the box next to each of the following Microsoft Graph application permissions. See AzureHound Data and Permissions: Entra ID for details on the least-privilege approach:
    PermissionPurpose
    User.Read.AllEnumerate users and user properties
    GroupMember.Read.AllEnumerate groups, group owners, and group members
    Application.Read.AllEnumerate applications, app owners, federated identity credentials, service principals, service principal owners, and app role assignments
    Device.Read.AllEnumerate devices and device registered owners
    Organization.Read.AllRead tenant/organization information
    RoleManagement.Read.DirectoryEnumerate Entra ID role definitions, role assignments, PIM eligible role assignments, and role management policy assignments
    AdministrativeUnit.Read.AllPopulate Administrative Unit properties on AU-scoped role assignments. Collected by AzureHound, not processed by BloodHound yet.
    AuditLog.Read.All(Optional) Collect signInActivity (last sign-in timestamps) on user objects. AzureHound gracefully degrades if this permission is missing.
  7. In the bottom of the window, select Add permissions.
  8. Click on Grant admin consent for <your_tenant_name>.
  9. Click Yes on the confirmation dialog.
  10. After being redirected to API Permissions again, you should see the Status column listing all permissions as Granted.
  11. Continue to the next section: “Add application authentication certificate”.

Add application authentication certificate

This section requires you have authentication material. We highly recommend using certificate-based authentication. If you do not already have a certificate created, follow the article AzureHound Enterprise Local Configuration and then return back here.
  1. Select the Certificates & secrets section on the left.
  2. Click on Certificates.
  3. Click Upload certificate.
  4. Locate the cert.pem file created during AzureHound setup (either on your own, or utilizing the instructions at AzureHound Enterprise Local Configuration).
  5. Click the folder icon and locate the “cert.pem” file. Add a description if desired.
  6. In the bottom of the window, select Add.
  7. Continue to the next section to optionally configure application branding.

Configure application branding (optional)

Note: All steps in this section are optional and do not affect the functionality of the collector.
  1. Download the AzureHound Enterprise icon to your computer.
  2. Still on the AzureHound application in the Entra ID admin center, open the Branding & properties section.
  3. Click the Select a file button and browse to the file you previously downloaded.
  4. Provide a human-readable Name for the application, e.g., BloodHound Enterprise Collector (AzureHound).
  5. Type your BloodHound Enterprise tenant URL into the Home page URL field. This is for record keeping only.
  6. Click the Save button and review the results.
  7. Continue to the next section to create and assign a custom role in Azure Resource Manager

Create and assign custom AzureHound Reader role in Azure Resource Manager

If you don’t have any management groups, you can skip this section. However, AzureHound will log a warning during each collection indicating it cannot collect management group data.Alternatively, you can create your Tenant Root Group by following the prompts in the Azure portal. This ensures visibility if another administrator begins using subscriptions in the future.
  1. Log into the Azure portal as a user with the User Access Administrator role.
  2. Search for and select the Management groups item in the top search bar.
  3. Select Tenant Root Group.
  4. Select Access control (IAM).
  5. Click Add, then Add custom role.
  6. Download azurehound-reader-role.json, open it in a text editor, and replace <tenantRootGroupId> with your Tenant Root Management Group ID (this is your Entra ID tenant ID).
  7. In Basics > File, upload the edited file and click Review + create.
    See AzureHound Data Collection and Permissions: Azure Resource Manager for details on each least-privilege permission in the AzureHound Reader role.
  8. Review the role and click Create at the bottom of the page.
  9. Back in Access control (IAM), click Add, then Add role assignment.
  10. Search for the AzureHound Reader role and select it.
  11. Click Members.
  12. Click Select members.
  13. Search for and click on your previously created service principal.
  14. Validate the principal selected, then click Select.
  15. Click the tab Review + Assign.
  16. Click Review + Assign at the bottom of the page.
  17. Confirm the role is present by refreshing this view. You may need to alter the filter to see this role.
  18. Continue to Run and Upgrade AzureHound (Windows, Docker, or Kubernetes)

Scripted configuration

As an alternative to the manual steps described in this document, you can use PowerShell to automate the entire configuration process. The script below registers AzureHound in Entra ID, creates the least-privilege Azure Reader role, and assigns it to the service principal. The script is idempotent—it safely handles re-runs by detecting existing resources and updating them as needed, preventing duplicate role definitions or assignments.
After running this script, follow the Add application authentication certificate section to complete the setup.
<#
.SYNOPSIS
Registers the Azure Data Exporter for BloodHound Enterprise (AzureHound)
as an application in Entra ID.

.DESCRIPTION
This script registers the Azure Data Exporter for BloodHound Enterprise (AzureHound)
as an application in Microsoft Entra ID and Azure, including all necessary read permissions
and a least-privilege custom Azure Reader role. The script is idempotent: on subsequent runs,
it detects existing role definitions and assignments, updates them if needed, and skips creation
to prevent duplicates.

The required PowerShell modules can be installed
from the PowerShell Gallery using the following command:

Install-Module -Scope AllUsers -Repository PSGallery -Force -Name @(
    Microsoft.Graph.Applications,
    Microsoft.Graph.Authentication,
    Az.Resources,
    Az.Accounts
)

.NOTES
Version: 1.1
#>

#Requires -Version 5
#Requires -Modules Microsoft.Graph.Applications,Microsoft.Graph.Authentication
#Requires -Modules Az.Resources,Az.Accounts

#region Entra ID

# Connect to Microsoft Entra ID through the Microsoft Graph API
# Note: The -TenantId parameter is also required when using an External ID.
Connect-MgGraph -NoWelcome -ContextScope Process -Scopes @(
   'User.Read',
   'Application.ReadWrite.All',
   'AppRoleAssignment.ReadWrite.All'
)

# Register the AzureHound application
[string] $appName = 'BloodHound Enterprise Collector'
[string] $appDescription =
  'Azure Data Exporter for BloodHound Enterprise (AzureHound)'
# TODO: Optionally provide the actual URL your BloodHound Enterprise tenant
[string] $homePage = 'https://specterops.io/bloodhound-enterprise'
[hashtable] $infoUrls = @{
    MarketingUrl      = 'https://specterops.io/bloodhound-enterprise'
    TermsOfServiceUrl = 'https://specterops.io/terms-of-service'
    PrivacyStatementUrl = 'https://specterops.io/privacy-policy'
    SupportUrl = 'https://bloodhound.specterops.io/'
}
[hashtable] $webUrls = @{
    HomePageUrl = $homePage
}

[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphApplication] $registeredApp =
   New-MgApplication -DisplayName $appName `
                     -Description $appDescription `
                     -Info $infoUrls `
                     -Web $webUrls `
                     -SignInAudience 'AzureADMyOrg'

# Configure the application logo
[string] $logoUrl =
  'https://bloodhound.specterops.io/assets/icons/entra-bhe-app-icon.png'
[string] $tempLogoPath = New-TemporaryFile

Invoke-WebRequest -Uri $logoUrl `
                  -OutFile $tempLogoPath `
                  -UseBasicParsing `
                  -ErrorAction Stop
try {
    Set-MgApplicationLogo -ApplicationId $registeredApp.Id `
                          -ContentType 'image/png' `
                          -InFile $tempLogoPath
}
finally {
    # Delete the local copy of the logo from temp
    Remove-Item -Path $tempLogoPath
}

# Make sure the app instance property lock is enabled
Update-MgApplication `
  -ApplicationId $registeredApp.Id `
  -ServicePrincipalLockConfiguration @{
    IsEnabled = $true
    AllProperties = $true
}

# Create the associated service principal object
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphServicePrincipal] $servicePrincipal =
   New-MgServicePrincipal -DisplayName $appName `
                          -AppId $registeredApp.AppId `
                          -AccountEnabled `
                          -ServicePrincipalType Application `
                          -Notes $appDescription `
                          -Homepage $homePage `
                          -Tags 'WindowsAzureActiveDirectoryIntegratedApp','HideApp'

# Fetch the Microsoft Graph applicaton ID,
# which should be 00000003-0000-0000-c000-000000000000
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphServicePrincipal] $microsoftGraph =
    Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Graph'"

# Define the least-privilege Microsoft Graph application permissions
# See: /collect-data/azurehound-data-permissions#entra-id
[string[]] $requiredPermissions = @(
    'User.Read.All'
    'GroupMember.Read.All'
    'Application.Read.All'
    'Device.Read.All'
    'Organization.Read.All'
    'RoleManagement.Read.Directory'
    'AdministrativeUnit.Read.All'
    'AuditLog.Read.All'  # Optional: for signInActivity on user objects
)

# Resolve each permission name to its AppRole definition
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphAppRole[]] $appRoles = $requiredPermissions | ForEach-Object {
    [Microsoft.Graph.PowerShell.Models.IMicrosoftGraphAppRole] $role =
        $microsoftGraph.AppRoles | Where-Object Value -eq $PSItem
    if (-not $role) { throw "Permission '$PSItem' not found in Microsoft Graph app roles" }
    $role
}

# Transform the app roles to the format required by Update-MgApplication
[hashtable[]] $resourceAccess = $appRoles | ForEach-Object {
    @{ id = $PSItem.Id; type = 'Role' }
}

# Delegate the required API permissions
Update-MgApplication -ApplicationId $registeredApp.Id -RequiredResourceAccess @{
    ResourceAppId = $microsoftGraph.AppId # 00000003-0000-0000-c000-000000000000
    ResourceAccess = $resourceAccess
}

# Admin-consent each permission
foreach ($access in $resourceAccess) {
    New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $servicePrincipal.Id `
                                            -PrincipalId $servicePrincipal.Id `
                                            -ResourceId $microsoftGraph.Id `
                                            -AppRoleId $access.id
}

# Get the environment-specific Microsoft Graph API endpoint
# Azure Global: https://graph.microsoft.com
# Azure USGov:  https://graph.microsoft.us
[string] $graphEndpoint =
    (Get-MgEnvironment -Name (Get-MgContext).Environment).GraphEndpoint

# Fetch the info about the current user
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphDirectoryObject] $currentUser =
    Invoke-MgGraphRequest -Method GET -Uri '/v1.0/me'

# OData IDs need to be used when assigning application ownership,
# e.g., https://graph.microsoft.com/v1.0/users/{bca3617a-4c54-45eb-9a32-744c1938242e}
[string] $currentUserOdataId = "$graphEndpoint/v1.0/users/{$($currentUser.Id)}"

# Assign the current user as the application object owner
New-MgApplicationOwnerByRef -ApplicationId $registeredApp.Id `
                            -OdataId $currentUserOdataId

# Assign the current user as the service principal owner
New-MgServicePrincipalOwnerByRef -ServicePrincipalId $servicePrincipal.Id `
                                 -OdataId $currentUserOdataId

# Sign out from Microsoft Graph
Disconnect-MgGraph | Out-Null

#endregion Entra ID

#region Azure

# Optionally enable browser-based login on Windows 10 and later
Update-AzConfig -EnableLoginByWam $false

# Authenticate against Azure Resource Manager
Connect-AzAccount -Environment AzureCloud -Scope Process

# Load the AzureHound Reader custom role definition
[string] $roleDefinitionJson = @'
{
    "properties": {
        "roleName": "AzureHound Reader",
        "description": "Least-privilege read-only role for AzureHound data collection from Azure Resource Manager.",
        "assignableScopes": [
            "/providers/Microsoft.Management/managementGroups/<tenantRootGroupId>"
        ],
        "permissions": [
            {
                "actions": [
                    "Microsoft.Resources/subscriptions/resourceGroups/read",
                    "Microsoft.Authorization/roleAssignments/read",
                    "Microsoft.Management/managementGroups/read",
                    "Microsoft.Management/managementGroups/descendants/read",
                    "Microsoft.Compute/virtualMachines/read",
                    "Microsoft.Compute/virtualMachineScaleSets/read",
                    "Microsoft.KeyVault/vaults/read",
                    "Microsoft.Web/sites/read",
                    "Microsoft.ContainerRegistry/registries/read",
                    "Microsoft.ContainerService/managedClusters/read",
                    "Microsoft.Automation/automationAccounts/read",
                    "Microsoft.Logic/workflows/read",
                    "Microsoft.Storage/storageAccounts/read",
                    "Microsoft.Storage/storageAccounts/blobServices/containers/read"
                ],
                "notActions": [],
                "dataActions": [],
                "notDataActions": []
            }
        ]
    }
}
'@

if (-not (Test-Path -Path 'variable:servicePrincipal')) {
    # Fetch the service principal if the Azure part of the script is executed independently
    [Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.IMicrosoftGraphServicePrincipal] $servicePrincipal =
        Get-AzADServicePrincipal -DisplayName 'BloodHound Enterprise Collector'
}

# Fetch the Tenant Root Group
[guid] $currentTenantId = (Get-AzContext).Tenant.Id
[Microsoft.Azure.Commands.Resources.Models.ManagementGroups.PSManagementGroup] $rootManagementGroup =
    Get-AzManagementGroup -GroupName $currentTenantId

# New-AzRoleDefinition expects a flat structure, not the nested `properties` wrapper
# used by the ARM portal upload format, so we must flatten it before passing it in.
[PSCustomObject] $roleDefinition = $roleDefinitionJson | ConvertFrom-Json

$flatRole = [PSCustomObject]@{
    Name             = $roleDefinition.properties.roleName
    Description      = $roleDefinition.properties.description
    Actions          = $roleDefinition.properties.permissions[0].actions
    NotActions       = $roleDefinition.properties.permissions[0].notActions
    DataActions      = $roleDefinition.properties.permissions[0].dataActions
    NotDataActions   = $roleDefinition.properties.permissions[0].notDataActions
    AssignableScopes = @($roleDefinition.properties.assignableScopes |
                         ForEach-Object { $_ -replace '<tenantRootGroupId>', $currentTenantId })
}

[string] $flatRolePath = New-TemporaryFile
$flatRole | ConvertTo-Json -Depth 10 | Set-Content -Path $flatRolePath

# Create the custom AzureHound Reader role
try {
    $existingRole = Get-AzRoleDefinition -Name 'AzureHound Reader' -ErrorAction SilentlyContinue
    
    if ($existingRole) {
        $existingRole.Actions = $flatRole.Actions
        $existingRole.NotActions = $flatRole.NotActions
        $existingRole.DataActions = $flatRole.DataActions
        $existingRole.NotDataActions = $flatRole.NotDataActions
        $existingRole.Description = $flatRole.Description
        $existingRole.AssignableScopes = $flatRole.AssignableScopes
        [Microsoft.Azure.Commands.Resources.Models.Authorization.PSRoleDefinition] $azureHoundReaderRole =
            Set-AzRoleDefinition -Role $existingRole
    }
    else {
        [Microsoft.Azure.Commands.Resources.Models.Authorization.PSRoleDefinition] $azureHoundReaderRole =
            New-AzRoleDefinition -InputFile $flatRolePath
    }
}
finally {
    Remove-Item -Path $flatRolePath
}

# Assign the AzureHound Reader role at the Tenant Root Group
$existingRoleAssignment = Get-AzRoleAssignment -ObjectId $servicePrincipal.Id `
                                               -Scope $rootManagementGroup.Id `
                                               -RoleDefinitionId $azureHoundReaderRole.Id `
                                               -ErrorAction SilentlyContinue

if ($null -ne $existingRoleAssignment) {
    [Microsoft.Azure.Commands.Resources.Models.Authorization.PSRoleAssignment] $readerRoleAssignment = $existingRoleAssignment
}
else {
    [Microsoft.Azure.Commands.Resources.Models.Authorization.PSRoleAssignment] $readerRoleAssignment =
        New-AzRoleAssignment -ObjectId $servicePrincipal.Id `
                             -Scope $rootManagementGroup.Id `
                             -RoleDefinitionId $azureHoundReaderRole.Id
}

# Sign out from Azure Resource Manager
Disconnect-AzAccount -Scope Process

#endregion Azure

#region Summary

Write-Host ''
Write-Host '=== Configuration Summary ===' -ForegroundColor Cyan
Write-Host ''
Write-Host 'Entra ID app registration:' -ForegroundColor Green
Write-Host "  Application Name:  $appName"
Write-Host "  Application ID:    $($registeredApp.AppId)"
Write-Host "  Object ID:         $($registeredApp.Id)"
Write-Host "  Tenant ID:         $currentTenantId"
Write-Host "  Owner:             $($currentUser.UserPrincipalName)"
Write-Host ''
Write-Host 'Microsoft Graph permissions (admin-consented):' -ForegroundColor Green
$requiredPermissions | ForEach-Object { Write-Host "  - $_" }
Write-Host ''

Write-Host 'Azure Resource Manager:' -ForegroundColor Green
Write-Host "  Custom Role:       $($azureHoundReaderRole.Name)"
Write-Host "  Role ID:           $($azureHoundReaderRole.Id)"
Write-Host "  Assigned Scope:    $($rootManagementGroup.Id)"
Write-Host ''

Write-Host 'Remaining manual step:' -ForegroundColor Yellow
Write-Host '  Upload an authentication certificate to the app registration.'
Write-Host '  See: https://bloodhound.specterops.io/install-data-collector/install-azurehound/azure-configuration#add-application-authentication-certificate'
Write-Host ''

#endregion Summary