Saltar al contenido
MS Scripts

Scripts de Entra

Baja de empleados desde CSV

Procesa un CSV de bajas de personal: bloquea el inicio de sesión de cada cuenta y revoca sus sesiones, y deja un reporte de lo que pasó con cada una.

Alto impactoPide 3 permisosProbado con respuestas simuladas. Sin ejecutar en un tenant real.

Antes de ejecutarloBloquea cuentas en masa: un CSV equivocado deja a gente sin trabajar, así que usa siempre -WhatIf primero. El script solo omite la cuenta con la que estás conectado y NO revisa cuentas de emergencia ni administradores: revisa el CSV contra get-exclusiones-cuentas-emergencia y get-admins-privilegiados antes de aplicar. El acceso no se corta al instante: la revocación tarda unos minutos y un token de acceso ya emitido sirve hasta que vence. En cuentas sincronizadas con Active Directory hay que deshabilitarlas también allá. La revocación no cierra sesiones de invitados en su tenant de origen. No borra la cuenta ni quita licencias; tampoco deshabilita sus dispositivos. Las filas sin cuenta aparecen en el reporte como "Sin UPN en el CSV", y tu propia cuenta se omite y cuenta como sin procesar.

Ejemplo en México

Maquiladora en Tijuana, 1,500 personas con turnos rotativos

Cada quincena Recursos Humanos entrega una lista de bajas y hacerlas una por una en el portal deja cuentas activas por días.

.\set-baja-empleados-csv.ps1 -RutaCsvBajas .\bajas.csv -RutaCsv .\resultado.csv -WhatIf

La simulación muestra 32 cuentas por bloquear y dos que no existen. Corrige el archivo con Recursos Humanos, lo corre sin -WhatIf y archiva el reporte de la quincena.

Cómo implementarlo

  1. Prepara un CSV separado por comas con una columna llamada Cuenta y el UPN de cada baja, y pide a Recursos Humanos que lo revise.
  2. Ejecuta el script con -RutaCsvBajas, -RutaCsv y -WhatIf, y revisa el reporte de la simulación.
  3. Ejecuta de nuevo sin -WhatIf y confirma cada cambio. Agrega -Confirm:$false solo después de revisar la simulación.
  4. Abre el reporte y atiende cualquier fila con Error o con una nota, y sigue con tu proceso de buzón, licencias y dispositivos.
Ver el código completo de set-baja-empleados-csv.ps1
<#
.SYNOPSIS
    Bloquea el inicio de sesión y revoca las sesiones de las cuentas de un CSV de bajas.

.DESCRIPTION
    Lee un CSV con la columna Cuenta (UPN). Por cada cuenta pone accountEnabled en false y revoca sus sesiones,
    en ese orden, y deja un reporte con el resultado de cada una. Una falla no detiene a las demás.
    No borra cuentas, buzones ni licencias. Prueba siempre primero con -WhatIf.
    CAMBIA EL TENANT: bloquea cuentas en masa.

.PARAMETER RutaCsvBajas
    CSV de entrada, separado por comas, con una columna llamada Cuenta.

.PARAMETER RutaCsv
    Ruta del reporte de resultados. Si se omite, solo se muestra en pantalla.

.EXAMPLE
    .\set-baja-empleados-csv.ps1 -RutaCsvBajas .\bajas.csv -RutaCsv .\resultado.csv -WhatIf

.EXAMPLE
    .\set-baja-empleados-csv.ps1 -RutaCsvBajas .\bajas.csv -RutaCsv .\resultado.csv

.NOTES
    Permisos: User.EnableDisableAccount.All, User.Read.All, User.RevokeSessions.All
    Rol mínimo: Administrador de usuarios (User Administrator). Para bloquear administradores: Administrador de autenticación con privilegios (Privileged Authentication Administrator)
    Licencia: Microsoft Entra ID Free
    Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas.
#>
#Requires -Version 7.0
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
    [Parameter(Mandatory)][string]$RutaCsvBajas,
    [string]$RutaCsv
)

$ErrorActionPreference = 'Stop'
$permisos = @('User.EnableDisableAccount.All', 'User.Read.All', 'User.RevokeSessions.All')

if (-not (Get-Command Connect-MgGraph -ErrorAction SilentlyContinue)) {
    throw 'Falta el módulo. Instálalo con: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser'
}

