Skip to content

feat: collect Microsoft Entra Domain Services resources - BED-9245 - #206

Open
martinsohn wants to merge 4 commits into
mainfrom
microsoft-entra-domain-services
Open

feat: collect Microsoft Entra Domain Services resources - BED-9245#206
martinsohn wants to merge 4 commits into
mainfrom
microsoft-entra-domain-services

Conversation

@martinsohn

@martinsohn martinsohn commented Aug 11, 2026

Copy link
Copy Markdown

Description

Adds Microsoft Entra Domain Services resource collection to az-rm, including accessible Microsoft.AAD/domainServices resources and their direct resource-scope role assignments.

Changes include:

  • Add AZEntraDS and AZEntraDSRoleAssignment output kinds and models.
  • Enumerate Entra DS resources and direct resource-scope role assignments.
  • Collect tenant, domain, synchronization, security, replica, and Secure LDAP configuration.
  • Normalize resource and identity identifiers for BloodHound ingest.
  • Add unit coverage for enumeration, marshaling, output kinds, and role assignments.

Motivation and Context

BloodHound requires Azure resource, configuration, and authorization data to anchor the Entra DS graph and model hybrid paths across Azure Resource Manager, Microsoft Entra ID, and the managed Active Directory domain.

This PR is part of: BED-9245

How Has This Been Tested?

  • go test ./...
  • Build AzureHound from this PR head.
  • Run az-rm against the lab tenant and confirm the expected records.

Screenshots (if appropriate):

N/A

Types of changes

  • Chore
  • Bug fix
  • New feature
  • Breaking change

Summary by CodeRabbit

  • New Features

    • Added support for listing Microsoft Entra Domain Services across Azure subscriptions.
    • Added a command to enumerate Domain Services and their role assignments.
    • Included Domain Service details, subscription metadata, resource groups, and role assignment information in output.
    • Added resource types for Domain Services and Domain Service role assignments.
  • Bug Fixes

    • Sensitive LDAPS certificate data is excluded from serialized output.
  • Tests

    • Added coverage for Domain Service listing, role assignments, metadata, and serialization.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The change adds Azure Domain Services models, Resource Manager retrieval, CLI commands, role-assignment collection, Azure RM pipeline integration, and related tests.

Changes

Azure Domain Services

Layer / File(s) Summary
Domain Service models
enums/kind.go, models/azure/domain_service.go, models/domain-service.go, models/domain-service_test.go
Adds Azure Domain Services models, resource-group helpers, output metadata, identifier normalization, and JSON serialization tests.
Resource Manager retrieval
client/client.go, client/domain_services.go, client/mocks/client.go
Adds the client method, asynchronous Resource Manager retrieval, and GoMock support.
Domain Services listing command
cmd/list-domain-services.go, cmd/list-domain-services_test.go
Adds concurrent subscription enumeration, domain-service wrapping, output streaming, and command tests.
Azure RM pipeline integration
cmd/list-azure-rm.go
Adds Domain Services and role-assignment streams to subscription fan-out and final output multiplexing.
Domain Service role assignments
cmd/list-domain-service-role-assignments.go, cmd/list-domain-service-role-assignments_test.go
Adds concurrent role-assignment retrieval, direct-scope filtering, output handling, and tests.

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

Sequence Diagram(s)

sequenceDiagram
  participant DomainServicesCommand
  participant SubscriptionStream
  participant AzureResourceManagerClient
  participant DomainServiceStream
  participant OutputStream
  DomainServicesCommand->>SubscriptionStream: enumerate subscription IDs
  SubscriptionStream->>AzureResourceManagerClient: ListAzureDomainServices(subscription ID)
  AzureResourceManagerClient-->>DomainServiceStream: stream domain-service results
  DomainServiceStream-->>OutputStream: emit DomainService records
Loading

Possibly related PRs

Suggested reviewers: ktstrader

Poem

A rabbit hops through Azure streams,
And gathers services from cloudlit dreams.
Roles are filtered, IDs align,
Channels close in orderly time.
New domain paths now safely bloom.

