Checks/EntraID/ARG.ENT.008.ps1

Register-ArgusCheck @{
    Id              = 'ARG.ENT.008'
    Title           = 'Global Administrator count outside {MinCount} to {MaxCount}'
    Area            = 'EntraID'
    Category        = 'Privileged access'
    Severity        = 'High'
    Description     = 'Counts the distinct holders of the Global Administrator role: active assignments and, with Entra ID P2, eligible (PIM) assignments. Members of role-assignable groups that hold the role are counted individually. Fails when the count is below **{MinCount}** or above **{MaxCount}**. Global exclusions are not applied: emergency-access accounts must be counted.'
    Rationale   = 'Too few Global Administrators risks losing control of the tenant; too many makes the most powerful role hard to govern. Keep two emergency-access accounts plus a small number of named administrators, and delegate the rest to least-privileged roles.'
    Remediation = @{
        Steps      = "1. Keep two cloud-only emergency-access accounts with permanent Global Administrator assignments.`n2. Move day-to-day administration to least-privileged roles (User, Exchange, SharePoint, Teams Administrator...).`n3. With Entra ID P2, make the remaining Global Administrator assignments eligible in PIM instead of permanent."
        PowerShell = "# Sample only - review before running. Lists current Global Administrator assignments.`nConnect-MgGraph -Scopes 'RoleManagement.Read.Directory'`nGet-MgRoleManagementDirectoryRoleAssignment -Filter `"roleDefinitionId eq '62e90394-69f5-4237-9190-012177145e10'`" -ExpandProperty principal"
        PortalUrl  = 'https://entra.microsoft.com/'
    }
    References      = @(
        @{ Title = 'Best practices for Microsoft Entra roles'; Url = 'https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices' }
        @{ Title = 'Manage emergency access admin accounts'; Url = 'https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/security-emergency-access' }
    )
    Requires        = @{
        Services    = @('Graph')
        GraphScopes = @('RoleManagement.Read.Directory', 'GroupMember.Read.All')
        DataSets    = @('DirectoryRoles')
    }
    Parameters      = @{ MinCount = 2; MaxCount = 4 }
    Tags            = @('Roles', 'Privileged access')
    ApplyExclusions = $false
    Test            = {
        param($Context)
        $globalAdmin = '62e90394-69f5-4237-9190-012177145e10'
        $roles = $Context.Data['DirectoryRoles']
        $members = @(Get-ArgusRoleMember -DirectoryRoles $roles -RoleTemplateId $globalAdmin)

        # One row per holder; a holder with both an active and an eligible assignment is counted once.
        $holders = @($members | Group-Object PrincipalId | ForEach-Object {
                $first = $_.Group[0]
                [pscustomobject]@{
                    DisplayName       = $first.DisplayName
                    UserPrincipalName = $first.UserPrincipalName
                    PrincipalType     = $first.PrincipalType
                    Assignment        = (@($_.Group.Assignment | Sort-Object -Unique) -join ', ')
                    Via               = (@($_.Group.Via | Sort-Object -Unique) -join ', ')
                    ObjectId          = $first.PrincipalId
                }
            } | Sort-Object DisplayName)

        $min = [int]$Context.Parameters.MinCount
        $max = [int]$Context.Parameters.MaxCount
        $count = $holders.Count
        $scope = if (Get-ArgusProperty $roles 'EligibleAvailable' $false) { 'active and eligible assignments' } else { 'active assignments; eligible PIM assignments not evaluated (Entra ID P2 not detected)' }
        $status = if ($count -lt $min -or $count -gt $max) { 'Failed' } else { 'Passed' }
        New-ArgusResult -Status $status -Findings $holders -AffectedCount $count -EvaluatedCount $count `
            -Summary ("$count Global Administrator" + $(if ($count -eq 1) { '' } else { 's' }) + " ($scope); the recommended range is {MinCount} to {MaxCount}.")
    }
}