Public/Reconnaissance/Get-EntraRoleMember.ps1
using namespace System.Management.Automation class EntraRoleNames : IValidateSetValuesGenerator { [string[]] GetValidValues() { try { if ($null -ne $script:SessionVariables -and $null -ne $script:SessionVariables.Roles) { return ($script:SessionVariables.Roles | Select-Object -ExpandProperty DisplayName | Sort-Object) } else { return @('Global Administrator', 'User Administrator', 'Privileged Role Administrator') } } catch { Write-Warning "Error retrieving role names: $_" return @('ErrorLoadingRoleNames') } } } function Get-PrincipalDetails { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string[]]$PrincipalIds, [Parameter(Mandatory = $true)] [hashtable]$ResultHashtable ) if ($PrincipalIds.Count -eq 1) { $principalId = $PrincipalIds[0] try { $objectInfo = Invoke-MsGraph -relativeUrl "directoryObjects/$principalId" -NoBatch -OutputFormat Object -ErrorAction SilentlyContinue if ($objectInfo) { $principalType = "Unknown" if ($objectInfo.'@odata.type' -match '#microsoft.graph.user') { $principalType = "User" } elseif ($objectInfo.'@odata.type' -match '#microsoft.graph.group') { $principalType = "Group" } elseif ($objectInfo.'@odata.type' -match '#microsoft.graph.servicePrincipal') { $principalType = "ServicePrincipal" } $ResultHashtable[$principalId] = @{ Type = $principalType Details = $objectInfo } return } } catch { Write-Verbose "DirectoryObjects endpoint failed for $principalId, trying individual endpoints" } try { $userInfo = Invoke-MsGraph -relativeUrl "users/$principalId" -NoBatch -OutputFormat Object -ErrorAction Stop $ResultHashtable[$principalId] = @{ Type = "User" Details = $userInfo } return } catch { Write-Verbose "User endpoint failed for $principalId" } try { $groupInfo = Invoke-MsGraph -relativeUrl "groups/$principalId" -NoBatch -OutputFormat Object -ErrorAction Stop $ResultHashtable[$principalId] = @{ Type = "Group" Details = $groupInfo } return } catch { Write-Verbose "Group endpoint failed for $principalId" } try { $spInfo = Invoke-MsGraph -relativeUrl "servicePrincipals/$principalId" -NoBatch -OutputFormat Object -ErrorAction Stop $ResultHashtable[$principalId] = @{ Type = "ServicePrincipal" Details = $spInfo } return } catch { Write-Verbose "ServicePrincipal endpoint failed for $principalId" } $ResultHashtable[$principalId] = @{ Type = "Unknown" Details = $null } return } $batchRequests = [System.Collections.Generic.List[hashtable]]::new() foreach ($principalId in $PrincipalIds) { $batchRequests.Add(@{ id = $principalId method = "GET" url = "/directoryObjects/$principalId" }) } if ($batchRequests.Count -gt 0) { $batchResults = Invoke-MsGraph -BatchRequests $batchRequests -ErrorAction SilentlyContinue foreach ($principalId in $PrincipalIds) { $result = $batchResults[$principalId] if ($result -and $result.Success -eq $true) { $objectInfo = $result.Data $principalType = "Unknown" if ($objectInfo.'@odata.type' -match '#microsoft.graph.user') { $principalType = "User" } elseif ($objectInfo.'@odata.type' -match '#microsoft.graph.group') { $principalType = "Group" } elseif ($objectInfo.'@odata.type' -match '#microsoft.graph.servicePrincipal') { $principalType = "ServicePrincipal" } $ResultHashtable[$principalId] = @{ Type = $principalType Details = $objectInfo } } else { $wasFound = $false try { $userInfo = Get-MgUser -UserId $principalId -ErrorAction Stop $ResultHashtable[$principalId] = @{ Type = "User" Details = $userInfo } $wasFound = $true } catch { Write-Verbose "Principal $principalId not found as user" } if (-not $wasFound) { try { $groupInfo = Get-MgGroup -GroupId $principalId -ErrorAction Stop $ResultHashtable[$principalId] = @{ Type = "Group" Details = $groupInfo } $wasFound = $true } catch { Write-Verbose "Principal $principalId not found as group" } } if (-not $wasFound) { try { $spInfo = Get-MgServicePrincipal -ServicePrincipalId $principalId -ErrorAction Stop $ResultHashtable[$principalId] = @{ Type = "ServicePrincipal" Details = $spInfo } $wasFound = $true } catch { Write-Verbose "Principal $principalId not found as service principal" } } if (-not $wasFound) { $ResultHashtable[$principalId] = @{ Type = "Unknown" Details = $null } } } } } } function Get-EntraRoleMember { [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0)] [ValidateNotNullOrEmpty()] [ValidateSet([EntraRoleNames])] [string]$RoleName = "Global Administrator", [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$RoleId, [Parameter(Mandatory = $false)] [switch]$ShowSummary, [Parameter(Mandatory = $false)] [ValidateSet("Object", "JSON", "CSV", "Table")] [string]$OutputFormat = "Table" ) begin { Write-Verbose "Starting function $($MyInvocation.MyCommand.Name)" $MyInvocation.MyCommand.Name | Invoke-BlackCat -ResourceTypeName 'MSGraph' $startTime = Get-Date $script:RoleName = $RoleName $script:RoleId = $RoleId $script:roleMembers = $null $script:principalTypes = @{ "User" = 0 "Group" = 0 "ServicePrincipal" = 0 "Unknown" = 0 } } process { try { if (-not $RoleId) { $roleDefinition = $null # Check if session variables are available and populated if ($null -eq $script:SessionVariables -or $null -eq $script:SessionVariables.Roles -or $script:SessionVariables.Roles.Count -lt 10) { Write-Verbose "Role session variables not available or incomplete. Attempting to initialize them..." # Check if we have an access token before trying to update roles if ($script:SessionVariables -and $script:SessionVariables.AccessToken) { Write-Verbose "Access token exists, refreshing Entra role definitions..." try { $refreshSuccess = Update-EntraRoles -ErrorAction Stop if (-not $refreshSuccess) { Write-Warning "Failed to initialize role definitions" } } catch { Write-Warning "Error initializing role definitions: $_" } } else { Write-Warning "No access token found. You must authenticate first with Connect-Entra or Connect-MSGraph" } } # Now try to use the roles (whether they were just refreshed or already existed) if ($null -ne $script:SessionVariables -and $null -ne $script:SessionVariables.Roles) { $roleDefinition = $script:SessionVariables.Roles | Where-Object { $_.DisplayName -eq $RoleName } | Select-Object -First 1 if ($roleDefinition) { $roleId = $roleDefinition.Id } else { Write-Warning "Could not find role definition for: $RoleName" throw "Role '$RoleName' not found. Check the role name or provide a role ID directly." } } else { throw "Session variables for roles not available. Ensure you're connected with Connect-Entra before calling this function." } Write-Host "Using role: $RoleName (ID: $roleId)" -ForegroundColor Cyan } else { $roleId = $RoleId $roleDefinition = $null # Check if session variables need to be initialized if ($null -eq $script:SessionVariables -or $null -eq $script:SessionVariables.Roles -or $script:SessionVariables.Roles.Count -lt 10) { Write-Verbose "Role session variables not available or incomplete. Attempting to initialize them..." # Check if we have an access token before trying to update roles if ($script:SessionVariables -and $script:SessionVariables.AccessToken) { Write-Verbose "Access token exists, refreshing Entra role definitions..." try { $refreshSuccess = Update-EntraRoles -ErrorAction Stop if (-not $refreshSuccess) { Write-Warning "Failed to initialize role definitions" } } catch { Write-Warning "Error initializing role definitions: $_" } } else { Write-Warning "No access token found. You must authenticate first with Connect-Entra or Connect-MSGraph" } } # Try to use the role ID to get the display name if ($null -ne $script:SessionVariables -and $null -ne $script:SessionVariables.Roles) { $roleDefinition = $script:SessionVariables.Roles | Where-Object { $_.Id -eq $roleId } | Select-Object -First 1 if ($roleDefinition) { $RoleName = $roleDefinition.DisplayName Write-Host "Using role: $RoleName (ID: $roleId)" -ForegroundColor Cyan } else { Write-Host "Using role with ID: $roleId" -ForegroundColor Cyan } } else { Write-Host "Using role with ID: $roleId" -ForegroundColor Cyan } } $script:RoleName = $RoleName $script:RoleId = $roleId try { $allRoleAssignments = Invoke-MsGraph -relativeUrl "roleManagement/directory/roleAssignments" -OutputFormat Object if (-not $allRoleAssignments -or $allRoleAssignments.Count -eq 0) { Write-Warning "No role assignments were returned. This may be due to permissions issues." throw "No role assignments found. Check that you have Directory.Read.All permissions." } $targetRoleAssignments = $allRoleAssignments | Where-Object { $_.roleDefinitionId -eq $roleId } if (-not $targetRoleAssignments -or $targetRoleAssignments.Count -eq 0) { Write-Host "No $RoleName assignments found" -ForegroundColor Yellow return $null } Write-Host "Found $($targetRoleAssignments.Count) $RoleName assignments" -ForegroundColor Cyan } catch { Write-Warning "Error retrieving role assignments: $($_.Exception.Message)" throw "Failed to retrieve role assignments. Check your permissions and network connectivity." } $script:roleMembers = [System.Collections.Generic.List[PSCustomObject]]::new() $script:principalTypes = @{ "User" = 0 "Group" = 0 "ServicePrincipal" = 0 "Unknown" = 0 } $principalIdToAssignment = @{} foreach ($assignment in $targetRoleAssignments) { $principalId = $assignment.principalId if ($principalId -match '^[0-9]{1,2}$' -or $principalId.Length -lt 5) { Write-Verbose "Skipping invalid principal ID: $principalId" continue } $principalIdToAssignment[$principalId] = $assignment } $uniquePrincipalIds = @($principalIdToAssignment.Keys) $script:roleMembers = [System.Collections.Generic.List[PSCustomObject]]::new() $principalDetails = @{} $script:principalTypes = @{ "User" = 0 "Group" = 0 "ServicePrincipal" = 0 "Unknown" = 0 } $batchSize = 20 for ($i = 0; $i -lt $uniquePrincipalIds.Count; $i += $batchSize) { $batchPrincipalIds = $uniquePrincipalIds[$i..([Math]::Min($i + $batchSize - 1, $uniquePrincipalIds.Count - 1))] Get-PrincipalDetails -PrincipalIds $batchPrincipalIds -ResultHashtable $principalDetails foreach ($principalId in $batchPrincipalIds) { if ($principalDetails.ContainsKey($principalId)) { $script:principalTypes[$principalDetails[$principalId].Type]++ } else { $script:principalTypes["Unknown"]++ } } foreach ($principalId in $batchPrincipalIds) { $principalInfo = $principalDetails[$principalId] $assignment = $principalIdToAssignment[$principalId] $details = $principalInfo.Details $isUnknown = ($principalInfo.Type -eq "Unknown" -or $null -eq $details) $roleMember = [PSCustomObject]@{ PrincipalId = $principalId PrincipalType = $isUnknown ? "Unknown" : $principalInfo.Type DisplayName = $isUnknown ? "Possibly Deleted or Inaccessible Object" : $details.displayName UserPrincipalName = ($principalInfo.Type -eq "User" -and $details) ? $details.userPrincipalName : $null Email = $details ? $details.mail : $null AccountEnabled = ($principalInfo.Type -eq "User" -and $details) ? $details.accountEnabled : $null AssignmentId = $assignment.id AssignmentScope = $assignment.directoryScopeId RoleName = $RoleName RoleId = $roleId Status = $isUnknown ? "Possibly Deleted or Inaccessible" : "Active" } $script:roleMembers.Add($roleMember) } } if ($ShowSummary) { $duration = (Get-Date) - $startTime Write-Host "`n📊 Role Member Discovery Summary:" -ForegroundColor Magenta Write-Host " Role: $RoleName (ID: $roleId)" -ForegroundColor Cyan Write-Host " Total Members Found: $($script:roleMembers.Count)" -ForegroundColor Green $principalTypeSummary = $script:roleMembers | Group-Object PrincipalType foreach ($group in $principalTypeSummary) { $color = switch ($group.Name) { "User" { "Green" } "Group" { "Yellow" } "ServicePrincipal" { "Cyan" } "Unknown" { "Red" } default { "White" } } Write-Host " $($group.Name): $($group.Count)" -ForegroundColor $color } if ($script:principalTypes["User"] -gt 0) { $enabledUsers = $script:roleMembers | Where-Object { $_.PrincipalType -eq "User" -and $_.AccountEnabled -eq $true } | Measure-Object $disabledUsers = $script:roleMembers | Where-Object { $_.PrincipalType -eq "User" -and $_.AccountEnabled -eq $false } | Measure-Object if ($enabledUsers.Count -gt 0) { Write-Host " Enabled Users: $($enabledUsers.Count)" -ForegroundColor Green } if ($disabledUsers.Count -gt 0) { Write-Host " Disabled Users: $($disabledUsers.Count)" -ForegroundColor Yellow } } $directoryScopes = $script:roleMembers | Where-Object { $_.AssignmentScope -ne "/" } | Measure-Object if ($directoryScopes.Count -gt 0) { Write-Host " Scoped Assignments: $($directoryScopes.Count)" -ForegroundColor Yellow } if ($script:principalTypes["Group"] -gt 0) { Write-Host "`n⚠️ Note: Group members also inherit this role but are not included in this count" -ForegroundColor Yellow } Write-Host " Duration: $($duration.TotalSeconds.ToString('F2')) seconds" -ForegroundColor White Write-Host " Processing Rate: $([math]::Round($script:roleMembers.Count / $duration.TotalSeconds, 2)) principals/second" -ForegroundColor White Write-Host "`n✅ Role member analysis completed successfully!" -ForegroundColor Green } $processingRate = if ($script:roleMembers.Count -gt 0 -and $duration.TotalSeconds -gt 0) { [math]::Round($script:roleMembers.Count / $duration.TotalSeconds, 2) } else { 0 } Write-Verbose "Processed $($script:roleMembers.Count) role members at $processingRate items/second" $formatParam = @{ Data = $script:roleMembers OutputFormat = $OutputFormat FunctionName = $MyInvocation.MyCommand.Name FilePrefix = "$($RoleName.Replace(' ', ''))-Members" } try { return Format-BlackCatOutput @formatParam } catch { Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message "Error formatting output: $($_.Exception.Message)" -Severity 'Warning' return $script:roleMembers } } catch { Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message $($_.Exception.Message) -Severity 'Error' return $null } } # End of process block end { # Calculate performance metrics $endTime = Get-Date $duration = $endTime - $startTime $totalSeconds = $duration.TotalSeconds # Log completion metrics if (-not $ShowSummary) { Write-Verbose "Function completed in $($totalSeconds.ToString('F2')) seconds" # Write detailed metrics to log if available $metrics = @{ FunctionName = $MyInvocation.MyCommand.Name DurationSeconds = $totalSeconds RoleName = $script:RoleName RoleId = $script:RoleId MemberCount = $script:roleMembers?.Count ?? 0 UserCount = $script:principalTypes["User"] GroupCount = $script:principalTypes["Group"] ServicePrincipalCount = $script:principalTypes["ServicePrincipal"] UnknownCount = $script:principalTypes["Unknown"] } } } <# .SYNOPSIS Gets all members of a specified Microsoft Entra ID (Azure AD) role. .DESCRIPTION The Get-EntraRoleMember function identifies all members (users, groups, or service principals) assigned to a specific Entra ID role. It first identifies the role definition by name or ID, then finds all assignments for that role, and determines the principal type and details for each assignment. .PARAMETER RoleName Specifies the display name of the Entra ID role to query. This parameter has tab completion for all available Entra ID roles. Default is "Global Administrator". .PARAMETER RoleId Specifies the ID of the Entra ID role to query. If provided, this takes precedence over RoleName. .PARAMETER ShowSummary When specified, displays a summary of the role members including counts by principal type and execution duration. .PARAMETER OutputFormat Specifies the output format of the results. Valid values are: - Object: Returns PowerShell objects (default for pipeline operations) - JSON: Returns a JSON string - CSV: Returns a CSV string - Table: Displays the results as a formatted table (default) .EXAMPLE Get-EntraRoleMember Retrieves all Global Administrators in the tenant (default role) and displays them in a table format. .EXAMPLE Get-EntraRoleMember -RoleName "User Administrator" -OutputFormat JSON Retrieves all User Administrators and exports the results to a JSON file. .EXAMPLE Get-EntraRoleMember -RoleName "Conditional Access Administrator" -ShowSummary Retrieves all Conditional Access Administrators and displays a summary of the results. .EXAMPLE Get-EntraRoleMember -RoleId "9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3" -OutputFormat Object Retrieves members of the role with the specified ID and returns them as PowerShell objects. #> } |