Src/Private/Get-AbrEntraIDRoles.ps1
|
function Get-AbrEntraIDRoles { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory)] [string]$TenantId ) begin { Write-PScriboMessage -Message "Collecting Entra ID Role Assignments for tenant $TenantId." Show-AbrDebugExecutionTime -Start -TitleMessage 'Directory Roles' } process { Section -Style Heading2 'Directory Roles' { Paragraph "The following section documents the Entra ID directory role assignments for tenant $TenantId, including ACSC Essential Eight Maturity Level compliance checks." BlankLine try { Write-Host " - Retrieving active directory role assignments..." $RoleAssignments = Get-MgRoleManagementDirectoryRoleAssignment -All ` -ExpandProperty RoleDefinition ` -ErrorAction Stop # Resolve each principal using typed endpoints (typed calls return consistent # displayName/UPN unlike Get-MgDirectoryObject AdditionalProperties) $script:PrincipalCache = @{} foreach ($Assignment in $RoleAssignments) { $PrincipalId = $Assignment.PrincipalId if ($script:PrincipalCache.ContainsKey($PrincipalId)) { continue } $resolved = $null # Try User try { $U = Get-MgUser -UserId $PrincipalId -Property 'id,displayName,userPrincipalName,accountEnabled,assignedLicenses,mail' -ErrorAction SilentlyContinue if ($U) { $resolved = [pscustomobject]@{ Kind = 'User' DisplayName= $U.DisplayName UPN = $U.UserPrincipalName Enabled = $U.AccountEnabled Licensed = ($U.AssignedLicenses.Count -gt 0) HasMailbox = ($null -ne $U.Mail -and $U.Mail -ne '') Mail = $U.Mail } } } catch {} if (-not $resolved) { # Try Service Principal try { $SP = Get-MgServicePrincipal -ServicePrincipalId $PrincipalId -Property 'id,displayName,appId' -ErrorAction SilentlyContinue if ($SP) { $resolved = [pscustomobject]@{ Kind = 'Service Principal' DisplayName= $SP.DisplayName UPN = "AppId: $($SP.AppId)" Enabled = $true Licensed = $false HasMailbox = $false Mail = '--' } } } catch {} } if (-not $resolved) { # Try Group try { $G = Get-MgGroup -GroupId $PrincipalId -Property 'id,displayName,mail' -ErrorAction SilentlyContinue if ($G) { $resolved = [pscustomobject]@{ Kind = 'Group' DisplayName= $G.DisplayName UPN = if ($G.Mail) { $G.Mail } else { '--' } Enabled = $true Licensed = $false HasMailbox = $false Mail = '--' } } } catch {} } if (-not $resolved) { $resolved = [pscustomobject]@{ Kind = 'Unknown/Foreign' DisplayName= "Unknown ($PrincipalId)" UPN = '--' Enabled = $null Licensed = $false HasMailbox = $false Mail = '--' } } $script:PrincipalCache[$PrincipalId] = $resolved } if ($RoleAssignments) { $PrivilegedRoles = @( 'Global Administrator','Privileged Role Administrator','Security Administrator', 'Exchange Administrator','SharePoint Administrator','Conditional Access Administrator', 'Authentication Administrator','Helpdesk Administrator','User Administrator', 'Application Administrator','Cloud Application Administrator', 'Billing Administrator','Password Administrator' ) # --- Summary --- $UniqueRoles = ($RoleAssignments | Select-Object -ExpandProperty RoleDefinition | Select-Object -ExpandProperty DisplayName -Unique | Measure-Object).Count $UniquePrincipals = ($RoleAssignments | Select-Object -ExpandProperty PrincipalId -Unique | Measure-Object).Count $PrivAssignments = @($RoleAssignments | Where-Object { $PrivilegedRoles -contains $_.RoleDefinition.DisplayName }).Count $GACount = @($RoleAssignments | Where-Object { $_.RoleDefinition.DisplayName -eq 'Global Administrator' }).Count $RoleSumObj = [System.Collections.ArrayList]::new() $roleSumInObj = [ordered] @{ 'Total Role Assignments' = @($RoleAssignments).Count 'Unique Roles Assigned' = $UniqueRoles 'Unique Principals with Roles' = $UniquePrincipals 'Privileged Role Assignments' = $PrivAssignments 'Global Administrators' = $GACount } $RoleSumObj.Add([pscustomobject]$roleSumInObj) | Out-Null $null = (& { if ($HealthCheck.EntraID.Roles) { $null = ($RoleSumObj | Where-Object { [int]$_.'Global Administrators' -gt 5 } | Set-Style -Style Critical | Out-Null) $null = ($RoleSumObj | Where-Object { [int]$_.'Privileged Role Assignments' -gt 10 } | Set-Style -Style Warning | Out-Null) } }) $RoleSumTableParams = @{ Name = "Role Assignment Summary - $TenantId"; List = $true; ColumnWidths = 55, 45 } if ($Report.ShowTableCaptions) { $RoleSumTableParams['Caption'] = "- $($RoleSumTableParams.Name)" } $RoleSumObj | Table @RoleSumTableParams # --- All Role Assignments --- $RoleObj = [System.Collections.ArrayList]::new() foreach ($Assignment in ($RoleAssignments | Sort-Object { $_.RoleDefinition.DisplayName })) { try { $RoleName = $Assignment.RoleDefinition.DisplayName $IsPriv = $PrivilegedRoles -contains $RoleName $P = $script:PrincipalCache[$Assignment.PrincipalId] $roleInObj = [ordered] @{ 'Role Name' = $RoleName 'Privileged' = if ($IsPriv) { 'Yes' } else { 'No' } 'Principal Name' = if ($P) { $P.DisplayName } else { $Assignment.PrincipalId } 'Principal UPN' = if ($P) { $P.UPN } else { '--' } 'Principal Type' = if ($P) { $P.Kind } else { 'Unknown' } 'Directory Scope' = if ($Assignment.DirectoryScopeId -and $Assignment.DirectoryScopeId -ne '/') { $Assignment.DirectoryScopeId } else { 'Tenant-wide (/)' } } $RoleObj.Add([pscustomobject](ConvertTo-HashToYN $roleInObj)) | Out-Null } catch { Write-PScriboMessage -IsWarning -Message "Role assignment processing: $($_.Exception.Message)" } } $null = (& { if ($HealthCheck.EntraID.Roles) { $null = ($RoleObj | Where-Object { $_.'Privileged' -eq 'Yes' } | Set-Style -Style Warning | Out-Null) $null = ($RoleObj | Where-Object { $_.'Role Name' -eq 'Global Administrator' } | Set-Style -Style Critical | Out-Null) } }) $RoleTableParams = @{ Name = "Directory Role Assignments - $TenantId"; List = $false; ColumnWidths = 22, 10, 20, 22, 14, 12 } if ($Report.ShowTableCaptions) { $RoleTableParams['Caption'] = "- $($RoleTableParams.Name)" } $RoleObj | Table @RoleTableParams $null = ($script:ExcelSheets['Role Assignments'] = $RoleObj) #region Privileged Role Distribution Bar Chart if (Get-Command New-AbrHorizontalBarChart -ErrorAction SilentlyContinue) { try { $RoleGroups = $RoleAssignments | Group-Object { $_.RoleDefinition.DisplayName } | Sort-Object Count -Descending | Select-Object -First 12 if ($RoleGroups) { $roleBarItems = $RoleGroups | ForEach-Object { $isPriv = $PrivilegedRoles -contains $_.Name @{ Label = $_.Name; Value = $_.Count Color = if ($_.Name -eq 'Global Administrator') { '#c0392b' } elseif ($isPriv) { '#e87722' } else { '#1a6eb5' } } } $roleBarB64 = New-AbrHorizontalBarChart ` -Items $roleBarItems ` -Title "Directory Role Assignments -- $TenantId" if ($roleBarB64) { BlankLine $null = (Image -Text 'Role Distribution' -Base64 $roleBarB64 -Percent 80 -Align Center) BlankLine } } } catch { Write-AbrDebugLog "Role bar chart failed: $($_.Exception.Message)" 'WARN' 'CHART' } } #endregion # --- Global Admins + ACSC E8 --- if ($InfoLevel.Roles -ge 1) { $GlobalAdmins = @($RoleAssignments | Where-Object { $_.RoleDefinition.DisplayName -eq 'Global Administrator' }) $GlobalAdminCount = $GlobalAdmins.Count if ($GlobalAdmins.Count -gt 0) { Section -Style Heading3 'Global Administrators' { Paragraph "The following $($GlobalAdmins.Count) principal(s) hold the Global Administrator role in tenant $TenantId." BlankLine Paragraph "ACSC Essential Eight: GA accounts must be dedicated cloud-only accounts with no licence, no mailbox, and must not be used for daily activity. Maximum 5 Global Administrators recommended." BlankLine $GAObj = [System.Collections.ArrayList]::new() foreach ($GA in ($GlobalAdmins | Sort-Object { ($script:PrincipalCache[$_.PrincipalId]).DisplayName })) { $P = $script:PrincipalCache[$GA.PrincipalId] $HasLicence = if ($P) { $P.Licensed } else { $false } $HasMailbox = if ($P) { $P.HasMailbox } else { $false } $AccEnabled = if ($P) { $P.Enabled } else { $null } $gaInObj = [ordered] @{ 'Display Name' = if ($P) { $P.DisplayName } else { "Unknown ($($GA.PrincipalId))" } 'UPN' = if ($P) { $P.UPN } else { '--' } 'Principal Type' = if ($P) { $P.Kind } else { 'Unknown' } 'Account Enabled' = if ($null -ne $AccEnabled) { $AccEnabled } else { '--' } 'Licence (E8: None)' = if ($HasLicence) { '[FAIL] Has licence' } else { '[OK] No licence' } 'Mailbox (E8: None)' = if ($HasMailbox) { '[FAIL] Has mailbox' } else { '[OK] No mailbox' } } $GAObj.Add([pscustomobject](ConvertTo-HashToYN $gaInObj)) | Out-Null } $null = (& { if ($HealthCheck.EntraID.Roles) { if ($GlobalAdmins.Count -gt 5) { $null = ($GAObj | Set-Style -Style Critical | Out-Null) } elseif ($GlobalAdmins.Count -gt 2) { $null = ($GAObj | Set-Style -Style Warning | Out-Null) } $null = ($GAObj | Where-Object { $_.'Licence (E8: None)' -like '*FAIL*' } | Set-Style -Style Critical | Out-Null) $null = ($GAObj | Where-Object { $_.'Mailbox (E8: None)' -like '*FAIL*' } | Set-Style -Style Critical | Out-Null) } }) $GATableParams = @{ Name = "Global Administrators - $TenantId"; List = $false; ColumnWidths = 18, 22, 13, 11, 18, 18 } if ($Report.ShowTableCaptions) { $GATableParams['Caption'] = "- $($GATableParams.Name)" } $GAObj | Table @GATableParams $null = ($script:ExcelSheets['Global Admins'] = $GAObj) # ACSC E8 Assessment table BlankLine Paragraph "ACSC Essential Eight Maturity Level Assessment -- Global Administrator Accounts:" BlankLine $GALicensed = @($GAObj | Where-Object { $_.'Licence (E8: None)' -like '*FAIL*' }).Count $GAMailbox = @($GAObj | Where-Object { $_.'Mailbox (E8: None)' -like '*FAIL*' }).Count $GANonUser = @($GlobalAdmins | Where-Object { ($script:PrincipalCache[$_.PrincipalId]).Kind -ne 'User' }).Count # Cloud-only check: admins synced from on-prem $AllPrivRoles = @($RoleAssignments | Where-Object { $_.RoleDefinition.DisplayName -in @( 'Global Administrator','Privileged Role Administrator','Security Administrator', 'Exchange Administrator','SharePoint Administrator','User Administrator', 'Conditional Access Administrator','Authentication Policy Administrator' )}) $SyncedAdminCount = 0 foreach ($RA in $AllPrivRoles) { $P2 = $script:PrincipalCache[$RA.PrincipalId] if ($P2 -and $P2.PSObject.Properties['OnPremSynced'] -and $P2.OnPremSynced) { $SyncedAdminCount++ } } # Break-glass count: GAs excluded from ALL enabled CA policies $BreakGlassCount = 0 try { $AllCAPolicies2 = Get-MgIdentityConditionalAccessPolicy -All -ErrorAction SilentlyContinue $EnabledCA = @($AllCAPolicies2 | Where-Object { $_.State -eq 'enabled' }) $TotalEnabled2 = $EnabledCA.Count if ($TotalEnabled2 -gt 0) { $ExclMap = @{} foreach ($CAP in $EnabledCA) { foreach ($Uid in $CAP.Conditions.Users.ExcludeUsers) { if (-not $ExclMap.ContainsKey($Uid)) { $ExclMap[$Uid] = 0 } $ExclMap[$Uid]++ } } $BreakGlassCount = @($ExclMap.GetEnumerator() | Where-Object { $_.Value -eq $TotalEnabled2 }).Count } } catch {} #region ACSC E8 Assessment (definitions from Src/Compliance/ACSC.E8.json) $_ComplianceVars = @{ 'GlobalAdmins' = $GlobalAdmins 'GlobalAdminCount' = $GlobalAdminCount 'SyncedAdminCount' = $SyncedAdminCount 'BreakGlassCount' = $BreakGlassCount 'GALicensed' = $GALicensed 'GAMailbox' = $GAMailbox 'GANonUser' = $GANonUser 'GALicensedDetail' = $GALicensedDetail 'GAMailboxDetail' = $GAMailboxDetail 'GANonUserDetail' = $GANonUserDetail 'GACountRemediation' = $GACountRemediation } $E8Checks = Build-AbrComplianceChecks ` -Definitions (Get-AbrE8Checks -Section 'Roles') ` -Framework E8 ` -CallerVariables $_ComplianceVars New-AbrE8AssessmentTable -Checks $E8Checks -Name 'GA Assessment' -TenantId $TenantId # Consolidated into ACSC E8 Assessment sheet if ($E8Checks) { $null = $script:E8AllChecks.AddRange([object[]](@($E8Checks | Select-Object @{N='Section';E={'Roles & Privileged Access'}}, ML, Control, Status, Detail ))) } #endregion if ($script:IncludeCISBaseline) { BlankLine Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- Global Administrator Accounts:" BlankLine #region CIS Assessment (definitions from Src/Compliance/CIS.M365.json) $CISGAChecks = Build-AbrComplianceChecks ` -Definitions (Get-AbrCISChecks -Section 'Roles') ` -Framework CIS ` -CallerVariables $_ComplianceVars New-AbrCISAssessmentTable -Checks $CISGAChecks -Name 'Global Administrators' -TenantId $TenantId # Consolidated into CIS Assessment sheet if ($CISGAChecks) { $null = $script:CISAllChecks.AddRange([object[]](@($CISGAChecks | Select-Object @{N='Section';E={'Roles & Privileged Access'}}, CISControl, Level, Status, Detail ))) } #endregion } } } } # --- PIM Eligible (InfoLevel 2 only) --- if ($InfoLevel.Roles -ge 2) { try { Write-Host " - Retrieving PIM eligible role assignments..." $PIMEligible = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -All ` -ExpandProperty RoleDefinition -ErrorAction SilentlyContinue if ($PIMEligible) { foreach ($Elig in $PIMEligible) { $PrincipalId = $Elig.PrincipalId if (-not $script:PrincipalCache.ContainsKey($PrincipalId)) { try { $U = Get-MgUser -UserId $PrincipalId -Property 'id,displayName,userPrincipalName' -ErrorAction SilentlyContinue if ($U) { $script:PrincipalCache[$PrincipalId] = [pscustomobject]@{ Kind='User'; DisplayName=$U.DisplayName; UPN=$U.UserPrincipalName } } } catch {} } } } if ($PIMEligible -and @($PIMEligible).Count -gt 0) { Section -Style Heading3 'PIM Eligible Role Assignments' { Paragraph "The following principals have PIM-eligible (just-in-time) role assignments in tenant $TenantId. PIM is an ACSC Essential Eight ML3 control -- all privileged access should be time-bound and require activation." BlankLine $PimObj = [System.Collections.ArrayList]::new() foreach ($Elig in ($PIMEligible | Sort-Object { $_.RoleDefinition.DisplayName })) { $P = $script:PrincipalCache[$Elig.PrincipalId] $pimInObj = [ordered] @{ 'Role Name' = $Elig.RoleDefinition.DisplayName 'Principal Name' = if ($P) { $P.DisplayName } else { $Elig.PrincipalId } 'Principal UPN' = if ($P -and $P.UPN) { $P.UPN } else { '--' } 'Schedule Start' = if ($Elig.ScheduleInfo.StartDateTime) { ($Elig.ScheduleInfo.StartDateTime).ToString('yyyy-MM-dd') } else { '--' } 'Schedule End' = if ($Elig.ScheduleInfo.Expiration.EndDateTime) { ($Elig.ScheduleInfo.Expiration.EndDateTime).ToString('yyyy-MM-dd') } else { 'No Expiry' } 'Status' = $Elig.Status } $PimObj.Add([pscustomobject]$pimInObj) | Out-Null } $PimTableParams = @{ Name = "PIM Eligible Assignments - $TenantId"; List = $false; ColumnWidths = 22, 20, 24, 12, 12, 10 } if ($Report.ShowTableCaptions) { $PimTableParams['Caption'] = "- $($PimTableParams.Name)" } $PimObj | Table @PimTableParams $null = ($script:ExcelSheets['PIM Eligible'] = $PimObj) } } } catch { Write-AbrSectionError -Section 'PIM Eligible Assignments' -Message "$($_.Exception.Message)" } } # end InfoLevel.Roles -ge 2 } else { Paragraph "No directory role assignments found in tenant $TenantId." } } catch { Write-AbrSectionError -Section 'Directory Roles section' -Message "$($_.Exception.Message)" } } # end Section Directory Roles } end { Show-AbrDebugExecutionTime -End -TitleMessage 'Directory Roles' } } |