Public/Entra/Role/Get-MgRoleReport.ps1
|
<#
.SYNOPSIS Get-MgRoleReport.ps1 - Reports on Microsoft Entra ID (Azure AD) roles .DESCRIPTION By default, the report contains only the roles with members. To get all the role, included empty roles, add -IncludeEmptyRoles $true .OUTPUTS The report is output to an array contained all the audit logs found. To export in a csv, do Get-MgRoleReport | Export-CSV -NoTypeInformation "$(Get-Date -Format yyyyMMdd)_adminRoles.csv" -Encoding UTF8 .PARAMETER IncludeEmptyRoles Switch parameter to include empty roles in the report .PARAMETER IncludePIMEligibleAssignments Boolean parameter to include PIM eligible assignments in the report. Default is $true .PARAMETER ForceNewToken Switch parameter to force getting a new token from Microsoft Graph .PARAMETER MaesterMode Switch parameter to use with the Maester framework (internal process not presented here) .EXAMPLE Get-MgRoleReport Get all the roles with members, including PIM eligible assignments but without empty roles .EXAMPLE Get-MgRoleReport -IncludeEmptyRoles Get all the roles, including the ones without members .EXAMPLE Get-MgRoleReport -IncludePIMEligibleAssignments $false Get all the roles with members (without empty roles), but without PIM eligible assignments .EXAMPLE Get-MgRoleReport | Export-CSV -NoTypeInformation "$(Get-Date -Format yyyyMMdd)_adminRoles.csv" -Encoding UTF8 .LINK https://ps365.clidsys.com/docs/commands/Get-MgRoleReport .NOTES https://itpro-tips.com/get-the-office-365-admin-roles-and-track-the-changes/ Written by Bastien Perez (Clidsys.com - ITPro-Tips.com) For more Office 365/Microsoft 365 tips and news, check out ITPro-Tips.com. #> function Get-MgRoleReport { [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [switch]$IncludeEmptyRoles = $false, [Parameter(Mandatory = $false)] [boolean]$IncludePIMEligibleAssignments = $true, [Parameter(Mandatory = $false)] [switch]$ForceNewToken, # using with the Maester framework [Parameter(Mandatory = $false)] [switch]$MaesterMode ) [System.Collections.Generic.List[PSObject]]$rolesMembersArray = @() [System.Collections.Generic.List[Object]]$objectsCacheArray = @() [System.Collections.Generic.List[Object]]$mgRolesArrayAssignment = @() $modules = @( 'Microsoft.Graph.Authentication' 'Microsoft.Graph.Identity.Governance' 'Microsoft.Graph.Users' 'Microsoft.Graph.Groups' 'Microsoft.Graph.Beta.Reports' ) foreach ($module in $modules) { try { Import-Module $module -ErrorAction Stop } catch { Write-Warning "First, install module $module" return } } $isConnected = $false $isConnected = $null -ne (Get-MgContext -ErrorAction SilentlyContinue) if ($ForceNewToken.IsPresent) { Write-Verbose 'Disconnecting from Microsoft Graph' $null = Disconnect-MgGraph -ErrorAction SilentlyContinue $isConnected = $false } $scopes = (Get-MgContext).Scopes # Audit.Log.Read.All for sign-in activity # RoleManagement.Read.All for role assignment (PIM eligible and permanent) # Directory.Read.All for user and group and service principal information $permissionsNeeded = 'Directory.Read.All', 'RoleManagement.Read.All', 'AuditLog.Read.All' foreach ($permission in $permissionsNeeded) { if ($scopes -notcontains $permission) { Write-Verbose "You need to have the $permission permission in the current token, disconnect to force getting a new token with the right permissions" } } if (-not $isConnected) { Write-Verbose "Connecting to Microsoft Graph. Scopes: $permissionsNeeded" $null = Connect-MgGraph -Scopes $permissionsNeeded -NoWelcome } Write-Verbose 'Collecting roles with assignments...' try { #$mgRolesArrayAssignment = Get-MgRoleManagementDirectoryRoleDefinition -ErrorAction Stop Get-MgRoleManagementDirectoryRoleAssignment -All -ExpandProperty Principal | ForEach-Object { $mgRolesArrayAssignment.Add($_) } #$mgRolesArrayAssignment = (Invoke-MgGraphRequest -Method GET -Uri 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments' -OutputType PSObject).Value $mgRolesDefinition = Get-MgRoleManagementDirectoryRoleAssignment -All -ExpandProperty roleDefinition } catch { Write-Warning $($_.Exception.Message) } # In *Assignment, we don't have the role definition, so we need to get it and add it to the object foreach ($assignment in $mgRolesArrayAssignment) { # Add the role definition to the object Add-Member -InputObject $assignment -MemberType NoteProperty -Name RoleDefinitionExtended -Value ($mgRolesDefinition | Where-Object { $_.id -eq $assignment.id }).roleDefinition # Add-Member -InputObject $assignment -MemberType NoteProperty -Name RoleDefinitionExtended -Value ($mgRolesDefinition | Where-Object { $_.id -eq $assignment.id }).roleDefinition.description } if ($IncludePIMEligibleAssignments) { Write-Verbose 'Collecting PIM eligible role assignments...' try { (Get-MgRoleManagementDirectoryRoleEligibilitySchedule -All -ExpandProperty * -ErrorAction Stop | Select-Object id, principalId, directoryScopeId, roleDefinitionId, status, principal, @{Name = 'RoleDefinitionExtended'; Expression = { $_.roleDefinition } }) | ForEach-Object { $mgRolesArrayAssignment.Add($_) } #$mgRoles += (Invoke-MgGraphRequest -Method GET -Uri 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilitySchedule' -OutputType PSObject).Value } catch { Write-Warning "Unable to get PIM eligible role assignments: $($_.Exception.Message)" } } foreach ($assignment in $mgRolesArrayAssignment) { $principal = switch ($assignment.principal.AdditionalProperties.'@odata.type') { '#microsoft.graph.user' { $assignment.principal.AdditionalProperties.userPrincipalName; break } '#microsoft.graph.servicePrincipal' { $assignment.principal.AdditionalProperties.appId; break } '#microsoft.graph.group' { $assignment.principalid; break } 'default' { '-' } } $object = [PSCustomObject][ordered]@{ Principal = $principal PrincipalDisplayName = $assignment.principal.AdditionalProperties.displayName PrincipalType = $assignment.principal.AdditionalProperties.'@odata.type'.Split('.')[-1] PrincipalObjectID = $assignment.principal.id AssignedRole = $assignment.RoleDefinitionExtended.displayName AssignedRoleDefinitionId = $assignment.RoleDefinitionId AssignedRoleScope = $assignment.directoryScopeId AssignmentType = if ($assignment.status -eq 'Provisioned') { 'Eligible' } else { 'Permanent' } RoleIsBuiltIn = $assignment.RoleDefinitionExtended.isBuiltIn RoleTemplate = $assignment.RoleDefinitionExtended.templateId DirectMember = $true Recommendations = 'Check if the user has alternate email or alternate phone number on Microsoft Entra ID' RecommendationSync = $null } $rolesMembersArray.Add($object) if ($object.PrincipalType -eq 'group') { # need to get ID for Get-MgGroupMember $group = Get-MgGroup -GroupId $object.Principal -Property Id, onPremisesSyncEnabled $object | Add-Member -MemberType NoteProperty -Name 'OnPremisesSyncEnabled' -Value $([bool]($group.onPremisesSyncEnabled -eq $true)) #$group = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/groups/$($object.Principal)" -OutputType PSObject) $groupMembers = Get-MgGroupMember -GroupId $group.Id -Property displayName, userPrincipalName #$groupMembers = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/groups/$($group.Id)/members" -OutputType PSObject).Value foreach ($member in $groupMembers) { $typeMapping = @{ '#microsoft.graph.user' = 'user' '#microsoft.graph.group' = 'group' '#microsoft.graph.servicePrincipal' = 'servicePrincipal' '#microsoft.graph.device' = 'device' '#microsoft.graph.orgContact' = 'contact' '#microsoft.graph.application' = 'application' } $memberType = if ($typeMapping[$member.AdditionalProperties.'@odata.type']) { $typeMapping[$member.AdditionalProperties.'@odata.type'] } else { 'Unknown' } $object = [PSCustomObject][ordered]@{ Principal = $member.AdditionalProperties.userPrincipalName PrincipalDisplayName = $member.AdditionalProperties.displayName PrincipalType = $memberType AssignedRole = $assignment.RoleDefinitionExtended.displayName AssignedRoleScope = $assignment.directoryScopeId AssignmentType = if ($assignment.status -eq 'Provisioned') { 'Eligible' } else { 'Permanent' } RoleIsBuiltIn = $assignment.RoleDefinitionExtended.isBuiltIn RoleTemplate = $assignment.RoleDefinitionExtended.templateId DirectMember = $false Recommendations = 'Check if the user has alternate email or alternate phone number on Microsoft Entra ID' RecommendationSync = $null } $rolesMembersArray.Add($object) } } } $object = [PSCustomObject] [ordered]@{ Principal = 'Partners' PrincipalDisplayName = 'Partners' PrincipalType = 'Partners' AssignedRole = 'Partners' AssignedRoleScope = 'Partners' AssignmentType = 'Partners' RoleIsBuiltIn = 'Not applicable' RoleTemplate = 'Not applicable' DirectMember = 'Not applicable' Recommendations = 'Please check this URL to identify if you have partner with admin roles https: / / admin.microsoft.com / AdminPortal / Home#/partners. More information on https://practical365.com/identifying-potential-unwanted-access-by-your-msp-csp-reseller/' RecommendationSync = $null } $rolesMembersArray.Add($object) #foreach user, we check if the user is global administrator. If global administrator, we add a new parameter to the object recommandationRole to tell the other role is not useful $globalAdminsHash = @{} $rolesMembersArray | Where-Object { $_.AssignedRole -eq 'Global Administrator' } | ForEach-Object { $globalAdminsHash[$_.Principal] = $true } $rolesMembersArray | ForEach-Object { if ($globalAdminsHash.ContainsKey($_.Principal) -and $_.AssignedRole -ne 'Global Administrator') { $_ | Add-Member -MemberType NoteProperty -Name 'RecommandationRole' -Value 'This user is Global Administrator. The other role(s) is/are not useful' } else { $_ | Add-Member -MemberType NoteProperty -Name 'RecommandationRole' -Value '' } } foreach ($member in $rolesMembersArray) { Write-Verbose "Processing $($member.AssignedRole) - $($member.AssignedRole)" $lastSignInDateTime = $null $accountEnabled = $null $onPremisesSyncEnabled = $null if ($objectsCacheArray.Principal -contains $member.Principal) { $accountEnabled = ($objectsCacheArray | Where-Object { $_.Principal -eq $member.Principal }).AccountEnabled $lastSignInDateTime = ($objectsCacheArray | Where-Object { $_.Principal -eq $member.Principal }).LastSignInDateTime $lastNonInteractiveSignInDateTime = ($objectsCacheArray | Where-Object { $_.Principal -eq $member.Principal }).LastNonInteractiveSignInDateTime $onPremisesSyncEnabled = ($objectsCacheArray | Where-Object { $_.Principal -eq $member.Principal }).onPremisesSyncEnabled } else { $lastSignInActivity = $null switch ($member.PrincipalType) { 'user' { # If we use Get-MgUser -UserId $member.Principal -Property AccountEnabled, SignInActivity, onPremisesSyncEnabled, # we encounter the error 'Get-MgUser_Get: Get By Key only supports UserId, and the key must be a valid GUID'. # This is because the sign-in data comes from a different source that requires a GUID to retrieve the account's sign-in activity. # Therefore, we must provide the account's object identifier for the command to function correctly. # To overcome this issue, we use the -Filter parameter to search for the user by their UserPrincipalName. $mgUser = Get-MgUser -Filter "UserPrincipalName eq '$($member.Principal)'" -Property AccountEnabled, SignInActivity, onPremisesSyncEnabled $accountEnabled = $mgUser.AccountEnabled $lastSignInDateTime = $mgUser.signInActivity.LastSignInDateTime $lastNonInteractiveSignInDateTime = $mgUser.signInActivity.LastNonInteractiveSignInDateTime $onPremisesSyncEnabled = [bool]($mgUser.onPremisesSyncEnabled -eq $true) break } 'group' { $accountEnabled = 'Not applicable' $lastSignInDateTime = 'Not applicable' $lastNonInteractiveSignInDateTime = 'Not applicable' # onpremisesSyncEnabled already get from Get-MgGroup in the previous loop $onPremisesSyncEnabled = $member.OnPremisesSyncEnabled break } 'servicePrincipal' { $lastSignInActivity = (Get-MgBetaReportServicePrincipalSignInActivity -Filter "appId eq '$($member.Principal)'").LastSignInActivity $accountEnabled = 'Not applicable' $lastSignInDateTime = $lastSignInActivity.LastSignInDateTime $lastNonInteractiveSignInDateTime = $lastSignInActivity.LastNonInteractiveSignInDateTime $onPremisesSyncEnabled = $false break } 'Partners' { $accountEnabled = 'Not applicable' $lastSignInDateTime = 'Not applicable' $lastNonInteractiveSignInDateTime = 'Not applicable' $onPremisesSyncEnabled = 'Not applicable' break } 'default' { $accountEnabled = 'Not applicable' $lastSignInDateTime = 'Not applicable' $lastNonInteractiveSignInDateTime = 'Not applicable' $onPremisesSyncEnabled = 'Not applicable' } } } $member | Add-Member -MemberType NoteProperty -Name 'LastSignInDateTime' -Value $lastSignInDateTime $member | Add-Member -MemberType NoteProperty -Name 'LastNonInteractiveSignInDateTime' -Value $lastNonInteractiveSignInDateTime $member | Add-Member -MemberType NoteProperty -Name 'AccountEnabled' -Value $accountEnabled $member | Add-Member -MemberType NoteProperty -Name 'OnPremisesSyncEnabled' -Value $onPremisesSyncEnabled if($onPremisesSyncEnabled) { $member.RecommendationSync = 'Privileged accounts should be cloud-only.' } # only add if not already in the cache if (-not $objectsCacheArray.Principal -contains $member.Principal) { $objectsCacheArray.Add($member) } } if ($IncludeEmptyRoles.IsPresent) { Write-Verbose 'Collecting all roles...' try { #$mgRolesArrayAssignment = (Invoke-MgGraphRequest -Method GET -Uri 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions' -OutputType PSObject).Value $mgRolesDefinition = Get-MgRoleManagementDirectoryRoleDefinition -All -ErrorAction Stop $emptyRoles = $mgRolesDefinition | Where-Object { $mgRolesArrayAssignment.RoleDefinitionId -notcontains $_.id } foreach ($emptyRole in $emptyRoles) { $object = [PSCustomObject][ordered]@{ Principal = 'Role has no members' PrincipalDisplayName = $null PrincipalType = $null PrincipalObjectID = $null AssignedRole = $emptyRole.displayName AssignedRoleScope = $null AssignmentType = $null RoleIsBuiltIn = $emptyRole.isBuiltIn RoleTemplate = $emptyRole.templateId DirectMember = $null Recommendations = $null LastSignInDateTime = $null LastNonInteractiveSignInDateTime = $null AccountEnabled = $null OnPremisesSyncEnabled = $null RecommandationRole = $null } $rolesMembersArray.Add($object) } } catch { Write-Warning $($_.Exception.Message) } } return $rolesMembersArray } |