🚥 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: collecting Microsoft Entra Domain Services resources.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch microsoft-entra-domain-services

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@models/azure/domain_service.go`:
- Around line 40-49: Add a []DomainServiceReplicaSet field named ReplicaSets to
DomainServiceProperties with the appropriate JSON tag replicaSets, so
ListAzureDomainServices preserves API-returned replica set entries including
location and subnetId during unmarshalling.

In `@models/domain-service_test.go`:
- Around line 34-40: The domain service test fixture currently exercises only
untyped extra fields, so it does not validate typed LDAPS serialization. Update
the typed model setup used by the domain-service test to populate pfxCertificate
and pfxCertificatePassword through DomainServiceLDAPSSettings or the dedicated
API DTO, then assert that MarshalJSON omits both fields while preserving the
existing ldaps and externalAccess serialization.
🪄 Autofix

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: 93784177-286b-4056-92b4-1c8bb9e77b1a

📥 Commits

Reviewing files that changed from the base of the PR and between 23735c8 and be9117f.

📒 Files selected for processing (12)
  • client/client.go
  • client/domain_services.go
  • client/mocks/client.go
  • cmd/list-azure-rm.go
  • cmd/list-domain-service-role-assignments.go
  • cmd/list-domain-service-role-assignments_test.go
  • cmd/list-domain-services.go
  • cmd/list-domain-services_test.go
  • enums/kind.go
  • models/azure/domain_service.go
  • models/domain-service.go
  • models/domain-service_test.go

Comment on lines +40 to +49
type DomainServiceProperties struct {
TenantID string `json:"tenantId,omitempty"`
DomainName string `json:"domainName,omitempty"`
DomainConfigurationType string `json:"domainConfigurationType,omitempty"`
FilteredSync string `json:"filteredSync,omitempty"`
SyncScope string `json:"syncScope,omitempty"`
SyncApplicationID string `json:"syncApplicationId,omitempty"`
DomainSecuritySettings DomainServiceSecuritySettings `json:"domainSecuritySettings,omitempty"`
LDAPSSettings DomainServiceLDAPSSettings `json:"ldapsSettings,omitempty"`
}

Copy link
Copy Markdown

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
fd -i 'domain_service.go|domain.*service|azure.*domain' . || true
printf '%s\n' '--- symbols and references ---'
rg -n --glob '*.go' 'DomainServiceProperties|ListAzureDomainServices|replicaSets|ReplicaSets' . || true
printf '%s\n' '--- model outline ---'
if [ -f models/azure/domain_service.go ]; then
  ast-grep outline models/azure/domain_service.go
  printf '%s\n' '--- model ---'
  cat -n models/azure/domain_service.go
fi

Repository: SpecterOps/AzureHound

Length of output: 6259


🌐 Web query:

Microsoft Azure ARM template Microsoft.AAD/domainServices 2025-06-01 replicaSets location subnetId schema

💡 Result:

In the Microsoft.AAD/domainServices ARM template API version 2025-06-01, the replicaSets property is a required configuration for defining the deployment locations and associated network subnets [1][2]. Key points regarding the replicaSets schema in this version include: Structure: The replicaSets property accepts an array of objects [1]. Each object within this array must define the following: location: A string specifying the Azure region (e.g., "eastus") where the replica set is deployed [1][2]. subnetId: A string representing the resource ID of the subnet where Domain Services is to be deployed [1][2]. This should be formatted as a full Azure resource ID (e.g., /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName}) [1][2]. Evolution of the Schema: Important Note: Starting with the 2025-06-01 API version, the property subnetId was removed from the top-level DomainServiceProperties [3]. Instead, the configuration for networking and location is now handled exclusively through the replicaSets array [1][3]. This change reflects a move toward a multi-region capable deployment model for Azure Active Directory Domain Services, requiring all subnet associations to be defined within the replicaSets configuration rather than as standalone top-level properties [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- client/domain_services.go ---'
cat -n client/domain_services.go
printf '%s\n' '--- list-domain-services.go ---'
cat -n cmd/list-domain-services.go
printf '%s\n' '--- domain-service output model ---'
cat -n models/domain-service.go
printf '%s\n' '--- related tests ---'
cat -n cmd/list-domain-services_test.go
cat -n models/domain-service_test.go
printf '%s\n' '--- official schema excerpt ---'
curl -LfsS 'https://learn.microsoft.com/en-us/azure/templates/microsoft.aad/2025-06-01/domainservices' |
  rg -n -i -C 3 'replicaSets|subnetId|location' | head -120 || true

Repository: SpecterOps/AzureHound

Length of output: 18410


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
import json

payload = {
    "properties": {
        "tenantId": "tenant",
        "replicaSets": [
            {"location": "eastus", "subnetId": "/subscriptions/s/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/v/subnets/sn"}
        ]
    }
}

# Equivalent shape of the current Go model: only declared JSON keys are retained.
current_keys = {
    "tenantId", "domainName", "domainConfigurationType", "filteredSync",
    "syncScope", "syncApplicationId", "domainSecuritySettings", "ldapsSettings"
}
current = {k: v for k, v in payload["properties"].items() if k in current_keys}

# Candidate shape from the review comment.
candidate = {
    k: v for k, v in payload["properties"].items()
    if k in current_keys or k == "replicaSets"
}
print("current replicaSets:", current.get("replicaSets"))
print("candidate replicaSets:", candidate.get("replicaSets"))
assert "replicaSets" not in current
assert candidate["replicaSets"][0]["location"] == "eastus"
assert candidate["replicaSets"][0]["subnetId"].endswith("/subnets/sn")
PY

Repository: SpecterOps/AzureHound

Length of output: 340


Preserve replicaSets in DomainServiceProperties.

ListAzureDomainServices uses the 2025-06-01 API, which returns replicaSets entries with location and subnetId. Add a []DomainServiceReplicaSet field so JSON unmarshalling retains this configuration.

🤖 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/azure/domain_service.go` around lines 40 - 49, Add a
[]DomainServiceReplicaSet field named ReplicaSets to DomainServiceProperties
with the appropriate JSON tag replicaSets, so ListAzureDomainServices preserves
API-returned replica set entries including location and subnetId during
unmarshalling.

