Saltar al contenido
Microsoft scripts

Scripts de Entra

Invitados obsoletos

Lista a los invitados que no aceptaron la invitación o que llevan N días sin actividad. Sirve para cerrar accesos de proveedores y consultores que ya terminaron.

Antes de correrlolastSuccessfulSignInDateTime solo tiene datos desde el 1 de diciembre de 2023. El estado de la invitación solo existe para quienes se invitaron con la API de invitaciones; en otros casos la columna EstadoInvitacion queda vacía.

Ejemplo en México

Constructora en Monterrey, 150 personas

Cada obra suma contratistas como invitados en Teams y nadie los da de baja cuando termina. Ya hay más invitados que empleados.

.\get-invitados-obsoletos.ps1 -Dias 120 -RutaCsv .\invitados.csv

Identifica 80 invitados de obras cerradas y 15 invitaciones nunca aceptadas. Los residentes de obra confirman la lista y TI los elimina.

Cómo implementarlo

  1. Abre PowerShell 7 con el módulo Microsoft.Graph.Authentication instalado.
  2. Corre el script con -Dias y -RutaCsv e inicia sesión con una cuenta con el rol Lector de informes.
  3. Revisa la columna Motivo y agrupa a los invitados por dominio de correo.
  4. Pregunta a quien los invitó si siguen colaborando, y bloquea o elimina a los que ya no.
Ver el código completo de get-invitados-obsoletos.ps1
<#
.SYNOPSIS
    ¿Qué invitados no aceptaron la invitación o ya no tienen actividad?

.DESCRIPTION
    Revisa a los usuarios invitados y marca tres casos, todos con más de N días: invitación sin aceptar,
    invitado que aceptó y nunca tuvo un inicio de sesión correcto registrado, e invitado sin actividad reciente.
    Sirve para limpiar accesos externos de proveedores y proyectos que ya terminaron.
    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
    Antigüedad en días para considerar obsoleto a un invitado. Por defecto, 90.

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

.EXAMPLE
    .\get-invitados-obsoletos.ps1 -Dias 120 -RutaCsv .\invitados.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,
    [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) }
}

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

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

$filas = foreach ($g in $invitados) {
    $creado = ConvertTo-FechaUtc $g.createdDateTime
    $cambioEstado = ConvertTo-FechaUtc $g.externalUserStateChangeDateTime
    $ultimo = ConvertTo-FechaUtc $g.signInActivity.lastSuccessfulSignInDateTime

    $motivo = $null
    if ($g.externalUserState -eq 'PendingAcceptance') {
        $desde = if ($cambioEstado) { $cambioEstado } else { $creado }
        if ($desde -and $desde -lt $limite) { $motivo = 'Invitación sin aceptar' }
    } elseif ($ultimo) {
        if ($ultimo -lt $limite) { $motivo = 'Sin actividad' }
    } elseif ($creado -and $creado -lt $limite) {
        $motivo = 'Sin inicio correcto registrado'
    }
    if (-not $motivo) { continue }

    [pscustomobject]@{
        Nombre           = $g.displayName
        Correo           = $g.mail
        Cuenta           = $g.userPrincipalName
        Motivo           = $motivo
        EstadoInvitacion = $g.externalUserState
        Habilitada       = if ($g.accountEnabled) { 'Sí' } else { 'No' }
        InvitadoEl       = Format-FechaLocal $creado
        UltimoInicioOk   = Format-FechaLocal $ultimo
    }
}

Write-Host "Invitados obsoletos con más de $Dias días: $(@($filas).Count)"
if ($RutaCsv) {
    # Los invitados eligen su propio nombre: Excel lo ejecutaría como fórmula si empieza 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