$entrada = @(Import-Csv -Path $RutaCsvBajas)
if (-not $entrada) { throw "El CSV $RutaCsvBajas está vacío." }
if ('Cuenta' -notin $entrada[0].PSObject.Properties.Name) {
    throw "El CSV debe tener una columna llamada Cuenta, separada por comas. Columnas encontradas: $($entrada[0].PSObject.Properties.Name -join ', ')"
}
$cuentas = @($entrada | ForEach-Object { "$($_.Cuenta)".Trim().ToLowerInvariant() } | Where-Object { $_ } | Select-Object -Unique)
$sinCuenta = @($entrada | Where-Object { -not "$($_.Cuenta)".Trim() }).Count
if (-not $cuentas) { throw "El CSV $RutaCsvBajas no trae ninguna cuenta en la columna Cuenta." }

Connect-MgGraph -Scopes $permisos -NoWelcome
$base = 'https://graph.microsoft.com/v1.0'
$yo = (Get-MgContext).Account

$filas = foreach ($cuenta in $cuentas) {
    $fila = [pscustomobject]@{ Cuenta = $cuenta; Nombre = $null; Bloqueo = 'Sin cambios'; Sesiones = 'Sin cambios'; Nota = $null }
    $fila

    # El valor viene de un archivo y termina en la URI.
    if ($cuenta -notmatch '^[^\s/\\?]+@[^\s/\\?]+$') { $fila.Nota = 'No parece un UPN válido'; continue }
    if ($yo -and $cuenta -eq $yo) { $fila.Nota = 'Omitida: es la cuenta con la que estás conectado'; continue }

    $usuario = $null
    try {
        $usuario = Invoke-MgGraphRequest -Method GET -Uri "$base/users/$([uri]::EscapeDataString($cuenta))?`$select=id,displayName,accountEnabled,userType,onPremisesSyncEnabled"
    } catch {
        if ("$_" -notmatch 'NotFound|Not Found|404') { $fila.Nota = "Error al leer: $_"; continue }
    }
    if (-not $usuario.id) { $fila.Nota = 'Cuenta no encontrada'; continue }
    $fila.Nombre = $usuario.displayName

    $notas = @()
    if ($usuario.onPremisesSyncEnabled) { $notas += 'Cuenta sincronizada: deshabilítala también en Active Directory' }
    if ($usuario.userType -eq 'Guest') { $notas += 'Invitado: la revocación no aplica a sus sesiones de su tenant de origen' }

    if ($usuario.accountEnabled -eq $false) {
        $fila.Bloqueo = 'Ya estaba bloqueada'
    } elseif ($PSCmdlet.ShouldProcess($cuenta, 'Bloquear el inicio de sesión')) {
        try {
            Invoke-MgGraphRequest -Method PATCH -Uri "$base/users/$($usuario.id)" -Body (@{ accountEnabled = $false } | ConvertTo-Json) -ContentType 'application/json' | Out-Null
            $fila.Bloqueo = 'Bloqueada'
        } catch { $fila.Bloqueo = "Error: $_" }
    } else {
        $fila.Bloqueo = 'Simulado'
    }

    # Se revoca aunque el bloqueo falle: cierra las sesiones abiertas mientras se resuelve.
    if ($PSCmdlet.ShouldProcess($cuenta, 'Revocar las sesiones')) {
        try {
            Invoke-MgGraphRequest -Method POST -Uri "$base/users/$($usuario.id)/revokeSignInSessions" | Out-Null
            $fila.Sesiones = 'Revocadas'
        } catch { $fila.Sesiones = "Error: $_" }
    } else {
        $fila.Sesiones = 'Simulado'
    }
    $fila.Nota = $notas -join '. '
}

$filas = @($filas) + @(for ($i = 0; $i -lt $sinCuenta; $i++) {
    [pscustomobject]@{ Cuenta = ''; Nombre = $null; Bloqueo = 'Sin cambios'; Sesiones = 'Sin cambios'; Nota = 'Sin UPN en el CSV' }
})
$fallas = @($filas | Where-Object { $_.Bloqueo -like 'Error*' -or $_.Sesiones -like 'Error*' -or $_.Nota -match '^(Error|Cuenta no|No parece|Omitida|Sin UPN)' })
Write-Host "Cuentas procesadas: $(@($filas).Count). Con falla o sin procesar: $($fallas.Count)"
if ($RutaCsv) {
    # Excel ejecutaría como fórmula un nombre que empiece con estos caracteres.
    foreach ($f in $filas) { foreach ($p in $f.PSObject.Properties) { if ($p.Value -is [string] -and $p.Value -match '^[=+\-@]') { $p.Value = "'" + $p.Value } } }
    # El reporte se escribe también con -WhatIf: es el resultado de la simulación, no un cambio en el tenant.
    $filas | Export-Csv -Path $RutaCsv -NoTypeInformation -Encoding utf8BOM -WhatIf:$false -Confirm:$false
    Write-Host "CSV guardado en $RutaCsv"
} else {
    $filas | Format-Table -AutoSize
}

Parte del playbook

Relacionados