Saltar al contenido
Microsoft scripts

Scripts de Entra

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.

Antes de correrloLas asignaciones elegibles de PIM sin activar no aparecen: alguien puede activar un rol después de correr el reporte. Sin dato en MFA suele indicar una cuenta deshabilitada, porque ese reporte no las incluye. Las aplicaciones pueden aparecer solo con su identificador si el permiso no alcanza para leer su nombre.

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.csv

Encuentra 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

  1. Abre PowerShell 7 con el módulo Microsoft.Graph.Authentication instalado.
  2. Corre el script con -RutaCsv e inicia sesión con una cuenta con el rol Lector global.
  3. Filtra el CSV por MfaRegistrado en No y por el rol Global Administrator.
  4. 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
}

Relacionados