Administradores con roles activos y su MFA
Muestra quién tiene hoy un rol de directorio activo, directo o por grupo, y si tiene MFA registrado. Sirve para encontrar administradores de más y cuentas privilegiadas sin protección.
Ejemplo en México
Manufactura en Querétaro, 400 personas
La auditoría del corporativo pide la lista de quién tiene privilegios en Microsoft 365 y con qué protección, y el equipo de TI solo tiene un archivo de hace dos años.
.\get-admins-privilegiados.ps1 -RutaCsv .\admins.csvEncuentra nueve administradores globales, dos sin MFA y un exempleado que heredó el rol por un grupo. Entrega el CSV como evidencia y reduce la lista a tres.
Cómo implementarlo
- Abre PowerShell 7 con el módulo Microsoft.Graph.Authentication instalado.
- Corre el script con -RutaCsv e inicia sesión con una cuenta con el rol Lector global.
- Filtra el CSV por MfaRegistrado en No y por el rol Global Administrator.
- Registra el MFA que falte y quita los roles que ya no se justifiquen.
Ver el código completo de get-admins-privilegiados.ps1
<#
.SYNOPSIS
¿Quién tiene roles de directorio activos y tiene MFA registrado?
.DESCRIPTION
Lista las asignaciones activas de roles de Microsoft Entra (permanentes y elegibles ya activadas en PIM),
abre los grupos con rol para ver a sus miembros y cruza cada usuario con el reporte de registro de MFA.
Las asignaciones elegibles que no están activadas no aparecen.
Solo lectura: no cambia nada en el tenant.
.PARAMETER RutaCsv
Ruta del CSV de salida. Si se omite, solo se muestra en pantalla.
.EXAMPLE
.\get-admins-privilegiados.ps1 -RutaCsv .\admins.csv
.NOTES
Permisos: RoleManagement.Read.Directory, AuditLog.Read.All, User.Read.All, GroupMember.Read.All
Rol mínimo: Lector global (Global Reader)
Licencia: Microsoft Entra ID P1
Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas.
#>
[CmdletBinding()]
param(
[string]$RutaCsv
)
$ErrorActionPreference = 'Stop'
$permisos = @('RoleManagement.Read.Directory', 'AuditLog.Read.All', 'User.Read.All', 'GroupMember.Read.All')
if (-not (Get-Command Connect-MgGraph -ErrorAction SilentlyContinue)) {
throw 'Falta el módulo. Instálalo con: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser'
}
Connect-MgGraph -Scopes $permisos -NoWelcome
function Get-TodasLasPaginas {
param([string]$Uri, [hashtable]$Encabezados = @{})
while ($Uri) {
$pagina = Invoke-MgGraphRequest -Method GET -Uri $Uri -Headers $Encabezados
$pagina.value
$Uri = $pagina.'@odata.nextLink'
}
}
$base = 'https://graph.microsoft.com/v1.0'
$roles = @{}
foreach ($d in Get-TodasLasPaginas "$base/roleManagement/directory/roleDefinitions") { $roles[$d.id] = $d.displayName }
$mfa = @{}
foreach ($r in Get-TodasLasPaginas "$base/reports/authenticationMethods/userRegistrationDetails") { $mfa[$r.id] = [bool]$r.isMfaRegistered }
function New-FilaUsuario {
param($Usuario, [string]$Rol, [string]$Alcance, [string]$Via)
[pscustomobject]@{
Nombre = $Usuario.displayName
Cuenta = $Usuario.userPrincipalName
Tipo = 'Usuario'
Rol = $Rol
Alcance = $Alcance
AsignadoPor = $Via
Habilitada = if ($Usuario.accountEnabled -eq $false) { 'No' } elseif ($Usuario.accountEnabled) { 'Sí' } else { 'Sin dato' }
# El reporte de registro no incluye cuentas deshabilitadas.
MfaRegistrado = if ($mfa.ContainsKey($Usuario.id)) { if ($mfa[$Usuario.id]) { 'Sí' } else { 'No' } } else { 'Sin dato' }
}
}
$asignaciones = Get-TodasLasPaginas "$base/roleManagement/directory/roleAssignments?`$expand=principal"
$filas = foreach ($a in $asignaciones) {
$rol = if ($roles[$a.roleDefinitionId]) { $roles[$a.roleDefinitionId] } else { $a.roleDefinitionId }
$alcance = if ($a.directoryScopeId -eq '/') { 'Todo el tenant' } else { $a.directoryScopeId }
$principal = $a.principal
switch ($principal.'@odata.type') {
'#microsoft.graph.user' {
New-FilaUsuario -Usuario $principal -Rol $rol -Alcance $alcance -Via 'Directo'
}
'#microsoft.graph.group' {
# Con conversión OData y $select la API exige ConsistencyLevel y $count.
$miembros = Get-TodasLasPaginas "$base/groups/$($principal.id)/transitiveMembers/microsoft.graph.user?`$count=true&`$select=id,displayName,userPrincipalName,accountEnabled" @{ ConsistencyLevel = 'eventual' }
foreach ($m in $miembros) { New-FilaUsuario -Usuario $m -Rol $rol -Alcance $alcance -Via "Grupo: $($principal.displayName)" }
}
default {
[pscustomobject]@{
Nombre = if ($principal.displayName) { $principal.displayName } else { $a.principalId }
Cuenta = $a.principalId
Tipo = if ($principal.'@odata.type' -eq '#microsoft.graph.servicePrincipal') { 'Aplicación' } else { 'Desconocido' }
Rol = $rol
Alcance = $alcance
AsignadoPor = 'Directo'
Habilitada = 'Sin dato'
MfaRegistrado = 'No aplica'
}
}
}
}
$filas = $filas | Sort-Object Rol, Nombre
Write-Host "Asignaciones activas: $(@($filas).Count). Usuarios con rol y sin MFA: $(@($filas | Where-Object MfaRegistrado -eq 'No').Count)"
if ($RutaCsv) {
# Excel ejecutaría como fórmula un nombre que empiece con estos caracteres.
foreach ($fila in $filas) { foreach ($p in $fila.PSObject.Properties) { if ($p.Value -is [string] -and $p.Value -match '^[=+\-@]') { $p.Value = "'" + $p.Value } } }
# UTF-8 con BOM para que Excel en español muestre bien los acentos.
$filas | Export-Csv -Path $RutaCsv -NoTypeInformation -Encoding utf8BOM
Write-Host "CSV guardado en $RutaCsv"
} else {
$filas | Format-Table -AutoSize
}