added documetation

This commit is contained in:
Jurjen Ladenius
2025-11-03 08:12:01 +01:00
parent 8840b0e300
commit a226ca97ac
37 changed files with 8315 additions and 1481 deletions

View File

@@ -1,4 +1,98 @@
#Connect-AzAccount
<#
.SYNOPSIS
Generates a hierarchical inventory of Azure Management Groups and Subscriptions.
.DESCRIPTION
This script creates a comprehensive mapping of Azure organizational structure by traversing
the management group hierarchy starting from a specified root management group. It discovers
and documents all subscriptions within a 3-level management group structure (Level 0-2).
The script provides detailed organizational visibility including:
- Hierarchical management group structure mapping
- Subscription placement and organizational context
- Subscription state tracking (Active, Disabled, etc.)
- Multi-level governance structure documentation
- CSV export for organizational analysis and compliance reporting
Note: The script is optimized for a maximum 3-level management group depth and starts
from a configurable root management group ID.
.PARAMETER RootManagementGroupId
The GUID of the root management group to start the hierarchy discovery from.
Defaults to the Effectory organization root management group.
Example: "12345678-1234-1234-1234-123456789012"
.OUTPUTS
CSV File: "<date> azure_managementgroups.csv"
Contains columns for:
- Subscription identification (ID, name, state)
- Level 0 Management Group (root level)
- Level 1 Management Group (department/division level)
- Level 2 Management Group (team/project level)
.EXAMPLE
.\ManagementGroups.ps1
Uses the default Effectory root management group and generates:
"2024-10-30 1435 azure_managementgroups.csv"
.EXAMPLE
.\ManagementGroups.ps1 -RootManagementGroupId "87654321-4321-4321-4321-210987654321"
Discovers management group hierarchy starting from a custom root management group.
.EXAMPLE
.\ManagementGroups.ps1 -RootManagementGroupId "tenant-root-mg" -Verbose
Runs with verbose output for detailed discovery logging.
.NOTES
File Name : ManagementGroups.ps1
Author : Cloud Engineering Team
Prerequisite : Azure PowerShell module (Az.Resources, Az.Accounts)
Copyright : (c) 2024 Effectory. All rights reserved.
Version History:
1.0 - Initial release with 3-level management group hierarchy discovery
1.1 - Added parameterized root management group for flexibility
.LINK
https://docs.microsoft.com/en-us/azure/governance/management-groups/
https://docs.microsoft.com/en-us/powershell/module/az.resources/
.COMPONENT
Requires Azure PowerShell modules:
- Az.Resources (for management group and subscription enumeration)
- Az.Accounts (for authentication and context management)
.ROLE
Required Azure permissions:
- Management Group Reader on the root management group and all child groups
- Reader access to view subscription details within management groups
.FUNCTIONALITY
- Hierarchical management group discovery (3-level maximum)
- Subscription placement mapping and state tracking
- Organizational structure documentation and CSV export
- Cross-tenant compatible with configurable root management group
#>
#Requires -Modules Az.Resources, Az.Accounts
#Requires -Version 5.1
# Uncomment the following line if authentication is required
#Connect-AzAccount
[CmdletBinding()]
param(
[Parameter(
Mandatory = $false,
HelpMessage = "The GUID of the root management group to start hierarchy discovery from"
)]
[ValidateNotNullOrEmpty()]
[string]$RootManagementGroupId = 'e9792fd7-4044-47e7-a40d-3fba46f1cd09'
)
class ResourceCheck {
[string] $SubscriptionId = ""
@@ -12,102 +106,286 @@ class ResourceCheck {
[string] $Level2_ManagementGroupName = ""
}
Write-Host "======================================================================================================================"
Write-Host "Creating list of Effectory Management Groups and subscriptions."
Write-Host "- Note: not very dynamic; Starts at hard coded root group and works up max 2 levels."
Write-Host "======================================================================================================================"
# Initialize script execution
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$startTime = Get-Date
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure_managementgroups.csv"
[ResourceCheck[]]$Result = @()
Write-Host "======================================================================================================================================================================"
Write-Host "🏗️ AZURE MANAGEMENT GROUP STRUCTURE DISCOVERY" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
Write-Host "⏰ Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Green
Write-Host ""
$rootManagementGroup = (Get-AzManagementGroup -GroupId 'e9792fd7-4044-47e7-a40d-3fba46f1cd09' -Expand)[0]
#level 0
Write-Host "---------------------------------------------------------------------------------------------"
Write-Host "Level 0 Management group [$($rootManagementGroup.Name)]"
Write-Host "---------------------------------------------------------------------------------------------"
$subscriptions = $rootManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
foreach ($subscription in $subscriptions)
{
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
Write-Host "Subscription [$($subscription.DisplayName) - $subscriptionId]"
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level0_ManagementGroupId = $rootManagementGroup.Id
$resourceCheck.Level0_ManagementGroupName = $rootManagementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscriptionId
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
}
#level 1
foreach ($level1ManagementGroupLister in ($rootManagementGroup.Children | Where-Object Type -EQ 'Microsoft.Management/managementGroups'))
{
$level1ManagementGroup = (Get-AzManagementGroup -Group $level1ManagementGroupLister.Name -Expand)[0]
Write-Host " ---------------------------------------------------------------------------------------------"
Write-Host " Level 1 Management group [$($level1ManagementGroup.Name)]"
Write-Host " ---------------------------------------------------------------------------------------------"
$subscriptions = $level1ManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
foreach ($subscription in $subscriptions)
{
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
Write-Host " Subscription [$($subscription.DisplayName) - $subscriptionId]"
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level0_ManagementGroupId = $rootManagementGroup.Id
$resourceCheck.Level0_ManagementGroupName = $rootManagementGroup.DisplayName
$resourceCheck.Level1_ManagementGroupId = $level1ManagementGroup.Id
$resourceCheck.Level1_ManagementGroupName = $level1ManagementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscriptionId
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
try {
# Validate Azure authentication
$context = Get-AzContext
if (-not $context) {
throw "No Azure context found. Please run Connect-AzAccount first."
}
Write-Host "🔐 Authenticated as: $($context.Account.Id)" -ForegroundColor Green
Write-Host "🏢 Tenant: $($context.Tenant.Id)" -ForegroundColor Green
Write-Host "🎯 Root Management Group: $RootManagementGroupId" -ForegroundColor Yellow
Write-Host ""
# Initialize output file and tracking variables
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure_managementgroups.csv"
Write-Host "📄 Output file: $fileName" -ForegroundColor Yellow
Write-Host ""
[ResourceCheck[]]$Result = @()
$totalSubscriptions = 0
$managementGroupCount = 0
# Get root management group with error handling
Write-Host "🔍 Discovering root management group structure..." -ForegroundColor Cyan
$rootManagementGroup = Get-AzManagementGroup -GroupId $RootManagementGroupId -Expand -ErrorAction Stop
if (-not $rootManagementGroup) {
throw "Root management group '$RootManagementGroupId' not found or not accessible."
}
Write-Host "✅ Root Management Group: $($rootManagementGroup.DisplayName)" -ForegroundColor Green
Write-Host " ID: $($rootManagementGroup.Id)" -ForegroundColor DarkGray
Write-Host ""
#level 2
foreach ($level2ManagementGroupLister in ($level1ManagementGroup.Children | Where-Object Type -EQ 'Microsoft.Management/managementGroups'))
{
$level2ManagementGroup = (Get-AzManagementGroup -Group $level2ManagementGroupLister.Name -Expand)[0]
# Process Level 0 (Root) subscriptions
$managementGroupCount++
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "📋 LEVEL 0 (Root): $($rootManagementGroup.DisplayName)" -ForegroundColor Cyan
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host " ---------------------------------------------------------------------------------------------"
Write-Host " Level 2 Management group [$($level2ManagementGroup.Name)]"
Write-Host " ---------------------------------------------------------------------------------------------"
$subscriptions = $level2ManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
$subscriptions = $rootManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
Write-Host " 📊 Direct subscriptions: $($subscriptions.Count)" -ForegroundColor Green
foreach ($subscription in $subscriptions)
{
foreach ($subscription in $subscriptions) {
try {
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
Write-Host " Subscription [$($subscription.DisplayName) - $subscriptionId]"
# Color code subscription state
$stateColor = switch ($subscription.State) {
"Enabled" { "Green" }
"Disabled" { "Red" }
"Warned" { "Yellow" }
default { "White" }
}
Write-Host " 📋 Subscription: $($subscription.DisplayName) [$subscriptionId] - $($subscription.State)" -ForegroundColor $stateColor
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level0_ManagementGroupId = $rootManagementGroup.Id
$resourceCheck.Level0_ManagementGroupName = $rootManagementGroup.DisplayName
$resourceCheck.Level1_ManagementGroupId = $level1ManagementGroup.Id
$resourceCheck.Level1_ManagementGroupName = $level1ManagementGroup.DisplayName
$resourceCheck.Level2_ManagementGroupId = $level2ManagementGroup.Id
$resourceCheck.Level2_ManagementGroupName = $level2ManagementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscriptionId
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
$totalSubscriptions++
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
}
}
# Process Level 1 management groups
$level1Groups = $rootManagementGroup.Children | Where-Object Type -EQ 'Microsoft.Management/managementGroups'
Write-Host ""
Write-Host "🔍 Found $($level1Groups.Count) Level 1 management groups" -ForegroundColor Green
Write-Host ""
foreach ($level1ManagementGroupLister in $level1Groups) {
try {
$managementGroupCount++
$level1ManagementGroup = Get-AzManagementGroup -Group $level1ManagementGroupLister.Name -Expand -ErrorAction Stop
Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
Write-Host " 📂 LEVEL 1: $($level1ManagementGroup.DisplayName)" -ForegroundColor Yellow
Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
$subscriptions = $level1ManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
Write-Host " 📊 Direct subscriptions: $($subscriptions.Count)" -ForegroundColor Green
foreach ($subscription in $subscriptions) {
try {
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
# Color code subscription state
$stateColor = switch ($subscription.State) {
"Enabled" { "Green" }
"Disabled" { "Red" }
"Warned" { "Yellow" }
default { "White" }
}
Write-Host " 📋 Subscription: $($subscription.DisplayName) [$subscriptionId] - $($subscription.State)" -ForegroundColor $stateColor
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level0_ManagementGroupId = $rootManagementGroup.Id
$resourceCheck.Level0_ManagementGroupName = $rootManagementGroup.DisplayName
$resourceCheck.Level1_ManagementGroupId = $level1ManagementGroup.Id
$resourceCheck.Level1_ManagementGroupName = $level1ManagementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscriptionId
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
$totalSubscriptions++
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
}
}
} catch {
Write-Host " ❌ Error accessing Level 1 management group '$($level1ManagementGroupLister.Name)': $($_.Exception.Message)" -ForegroundColor Red
}
# Process Level 2 management groups (nested within Level 1)
$level2Groups = $level1ManagementGroup.Children | Where-Object Type -EQ 'Microsoft.Management/managementGroups'
if ($level2Groups.Count -gt 0) {
Write-Host " 🔍 Found $($level2Groups.Count) Level 2 management groups" -ForegroundColor Green
foreach ($level2ManagementGroupLister in $level2Groups) {
try {
$managementGroupCount++
$level2ManagementGroup = Get-AzManagementGroup -Group $level2ManagementGroupLister.Name -Expand -ErrorAction Stop
Write-Host ""
Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
Write-Host " 📁 LEVEL 2: $($level2ManagementGroup.DisplayName)" -ForegroundColor Magenta
Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
$subscriptions = $level2ManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
Write-Host " 📊 Direct subscriptions: $($subscriptions.Count)" -ForegroundColor Green
foreach ($subscription in $subscriptions) {
try {
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
# Color code subscription state
$stateColor = switch ($subscription.State) {
"Enabled" { "Green" }
"Disabled" { "Red" }
"Warned" { "Yellow" }
default { "White" }
}
Write-Host " 📋 Subscription: $($subscription.DisplayName) [$subscriptionId] - $($subscription.State)" -ForegroundColor $stateColor
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level0_ManagementGroupId = $rootManagementGroup.Id
$resourceCheck.Level0_ManagementGroupName = $rootManagementGroup.DisplayName
$resourceCheck.Level1_ManagementGroupId = $level1ManagementGroup.Id
$resourceCheck.Level1_ManagementGroupName = $level1ManagementGroup.DisplayName
$resourceCheck.Level2_ManagementGroupId = $level2ManagementGroup.Id
$resourceCheck.Level2_ManagementGroupName = $level2ManagementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscriptionId
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
$totalSubscriptions++
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
}
}
} catch {
Write-Host " ❌ Error accessing Level 2 management group '$($level2ManagementGroupLister.Name)': $($_.Exception.Message)" -ForegroundColor Red
}
}
} else {
Write-Host " No Level 2 management groups found" -ForegroundColor DarkGray
}
}
# Export results to CSV
Write-Host ""
Write-Host "💾 Exporting results to CSV..." -ForegroundColor Cyan
$Result | Export-Csv -Path $fileName -NoTypeInformation
# Calculate execution time and generate summary report
$endTime = Get-Date
$executionTime = $endTime - $startTime
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "📊 AZURE MANAGEMENT GROUP DISCOVERY SUMMARY" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
Write-Host "⏰ Execution Time: $($executionTime.ToString('hh\:mm\:ss'))" -ForegroundColor Green
Write-Host "🏗️ Management Groups Discovered: $managementGroupCount" -ForegroundColor Green
Write-Host "📋 Total Subscriptions Found: $totalSubscriptions" -ForegroundColor Green
Write-Host "📄 Results Exported To: $fileName" -ForegroundColor Yellow
if (Test-Path $fileName) {
$fileSize = (Get-Item $fileName).Length
Write-Host "💾 File Size: $([math]::Round($fileSize/1KB, 2)) KB" -ForegroundColor Green
}
# Analyze subscription states
$subscriptionStates = $Result | Group-Object SubscriptionState
if ($subscriptionStates.Count -gt 0) {
Write-Host ""
Write-Host "📈 SUBSCRIPTION STATE ANALYSIS:" -ForegroundColor Cyan
foreach ($state in $subscriptionStates) {
$stateColor = switch ($state.Name) {
"Enabled" { "Green" }
"Disabled" { "Red" }
"Warned" { "Yellow" }
default { "White" }
}
Write-Host " $($state.Name): $($state.Count) subscriptions" -ForegroundColor $stateColor
}
}
# Provide organizational insights
$level0Subs = ($Result | Where-Object { [string]::IsNullOrEmpty($_.Level1_ManagementGroupId) }).Count
$level1Subs = ($Result | Where-Object { -not [string]::IsNullOrEmpty($_.Level1_ManagementGroupId) -and [string]::IsNullOrEmpty($_.Level2_ManagementGroupId) }).Count
$level2Subs = ($Result | Where-Object { -not [string]::IsNullOrEmpty($_.Level2_ManagementGroupId) }).Count
Write-Host ""
Write-Host "🏗️ ORGANIZATIONAL STRUCTURE:" -ForegroundColor Cyan
Write-Host " Root Level (Level 0): $level0Subs subscriptions" -ForegroundColor Green
Write-Host " Department/Division Level (Level 1): $level1Subs subscriptions" -ForegroundColor Yellow
Write-Host " Team/Project Level (Level 2): $level2Subs subscriptions" -ForegroundColor Magenta
Write-Host ""
Write-Host "📈 NEXT STEPS:" -ForegroundColor Cyan
Write-Host " 1. Review the generated CSV file for detailed organizational mapping" -ForegroundColor White
Write-Host " 2. Analyze subscription placement for governance compliance" -ForegroundColor White
Write-Host " 3. Consider moving orphaned subscriptions to appropriate management groups" -ForegroundColor White
Write-Host " 4. Use this data for policy assignment and resource organization planning" -ForegroundColor White
Write-Host ""
Write-Host "✅ Azure Management Group discovery completed successfully!" -ForegroundColor Green
Write-Host "======================================================================================================================================================================"
} catch {
Write-Host ""
Write-Host "❌ CRITICAL ERROR OCCURRED" -ForegroundColor Red
Write-Host "======================================================================================================================================================================"
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Line: $($_.InvocationInfo.ScriptLineNumber)" -ForegroundColor Red
Write-Host "Position: $($_.InvocationInfo.OffsetInLine)" -ForegroundColor Red
Write-Host ""
Write-Host "🔧 TROUBLESHOOTING STEPS:" -ForegroundColor Yellow
Write-Host " 1. Verify you are authenticated to Azure (Connect-AzAccount)" -ForegroundColor White
Write-Host " 2. Ensure you have Management Group Reader permissions on the root management group" -ForegroundColor White
Write-Host " 3. Verify the root management group ID '$RootManagementGroupId' exists and is accessible" -ForegroundColor White
Write-Host " 4. Check that Azure PowerShell modules are installed and up to date" -ForegroundColor White
Write-Host " 5. Try running with a different root management group ID if the current one is invalid" -ForegroundColor White
Write-Host ""
Write-Host "📞 For additional support, contact the Cloud Engineering team" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
# Ensure we exit with error code for automation scenarios
exit 1
} finally {
# Reset progress preference
$ProgressPreference = "Continue"
}
$Result | Export-Csv -Path $fileName -NoTypeInformation
Write-Host "============================================================================================="
Write-Host "Done."