Public/Get-EGIGroupBlastRadius.ps1
|
function Get-EGIGroupBlastRadius { <# .SYNOPSIS Maps everything downstream that depends on a given Entra ID group. .DESCRIPTION A dynamic group's rule usually looks harmless in isolation, but the group itself is frequently the target of Conditional Access policies, license assignment, application role assignments, and (for role-assignable groups) PIM eligibility. This function collects all of those references so a membership-rule change can be evaluated against its real blast radius before it is saved. Requires an existing Microsoft Graph connection (Connect-MgGraph) with at least: Group.Read.All, Policy.Read.All, Directory.Read.All, and RoleManagement.Read.Directory if the group is role-assignable. .PARAMETER GroupId Object ID of the group to analyze. .EXAMPLE Get-EGIGroupBlastRadius -GroupId '11111111-2222-3333-4444-555555555555' | Format-List #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidatePattern('^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$')] [string] $GroupId ) $group = Invoke-MgGraphRequest -Method GET ` -Uri "https://graph.microsoft.com/v1.0/groups/$GroupId`?`$select=id,displayName,groupTypes,membershipRule,membershipRuleProcessingState,assignedLicenses,isAssignableToRole,resourceProvisioningOptions" Write-Verbose "Analyzing blast radius for group '$($group.displayName)' ($GroupId)" # --- Conditional Access policies referencing this group ------------ $caPolicies = Invoke-EGIGraphPaged -Uri 'https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies?$select=id,displayName,state,conditions' $caMatches = foreach ($policy in $caPolicies) { $includeHit = $policy.conditions.users.includeGroups -contains $GroupId $excludeHit = $policy.conditions.users.excludeGroups -contains $GroupId if ($includeHit -or $excludeHit) { [pscustomobject]@{ DisplayName = $policy.displayName State = $policy.state Reference = if ($includeHit -and $excludeHit) { 'Include+Exclude' } elseif ($includeHit) { 'Include' } else { 'Exclude' } } } } # --- License assignment on the group itself ------------------------- # Outer @() keeps a single license from unrolling to a bare hashtable, # whose .Count would be its key count rather than 1. $licenses = @(@($group.assignedLicenses) | Where-Object { $_.skuId }) # --- Application role assignments (enterprise app access) ----------- $appRoleAssignments = try { Invoke-EGIGraphPaged -Uri "https://graph.microsoft.com/v1.0/groups/$GroupId/appRoleAssignments" } catch { Write-Warning "Could not read app role assignments for group '$($group.displayName)': $($_.Exception.Message)" @() } # --- PIM eligibility, only relevant for role-assignable groups ------- $pimEligibility = @() if ($group.isAssignableToRole) { try { $pimEligibility = Invoke-EGIGraphPaged -Uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilityScheduleInstances?`$filter=principalId eq '$GroupId'" } catch { Write-Warning "Could not read PIM eligibility for group '$($group.displayName)' - check RoleManagement.Read.Directory permission: $($_.Exception.Message)" } } # --- Teams / SharePoint provisioning (Microsoft 365 groups only) ---- $isTeamsGroup = $group.resourceProvisioningOptions -contains 'Team' $totalDependencies = @($caMatches).Count + $licenses.Count + @($appRoleAssignments).Count + @($pimEligibility).Count + [int]$isTeamsGroup $riskLevel = switch ($true) { { $totalDependencies -eq 0 } { 'None'; break } { @($pimEligibility).Count -gt 0 -or ($caMatches | Where-Object State -EQ 'enabled') } { 'Critical'; break } { $totalDependencies -ge 3 } { 'High'; break } { $totalDependencies -ge 2 } { 'Medium'; break } default { 'Low' } } [pscustomobject]@{ GroupId = $GroupId DisplayName = $group.displayName IsRoleAssignable = [bool]$group.isAssignableToRole IsTeamsGroup = $isTeamsGroup ConditionalAccessPolicies = @($caMatches) AssignedLicenseSkuIds = @($licenses.skuId) AppRoleAssignments = @($appRoleAssignments | Select-Object resourceDisplayName, appRoleId) PimEligibleRoleCount = @($pimEligibility).Count TotalDependencyCount = $totalDependencies RiskLevel = $riskLevel } } |