Skip to content

Instantly share code, notes, and snippets.

@santisq
Last active March 12, 2024 18:47
Show Gist options
  • Save santisq/cb2847c0646ba7b0aefc663ff36128c5 to your computer and use it in GitHub Desktop.
Save santisq/cb2847c0646ba7b0aefc663ff36128c5 to your computer and use it in GitHub Desktop.
Script to report on Tenant Management Groups using Resource Manager API, KQL and PowerShell
$subIdContext = @{
Stage = 'xxxxx-xxxxx-xxxxx-xxxx'
Prod = 'xxxxx-xxxxx-xxxxx-xxxx'
}
if ($connected.Count -ne 2) {
$connected = @(
Connect-AzAccount -Subscription $subIdContext['Prod'] -SkipContextPopulation
Connect-MgGraph
)
}
if ($connected.Count -ne 2) {
throw 'Failed to connect.'
}
class Identity {
[string] $Id
[string] $DisplayName
[string] $Type
Identity([hashtable] $identity) {
$this.Id = $identity['id']
$this.DisplayName = $identity['displayName']
$this.Type = $identity['@odata.type'].SubString(17)
}
}
$owner = Search-AzGraph @'
authorizationresources
| where ['type'] == 'microsoft.authorization/roledefinitions'
and properties.roleName == 'Owner'
| take 1
| project ['id']
'@ -AllowPartialScope -UseTenantScope
$query = @'
resourcecontainers
| where ['type'] == 'microsoft.management/managementgroups'
| join kind = leftouter (
resourcecontainers
| where ['type'] == 'microsoft.resources/subscriptions'
| extend name = tostring(properties.managementGroupAncestorsChain[0].name)
| summarize
SubscriptionCount = count() by name
) on name
| join kind = leftouter (
resourcecontainers
| where ['type'] == 'microsoft.management/managementgroups'
| extend id = tostring(properties.details.parent.id)
| summarize ChildManagementGroupsCount = count() by id
) on id
| join kind = leftouter (
resourcecontainers
| where ['type'] == 'microsoft.management/managementgroups'
| extend chain = properties.details.managementGroupAncestorsChain
| mv-expand chain
| summarize parentChain = make_list(chain.displayName) by id
) on id
| extend DisplayName = properties.displayName
| project
Name = name,
DisplayName = DisplayName,
SubscriptionCount = iif(isnull(SubscriptionCount), 0, SubscriptionCount),
ChildManagementGroupsCount = iif(isnull(ChildManagementGroupsCount), 0, ChildManagementGroupsCount),
ParentGroupName = properties.details.parent.name,
ParentGroupDisplayName = properties.details.parent.displayName,
ManagementGroupChain = array_concat(pack_array(DisplayName), parentChain),
Owners = ''
| order by SubscriptionCount, ChildManagementGroupsCount
'@
$searchAzGraphSplat = @{
Query = $query
}
$result = do {
$response = Search-AzGraph @searchAzGraphSplat
$searchAzGraphSplat['SkipToken'] = $response.SkipToken
if ($response.Data.Count) {
$response.Data
}
}
while ($response.SkipToken)
$searchAzGraphSplat.Remove('SkipToken')
$searchAzGraphSplat['UseTenantScope'] = $true
$searchAzGraphSplat['AllowPartialScope'] = $true
$searchAzGraphSplat['Query'] = @"
authorizationresources
| extend scope = properties.scope
| where ['type'] == 'microsoft.authorization/roleassignments'
and properties.roleDefinitionId == '$($owner.id)'
and scope == '/'
or scope startswith '/providers/Microsoft.Management/managementGroups'
| extend scope = properties.scope
| extend name = substring(scope, 49)
| extend principalId = properties.principalId
| where scope == '/' or scope startswith '/providers/Microsoft.Management/managementGroups'
| summarize Owners = make_list(principalId) by name
"@
$uri = 'v1.0/directoryObjects/{0}'
$owners = [System.Collections.Generic.Dictionary[string, Identity[]]]::new()
$skipHash = [System.Collections.Generic.HashSet[string]]::new()
do {
$response = Search-AzGraph @searchAzGraphSplat
$searchAzGraphSplat['SkipToken'] = $response.SkipToken
foreach ($i in $response.Data) {
$ids = foreach ($id in $i.Owners) {
if ($skipHash.Contains($id)) {
continue
}
if (-not $owners.ContainsKey($id)) {
try {
Invoke-MgGraphRequest GET ($uri -f $id)
}
catch {
$null = $skipHash.Add($id)
continue
}
}
}
$owners.Add($i.name, @($ids))
}
}
while ($response.SkipToken)
foreach ($item in $result) {
if ($owners.ContainsKey($item.Name)) {
$json = ConvertTo-Json -InputObject $owners[$item.Name] -Compress
$item.Owners = $json
}
if ($item.ManagementGroupChain.Length -eq 1) {
$item.ManagementGroupChain = $item.ManagementGroupChain[0]
continue
}
[array]::Reverse($item.ManagementGroupChain)
$item.ManagementGroupChain = $item.ManagementGroupChain -join '/'
}
$name = 'managementGroups {0}' -f [datetime]::Now.ToString('MM-dd-yyyy')
$excelParams = @{
WorksheetName = $name
BoldTopRow = $true
TableName = 'managementGroupReport'
TableStyle = 'Medium11'
InputObject = $result
NoNumberConversion = 'Name', 'DisplayName'
Show = $true
Path = [System.IO.Path]::Combine(
'.\managementGroupReport\export',
[System.IO.Path]::ChangeExtension(
$connected[0].Context.Tenant.Id + ' ' + $name ,
'xlsx'))
}
Export-Excel @excelParams
$subIdContext = @{
Prod = @{
Subscription = 'xxxx-xxx-xxxx-xxxxx-xxxxx'
TenantId = 'xxxx-xxx-xxxx-xxxxx-xxxxx'
}
}
if ($connected.Count -ne 2) {
$connected = @(
$ctx = $subIdContext['Prod']
$connectAzAccountSplat = @{
Subscription = $ctx.Subscription
SkipContextPopulation = $true
Tenant = $ctx.TenantId
}
Connect-AzAccount @connectAzAccountSplat
Connect-MgGraph -TenantId $ctx.TenantId
)
}
if ($connected.Count -ne 2) {
throw 'Failed to connect.'
}
$owner = Get-AzRoleDefinition -Name Owner
$query = @'
resourcecontainers
| where ['type'] == 'microsoft.management/managementgroups'
| join kind = leftouter (
resourcecontainers
| where ['type'] == 'microsoft.resources/subscriptions'
| extend name = tostring(properties.managementGroupAncestorsChain[0].name)
| summarize
SubscriptionCount = count() by name
) on name
| join kind = leftouter (
resourcecontainers
| where ['type'] == 'microsoft.management/managementgroups'
| extend id = tostring(properties.details.parent.id)
| summarize ChildManagementGroupsCount = count() by id
) on id
| join kind = leftouter (
resourcecontainers
| where ['type'] == 'microsoft.management/managementgroups'
| extend chain = properties.details.managementGroupAncestorsChain
| mv-expand chain
| summarize parentChain = make_list(chain.displayName) by id
) on id
| extend DisplayName = properties.displayName
| project
Id = id,
Name = name,
DisplayName = DisplayName,
SubscriptionCount = iif(isnull(SubscriptionCount), 0, SubscriptionCount),
ChildManagementGroupsCount = iif(isnull(ChildManagementGroupsCount), 0, ChildManagementGroupsCount),
ParentGroupName = properties.details.parent.name,
ParentGroupDisplayName = properties.details.parent.displayName,
ManagementGroupChain = array_concat(pack_array(DisplayName), parentChain),
OwnerCount = 0,
InheritedOwnerCount = 0,
Owners = ''
| order by SubscriptionCount, ChildManagementGroupsCount
'@
$searchAzGraphSplat = @{
Query = $query
}
$result = do {
$response = Search-AzGraph @searchAzGraphSplat
$searchAzGraphSplat['SkipToken'] = $response.SkipToken
if ($response.Data.Count) {
$response.Data
}
}
while ($response.SkipToken)
$result | ForEach-Object -Parallel {
$retries = 5
$assignments = while ($retries--) {
try {
$getAzRoleAssignmentSplat = @{
Scope = $_.Id
RoleDefinitionId = $using:owner.Id
}
Get-AzRoleAssignment @getAzRoleAssignmentSplat
break
}
catch {
Write-Warning $_.ToString()
}
}
$inheritedCount = 0
$owners = foreach ($assignment in $assignments) {
if ($assignment.ObjectType -eq 'Unknown') {
continue
}
if ($isInherited = $assignment.Scope -ne $_.Id) {
$inheritedCount++
}
[pscustomobject]@{
ObjectId = $assignment.ObjectId
DisplayName = $assignment.DisplayName
ObjectType = $assignment.ObjectType
IsInherited = $isInherited
}
}
$_.OwnerCount = $owners.Count
$_.InheritedOwnerCount = $inheritedCount
$_.Owners = ($owners | ConvertTo-Csv) -join [System.Environment]::NewLine
if ($_.ManagementGroupChain.Length -eq 1) {
$_.ManagementGroupChain = $_.ManagementGroupChain[0]
}
else {
[array]::Reverse($_.ManagementGroupChain)
$_.ManagementGroupChain = $_.ManagementGroupChain -join '/'
}
}
$name = 'managementGroups {0}' -f [datetime]::Now.ToString('MM-dd-yyyy')
$excelParams = @{
WorksheetName = $name
BoldTopRow = $true
TableName = 'managementGroupReport'
TableStyle = 'Medium11'
InputObject = $result
NoNumberConversion = 'Name', 'DisplayName'
Show = $true
Path = [System.IO.Path]::Combine(
'.\managementGroupReport\export',
[System.IO.Path]::ChangeExtension(
$connected[0].Context.Tenant.Id + ' ' + $name, 'xlsx'))
}
Export-Excel @excelParams
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment