<# .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 }