Public/Get-AzPolicyAssignmentScan.ps1
|
function Get-AzPolicyAssignmentScan { <# .SYNOPSIS Scan Azure Policy assignments across management groups and subscriptions. .DESCRIPTION Retrieves all policy assignments from specified scope (Management Group or Subscription), optionally scanning child resources recursively. Expands PolicySet assignments into individual policies with resolved effects. Filters out excluded assignments by name pattern. ⚡ OPTIMIZED VERSION - Caches PolicySet expansions to avoid redundant API calls. .PARAMETER ManagementGroupId Management Group ID to scan. If specified with -Recursive, will also scan all child MGs and subscriptions. Mutually exclusive with -SubscriptionId. .PARAMETER SubscriptionId Subscription ID to scan. Mutually exclusive with -ManagementGroupId. .PARAMETER Recursive Scan all child management groups and subscriptions recursively. Default: $true .PARAMETER ExcludeAssignments Array of assignment names or patterns to exclude. Supports wildcards. Example: @("Microsoft cloud security benchmark", "ASC*") .PARAMETER DirectOnly Only return assignments directly scoped to the target (no inherited assignments). Default: $false .EXAMPLE # Scan entire tenant from root management group $assignments = Get-AzPolicyAssignmentScan -ManagementGroupId "MyRootMG" -Recursive Write-Host "Total assignments found: $($assignments.Count)" Write-Host "Total expanded policies: $(($assignments | ForEach-Object { $_.ExpandedPolicies }).Count)" .EXAMPLE # Scan specific subscription only $assignments = Get-AzPolicyAssignmentScan -SubscriptionId "12345678-1234-1234-1234-123456789012" .EXAMPLE # Scan MG with exclusions $assignments = Get-AzPolicyAssignmentScan -ManagementGroupId "MyMG" ` -ExcludeAssignments @("Microsoft cloud security benchmark", "ASC*") .EXAMPLE # Scan subscription with direct assignments only (no inherited) $assignments = Get-AzPolicyAssignmentScan -SubscriptionId "sub-id" -DirectOnly .OUTPUTS Array of PSCustomObject with properties: - AssignmentId: Full Azure resource ID of the assignment - AssignmentName: Short name of the assignment - AssignmentDisplayName: Display name - Scope: Scope where assignment is defined - PolicyDefinitionId: ID of the assigned policy/initiative - PolicySetDisplayName: Display name of initiative (if applicable) - IsPolicySet: Boolean indicating if this is an initiative - IsIndividualPolicy: Boolean indicating if this is a single policy - ExpandedPolicies: Array of individual policies with resolved effects: * PolicyDisplayName: Display name * PolicyDefinitionId: Resource ID * Effect: Resolved effect (from assignment parameters or definition default) * Version: Policy version * Category: Policy category .NOTES Requires Az.Resources module. Must be authenticated with Connect-AzAccount. Performance: Scanning large tenants with many assignments can take several minutes. Use -ExcludeAssignments to filter out known assignments you don't need. #> [CmdletBinding(DefaultParameterSetName = 'ManagementGroup')] [OutputType([PSCustomObject[]])] param( [Parameter(ParameterSetName = 'ManagementGroup', Mandatory = $true)] [string]$ManagementGroupId, [Parameter(ParameterSetName = 'Subscription', Mandatory = $true)] [string]$SubscriptionId, [switch]$Recursive = $true, [string[]]$ExcludeAssignments = @(), [switch]$DirectOnly ) begin { Write-Debug "Get-AzPolicyAssignmentScan: Starting assignment scan" Write-Debug " ManagementGroupId: $ManagementGroupId" Write-Debug " SubscriptionId: $SubscriptionId" Write-Debug " Recursive: $Recursive" Write-Debug " ExcludeAssignments: $($ExcludeAssignments -join ', ')" # Verify Az context try { $context = Get-AzContext if (-not $context) { throw "No Azure context found. Please run Connect-AzAccount" } Write-Verbose "Azure context: $($context.Account.Id) -> $($context.Subscription.Name)" } catch { throw "Failed to get Azure context: $($_.Exception.Message)" } # Use module-scoped caches (persistent across calls) # DO NOT reinitialize - reuse existing cache if (-not $script:PolicyDefinitionCache) { $script:PolicyDefinitionCache = @{} } if (-not $script:PolicySetCache) { $script:PolicySetCache = @{} } } process { try { $allAssignments = @() # Step 1: Collect assignments based on scope if ($PSCmdlet.ParameterSetName -eq 'ManagementGroup') { Write-Verbose "Scanning Management Group: $ManagementGroupId" # Get assignments from this MG $mgAssignments = Get-MgAssignments -ManagementGroupId $ManagementGroupId Write-Verbose " Direct MG assignments: $($mgAssignments.Count)" $allAssignments += $mgAssignments # Get assignments from child MGs and subs if recursive if ($Recursive) { Write-Verbose " Scanning child management groups and subscriptions..." Write-Host " ├─ Discovering child Management Groups under $ManagementGroupId..." -ForegroundColor Cyan try { # ✅ Récupérer la hiérarchie complète en UNE SEULE fois (avec -Recurse) $targetMg = Get-AzManagementGroup -GroupName $ManagementGroupId -Expand -Recurse # Extraire tous les MGs enfants de la structure déjà chargée (pas d'appels API supplémentaires) function Get-ChildMgs($mg) { $results = @() if ($mg.Children) { foreach ($child in $mg.Children) { if ($child.Type.Contains("Microsoft.Management/managementGroups")) { $results += $child # Récursif sur la structure déjà chargée (pas d'appel API) $results += Get-ChildMgs $child } } } return $results } $childManagementGroups = @(Get-ChildMgs $targetMg) Write-Host " │ ├─ Total child MGs found: $($childManagementGroups.Count)" -ForegroundColor Gray Write-Verbose " Child MGs: $(($childManagementGroups | ForEach-Object { $_.Name }) -join ', ')" # Scanner les assignments de chaque MG enfant $childMgAssignments = @() $mgCounter = 0 Write-Host " │ ├─ [$($childManagementGroups.Count)] Scanning child MGs..." -ForegroundColor Gray foreach ($childMg in $childManagementGroups) { $mgCounter++ $singleCallTimer = [System.Diagnostics.Stopwatch]::StartNew() # ⏱️ Mesurer le temps de chaque Get-AzPolicyAssignment Write-Verbose " [$mgCounter/$($childManagementGroups.Count)] Scanning child MG: $($childMg.Name)" try { $mgScope = "/providers/Microsoft.Management/managementGroups/$($childMg.Name)" $mgAssignments = Get-AzPolicyAssignment -Scope $mgScope -ErrorAction SilentlyContinue $singleCallTimer.Stop() if ($mgAssignments) { $childMgAssignments += $mgAssignments Write-Verbose " Found $($mgAssignments.Count) assignments in $($childMg.Name) (took $($singleCallTimer.ElapsedMilliseconds)ms)" } else { Write-Verbose " No assignments in $($childMg.Name) (took $($singleCallTimer.ElapsedMilliseconds)ms)" } } catch { $singleCallTimer.Stop() Write-Warning "Failed to scan child MG '$($childMg.Name)' after $($singleCallTimer.ElapsedMilliseconds)ms: $($_.Exception.Message)" } } Write-Verbose " ⏱️ Total scan time: $([math]::Round(($childMgAssignments | Measure-Object).Count / $childManagementGroups.Count, 2)) avg assignments per MG" Write-Host " │ └─ Found $($childMgAssignments.Count) assignments across all child MGs" -ForegroundColor Gray } catch { Write-Warning "Failed to discover child Management Groups: $($_.Exception.Message)" $childMgAssignments = @() } Write-Verbose " Child MG assignments: $($childMgAssignments.Count)" $allAssignments += $childMgAssignments } } elseif ($PSCmdlet.ParameterSetName -eq 'Subscription') { Write-Verbose "Scanning Subscription: $SubscriptionId" $subAssignments = Get-SubAssignments -SubscriptionId $SubscriptionId -DirectOnly:$DirectOnly Write-Verbose " Subscription assignments: $($subAssignments.Count)" $allAssignments += $subAssignments } Write-Verbose "Total assignments collected: $($allAssignments.Count)" # Step 2: Apply exclusion filters if ($ExcludeAssignments.Count -gt 0) { Write-Verbose "Applying exclusion filters..." $filteredAssignments = $allAssignments | Where-Object { $assignment = $_ $shouldExclude = $false foreach ($pattern in $ExcludeAssignments) { if ($assignment.DisplayName -like $pattern -or $assignment.Name -like $pattern) { $shouldExclude = $true break } } -not $shouldExclude } $excludedCount = $allAssignments.Count - $filteredAssignments.Count Write-Verbose " Excluded assignments: $excludedCount" $allAssignments = $filteredAssignments } # Step 3: Expand PolicySets to individual policies Write-Verbose "Expanding PolicySet assignments to individual policies..." # ✅ OPTIMISATION: Cache des PolicySets déjà expandés $expandedSetsCache = @{} $expandedAssignments = @() $progressCounter = 0 foreach ($assignment in $allAssignments) { $progressCounter++ if ($progressCounter % 10 -eq 0) { Write-Progress -Activity "Expanding Policy Assignments" ` -Status "Processing assignment $progressCounter of $($allAssignments.Count)" ` -PercentComplete (($progressCounter / $allAssignments.Count) * 100) } try { $policySetId = $assignment.PolicyDefinitionId $expandedPolicies = $null # ✅ Vérifier si ce PolicySet a déjà été expandé if ($expandedSetsCache.ContainsKey($policySetId)) { # Réutiliser l'expansion existante $expandedPolicies = $expandedSetsCache[$policySetId] Write-Debug "Reusing cached expansion for: $policySetId ($($expandedPolicies.Count) policies)" } else { # Expander et mettre en cache $expandedPolicies = Expand-PolicySetMembers -Assignment $assignment ` -PolicyDefinitionCache $script:PolicyDefinitionCache ` -PolicySetDefinitionCache $script:PolicySetCache # Mettre en cache seulement pour les PolicySets (pas les individual policies) $isIndividualPolicy = $assignment.PolicyDefinitionId -like "*/policyDefinitions/*" -and $assignment.PolicyDefinitionId -notlike "*/policySetDefinitions/*" if (-not $isIndividualPolicy -and $expandedPolicies.Count -gt 0) { $expandedSetsCache[$policySetId] = $expandedPolicies Write-Debug "Cached expansion for: $policySetId ($($expandedPolicies.Count) policies)" } } # Add expanded policies to assignment object $expandedAssignment = [PSCustomObject]@{ AssignmentId = $assignment.Id AssignmentName = $assignment.Name AssignmentDisplayName = $assignment.DisplayName Scope = $assignment.Scope PolicyDefinitionId = $assignment.PolicyDefinitionId PolicySetDisplayName = $assignment.PolicySetDisplayName IsPolicySet = $assignment.IsPolicySet IsIndividualPolicy = $assignment.IsIndividualPolicy ExpandedPolicies = $expandedPolicies } $expandedAssignments += $expandedAssignment } catch { Write-Warning "Failed to expand assignment '$($assignment.DisplayName)': $($_.Exception.Message)" continue } } Write-Progress -Activity "Expanding Policy Assignments" -Completed # ✅ Stats sur le cache $uniqueSets = $expandedSetsCache.Count $cacheReuses = $expandedAssignments.Count - $uniqueSets Write-Verbose "Expansion cache stats: $uniqueSets unique PolicySets, $cacheReuses reuses" Write-Verbose "Expansion complete: $($expandedAssignments.Count) assignments with $(($expandedAssignments | ForEach-Object { $_.ExpandedPolicies.Count } | Measure-Object -Sum).Sum) total policies" Write-Debug "Get-AzPolicyAssignmentScan: Completed successfully" return $expandedAssignments } catch { Write-Error "Failed to scan policy assignments: $($_.Exception.Message)" throw } } } |