Entra/Get-EntraAdminRoleSeparationConfig.ps1
|
<# .SYNOPSIS Evaluates whether privileged admin accounts are separated from daily-use accounts by checking for Exchange Online mailbox plans on admin accounts. .DESCRIPTION Queries role assignments for high-privilege Entra ID roles (Global Admin, Privileged Role Admin, Security Admin, Exchange Admin, SharePoint Admin) then checks each assigned user's license details for Exchange Online service plans. An admin account with an Exchange mailbox is likely used for daily work, violating user/system management separation. Satisfies CMMC SC.L2-3.13.3. Requires an active Microsoft Graph connection with RoleManagement.Read.Directory and Directory.Read.All permissions. .PARAMETER OutputPath Optional path to export results as CSV. If not specified, results are returned to the pipeline. .EXAMPLE PS> .\Entra\Get-EntraAdminRoleSeparationConfig.ps1 Displays admin role separation evaluation results. .EXAMPLE PS> .\Entra\Get-EntraAdminRoleSeparationConfig.ps1 -OutputPath '.\entra-adminrole-separation.csv' Exports the evaluation to CSV. .NOTES Author: Daren9m CMMC: SC.L2-3.13.3 — Separate user functionality from system management #> [CmdletBinding()] param( [Parameter()] [ValidateNotNullOrEmpty()] [string]$OutputPath ) $ErrorActionPreference = 'Stop' $_scriptDir = if ($MyInvocation.MyCommand.Path) { Split-Path -Parent $MyInvocation.MyCommand.Path } else { $PSScriptRoot } . (Join-Path -Path $_scriptDir -ChildPath '..\Common\SecurityConfigHelper.ps1') $ctx = Initialize-SecurityConfig $settings = $ctx.Settings $checkIdCounter = $ctx.CheckIdCounter function Add-Setting { param( [string]$Category, [string]$Setting, [string]$CurrentValue, [string]$RecommendedValue, [string]$Status, [string]$CheckId = '', [string]$Remediation = '' ) $p = @{ Settings = $settings CheckIdCounter = $checkIdCounter Category = $Category Setting = $Setting CurrentValue = $CurrentValue RecommendedValue = $RecommendedValue Status = $Status CheckId = $CheckId Remediation = $Remediation } Add-SecuritySetting @p } # ------------------------------------------------------------------ # Well-known role template IDs for high-privilege roles # ------------------------------------------------------------------ $privilegedRoleIds = @( '62e90394-69f5-4237-9190-012177145e10' # Global Administrator 'e8611ab8-c189-46e8-94e1-60213ab1f814' # Privileged Role Administrator '194ae4cb-b126-40b2-bd5b-6091b380977d' # Security Administrator '29232cdf-9323-42fd-aeaf-7d3bbd031fae' # Exchange Administrator 'f28a1f50-f6e7-4571-818b-6a12f2af6b6c' # SharePoint Administrator ) # Exchange Online service plan GUIDs (Plan 1 and Plan 2) $exchangePlanIds = @( 'efb87545-963c-4e0d-99df-69c6916d9eb0' # Exchange Online Plan 1 '19ec0d23-8335-4cbd-94ac-6050e30712fa' # Exchange Online Plan 2 ) # ------------------------------------------------------------------ # 1. Collect unique user IDs assigned to any high-privilege role # ------------------------------------------------------------------ try { $adminUserIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($roleId in $privilegedRoleIds) { Write-Verbose "Checking assignments for role $roleId..." $assignParams = @{ Method = 'GET' Uri = "/v1.0/roleManagement/directory/roleAssignments?`$filter=roleDefinitionId eq '$roleId'&`$top=999" ErrorAction = 'Stop' } $assignments = Invoke-MgGraphRequest @assignParams if ($assignments -and $assignments['value']) { foreach ($a in @($assignments['value'])) { $principalId = $a['principalId'] if ($principalId) { [void]$adminUserIds.Add($principalId) } } } } if ($adminUserIds.Count -eq 0) { $settingParams = @{ Category = 'Admin Role Separation' Setting = 'Privileged Account vs Daily-Use Account Separation' CurrentValue = 'No privileged role assignments found' RecommendedValue = 'Admin accounts must not have Exchange mailbox service plans' Status = 'Pass' CheckId = 'ENTRA-ADMINROLE-SEPARATION-001' Remediation = 'Assign at least one user to Global Administrator or other privileged roles.' } Add-Setting @settingParams Export-SecurityConfigReport -Settings $settings -OutputPath $OutputPath -ServiceLabel 'Entra Admin Role Separation' return } # ------------------------------------------------------------------ # 2. Check each admin user for Exchange Online service plans # ------------------------------------------------------------------ $mixedAccounts = @() foreach ($userId in $adminUserIds) { Write-Verbose "Checking license details for user $userId..." $licParams = @{ Method = 'GET' Uri = "/v1.0/users/$userId/licenseDetails" ErrorAction = 'Stop' } $licDetails = Invoke-MgGraphRequest @licParams if (-not $licDetails -or -not $licDetails['value']) { continue } foreach ($sku in @($licDetails['value'])) { $planIds = @($sku['servicePlans'] | ForEach-Object { $_['servicePlanId'] }) $hasExchange = $planIds | Where-Object { $exchangePlanIds -contains $_ } if ($hasExchange) { $mixedAccounts += $userId break } } } $adminCount = $adminUserIds.Count if ($mixedAccounts.Count -eq 0) { $currentValue = "Admin accounts checked: $adminCount — none have Exchange Online plans" $status = 'Pass' } else { $currentValue = "$($mixedAccounts.Count) of $adminCount admin account(s) have Exchange Online mailbox plans" $status = 'Fail' } $settingParams = @{ Category = 'Admin Role Separation' Setting = 'Privileged Account vs Daily-Use Account Separation' CurrentValue = $currentValue RecommendedValue = 'Admin accounts must not have Exchange mailbox service plans' Status = $status CheckId = 'ENTRA-ADMINROLE-SEPARATION-001' Remediation = 'Create separate cloud-only admin accounts without Exchange Online licenses. Remove mailbox service plan assignments from privileged role accounts. Entra admin center > Users > select admin user > Licenses.' } Add-Setting @settingParams } catch { if ($_.Exception.Message -match '403|Forbidden|Authorization|Ensure the required|service is connected|Access_Denied|Authorization_RequestDenied') { $settingParams = @{ Category = 'Admin Role Separation' Setting = 'Privileged Account vs Daily-Use Account Separation' CurrentValue = 'Insufficient permissions' RecommendedValue = 'Admin accounts must not have Exchange mailbox service plans' Status = 'Review' CheckId = 'ENTRA-ADMINROLE-SEPARATION-001' Remediation = 'Requires RoleManagement.Read.Directory and Directory.Read.All permissions. Grant via Entra admin center or reconnect with additional scopes.' } Add-Setting @settingParams Write-Host '' Write-Host " $([char]0x26A0) Missing permission for Admin Role Separation check:" -ForegroundColor Yellow Write-Host ' Identity: RoleManagement.Read.Directory' -ForegroundColor Yellow Write-Host ' To fix: add the missing permission to your app registration, then grant admin consent.' -ForegroundColor DarkGray Write-Host ' Entra ID > App registrations > [your app] > API permissions >' -ForegroundColor DarkGray Write-Host ' Add a permission > Microsoft Graph > Application permissions' -ForegroundColor DarkGray Write-Host " Then click 'Grant admin consent for [tenant]' and re-run." -ForegroundColor DarkGray Write-Host '' } elseif ($_.Exception.Message -match '404|ResourceNotFound') { Write-Verbose "Role assignment query returned 404 — no qualifying assignments found." } else { Write-Warning "Could not check admin role separation: $_" } } # ------------------------------------------------------------------ # Output results # ------------------------------------------------------------------ Export-SecurityConfigReport -Settings $settings -OutputPath $OutputPath -ServiceLabel 'Entra Admin Role Separation' |