Comment on lines +34 to +40
"ldapsSettings":{
"ldaps":"Enabled",
"externalAccess":"Disabled",
"publicCertificate":"certificate-data",
"certificateNotAfter":"2030-01-01T00:00:00Z",
"certificateThumbprint":"thumbprint"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)models/domain-service_test\.go$|domain.?service|domainservices' || true
printf '%s\n' '--- test file outline ---'
ast-grep outline models/domain-service_test.go 2>/dev/null || true
printf '%s\n' '--- relevant test sections ---'
sed -n '1,120p' models/domain-service_test.go
printf '%s\n' '--- related model definitions and field names ---'
rg -n -C 3 'ldapsSettings|pfxCertificate|pfxCertificatePassword|publicCertificate|certificateNotAfter|certificateThumbprint' . -g '*.go' -g '*.json' -g '*.yaml' -g '*.yml'

Repository: SpecterOps/AzureHound

Length of output: 6182


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- model definitions ---'
sed -n '1,180p' models/azure/domain_service.go
printf '%s\n' '--- domain service marshal implementation ---'
sed -n '1,220p' models/domain-service.go
printf '%s\n' '--- all LDAPS-related declarations and marshal filters ---'
rg -n -C 5 'DomainServiceLDAPSSettings|LDAPSSettings|MarshalJSON|publicCertificate|certificateNotAfter|certificateThumbprint|pfxCertificate|pfxCertificatePassword' models client cmd
printf '%s\n' '--- API schema references ---'
rg -n -i -C 3 'Microsoft\.AAD|2025-06-01|ldapsSettings|pfxCertificate' . -g '!vendor/**' -g '!node_modules/**'

Repository: SpecterOps/AzureHound

Length of output: 50378


🌐 Web query:

Microsoft.AAD 2025-06-01 domainServices ldapsSettings pfxCertificate pfxCertificatePassword

💡 Result:

In the Microsoft.AAD API version 2025-06-01 for Domain Services, the LdapsSettings object is used to configure Secure LDAP (LDAPS) [1][2]. This object includes two key properties related to certificate management: pfxCertificate: This property requires a base64-encoded representation of the certificate PFX file used to configure Secure LDAP [1][2]. When using the Azure CLI, this parameter can accept either the file path to the PFX file or its base64-encoded string [3]. pfxCertificatePassword: This property requires the password used to decrypt the provided Secure LDAP certificate PFX file [1][2][4]. These settings are part of the broader domainServices resource configuration, which also allows you to toggle the enabled or disabled status of Secure LDAP and external access via the ldaps and externalAccess flags, respectively [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
import json

raw = {
    "properties": {
        "ldapsSettings": {
            "ldaps": "Enabled",
            "externalAccess": "Disabled",
            "pfxCertificate": "certificate-data",
            "pfxCertificatePassword": "certificate-password",
            "publicCertificate": "legacy-certificate",
            "certificateNotAfter": "2030-01-01T00:00:00Z",
            "certificateThumbprint": "thumbprint",
        }
    }
}

# Model the exact JSON surface of azure.DomainServiceLDAPSSettings.
known = {"ldaps", "externalAccess"}
settings = {
    key: value
    for key, value in raw["properties"]["ldapsSettings"].items()
    if key in known
}
encoded = {"properties": {"ldapsSettings": settings}}

print("input fields:   ", sorted(raw["properties"]["ldapsSettings"]))
print("output fields:  ", sorted(encoded["properties"]["ldapsSettings"]))
print("sensitive output:", {
    key: encoded["properties"]["ldapsSettings"].get(key)
    for key in ("pfxCertificate", "pfxCertificatePassword")
})
PY

Repository: SpecterOps/AzureHound

Length of output: 437


Exercise the typed LDAPS serialization path.

pfxCertificate and pfxCertificatePassword are fields in Microsoft.AAD/domainServices 2025-06-01. DomainServiceLDAPSSettings declares only ldaps and externalAccess, so json.Unmarshal discards these fields before MarshalJSON runs. Adding them to this fixture and exclusion list alone will not test their serialization. Populate them through the typed model or a dedicated API DTO, then assert that MarshalJSON omits both fields.

🤖 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/domain-service_test.go` around lines 34 - 40, The domain service test
fixture currently exercises only untyped extra fields, so it does not validate
typed LDAPS serialization. Update the typed model setup used by the
domain-service test to populate pfxCertificate and pfxCertificatePassword
through DomainServiceLDAPSSettings or the dedicated API DTO, then assert that
MarshalJSON omits both fields while preserving the existing ldaps and
externalAccess serialization.

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