Saltar al contenido
Microsoft scripts

Scripts de Entra

Cuentas inactivas

Encuentra las cuentas que llevan N días sin un inicio de sesión correcto, con opción de dejar fuera a los invitados. Son candidatas a bloquearse y a liberar su licencia.

Antes de correrloUsa lastSuccessfulSignInDateTime, que Microsoft registra desde el 1 de diciembre de 2023 sin datos anteriores, así que una cuenta sin inicios correctos desde entonces sale como Sin inicio correcto registrado. Las cuentas de servicio y de buzones compartidos pueden verse inactivas sin estarlo: revísalas antes de bloquearlas.

Ejemplo en México

Maquiladora en Ciudad Juárez, 900 personas con tres turnos

La rotación es alta y Recursos Humanos no siempre avisa de las salidas. TI sospecha que hay cuentas activas de personas que ya no trabajan ahí.

.\get-cuentas-inactivas.ps1 -Dias 60 -ExcluirInvitados -RutaCsv .\inactivas.csv

Obtiene 48 cuentas sin entrar en 60 días. Las cruza con la nómina, bloquea 41 y libera sus licencias.

Cómo implementarlo

  1. Abre PowerShell 7 con el módulo Microsoft.Graph.Authentication instalado.
  2. Decide el periodo y corre el script con -Dias, -RutaCsv y, si quieres, -ExcluirInvitados.
  3. Inicia sesión con una cuenta con el rol Lector de informes.
  4. Confirma con Recursos Humanos o con cada jefe de área, y bloquea las cuentas que ya no se ocupen.
Ver el código completo de get-cuentas-inactivas.ps1
<#
.SYNOPSIS
    ¿Qué cuentas llevan N días o más sin un inicio de sesión correcto?

.DESCRIPTION
    Revisa signInActivity de cada usuario y compara lastSuccessfulSignInDateTime contra el límite.
    Usa el último inicio correcto porque los intentos fallidos también actualizan las otras fechas.
    Omite cuentas creadas dentro del periodo, que todavía no han tenido oportunidad de entrar.
    Solo lectura: no cambia nada en el tenant.

.PARAMETER RutaCsv
    Ruta del CSV de salida. Si se omite, solo se muestra en pantalla.

.PARAMETER Dias
    Días sin iniciar sesión para considerar inactiva una cuenta. Por defecto, 90.

.PARAMETER ExcluirInvitados
    Deja fuera a los usuarios invitados.

.PARAMETER ZonaHoraria
    Zona horaria para mostrar las fechas. Por defecto, Central Standard Time (Mexico).

.EXAMPLE
    .\get-cuentas-inactivas.ps1 -Dias 60 -ExcluirInvitados -RutaCsv .\inactivas.csv

.NOTES
    Permisos: AuditLog.Read.All, User.Read.All
    Rol mínimo: Lector de informes (Reports 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,
    [ValidateRange(1, 36500)][int]$Dias = 90,
    [switch]$ExcluirInvitados,
    [string]$ZonaHoraria = 'Central Standard Time (Mexico)'
)

$ErrorActionPreference = 'Stop'
$permisos = @('AuditLog.Read.All', 'User.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'
}
$zona = [TimeZoneInfo]::FindSystemTimeZoneById($ZonaHoraria)
Connect-MgGraph -Scopes $permisos -NoWelcome

function ConvertTo-FechaUtc {
    param($Valor)
    # Graph puede dar texto o fecha; vacío o el año 0001 significan que no hay dato.
    if (-not $Valor) { return $null }
    $fecha = if ($Valor -is [datetimeoffset]) { $Valor.UtcDateTime }
             elseif ($Valor -is [datetime]) { if ($Valor.Kind -eq 'Unspecified') { [datetime]::SpecifyKind($Valor, 'Utc') } else { $Valor.ToUniversalTime() } }
             else { [datetimeoffset]::Parse("$Valor", [cultureinfo]::InvariantCulture).UtcDateTime }
    if ($fecha.Year -le 1) { return $null }
    $fecha
}
function Format-FechaLocal {
    param($FechaUtc)
    if ($FechaUtc) { [TimeZoneInfo]::ConvertTimeFromUtc($FechaUtc, $zona).ToString('yyyy-MM-dd HH:mm', [cultureinfo]::InvariantCulture) }
}

$ahora = [datetime]::UtcNow
$limite = $ahora.AddDays(-$Dias)

# Con signInActivity la página máxima es de 500.
$uri = 'https://graph.microsoft.com/v1.0/users?$select=id,displayName,userPrincipalName,userType,accountEnabled,createdDateTime,signInActivity&$top=500'
$usuarios = while ($uri) {
    $pagina = Invoke-MgGraphRequest -Method GET -Uri $uri
    $pagina.value
    $uri = $pagina.'@odata.nextLink'
}

$filas = foreach ($u in $usuarios) {
    if ($ExcluirInvitados -and $u.userType -eq 'Guest') { continue }
    $ultimo = ConvertTo-FechaUtc $u.signInActivity.lastSuccessfulSignInDateTime
    if ($ultimo -and $ultimo -ge $limite) { continue }
    $creada = ConvertTo-FechaUtc $u.createdDateTime
    if (-not $ultimo -and $creada -and $creada -ge $limite) { continue }
    $intentos = @((ConvertTo-FechaUtc $u.signInActivity.lastSignInDateTime), (ConvertTo-FechaUtc $u.signInActivity.lastNonInteractiveSignInDateTime)) | Where-Object { $_ } | Sort-Object -Descending
    [pscustomobject]@{
        Nombre            = $u.displayName
        Cuenta            = $u.userPrincipalName
        Tipo              = if ($u.userType -eq 'Guest') { 'Invitado' } else { 'Miembro' }
        Habilitada        = if ($u.accountEnabled) { 'Sí' } else { 'No' }
        UltimoInicioOk    = if ($ultimo) { Format-FechaLocal $ultimo } else { 'Sin inicio correcto registrado' }
        DiasSinIniciar    = if ($ultimo) { [int][math]::Floor(($ahora - $ultimo).TotalDays) } else { $null }
        UltimoIntento     = Format-FechaLocal @($intentos)[0]
        CreadaEl          = Format-FechaLocal $creada
    }
}

Write-Host "Cuentas sin inicio de sesión correcto en $Dias días: $(@($filas).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