<# .SYNOPSIS ¿Quién inició sesión correctamente desde fuera de los países permitidos? .DESCRIPTION Revisa los inicios de sesión correctos de los últimos N días y lista los que vienen de otro país o de uno que Microsoft no pudo determinar. Útil cuando la empresa opera solo en México y un acceso desde otro país es una señal de alerta. 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 hacia atrás que se revisan, de 1 a 30. Por defecto, 7. .PARAMETER PaisesPermitidos Códigos de país de dos letras que no se reportan. Por defecto, MX. .PARAMETER ZonaHoraria Zona horaria para mostrar las fechas. Por defecto, Central Standard Time (Mexico). .EXAMPLE .\get-inicios-sesion-fuera-de-mexico.ps1 -Dias 14 -PaisesPermitidos MX,US -RutaCsv .\fuera-de-mexico.csv .NOTES Permisos: AuditLog.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, # Microsoft Entra ID P1 y P2 conservan los inicios de sesión 30 días. [ValidateRange(1, 30)][int]$Dias = 7, [ValidatePattern('^[A-Za-z]{2}$')][string[]]$PaisesPermitidos = @('MX'), [string]$ZonaHoraria = 'Central Standard Time (Mexico)' ) $ErrorActionPreference = 'Stop' $permisos = @('AuditLog.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 Format-FechaLocal { param($Valor) if (-not $Valor) { return $null } $utc = 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 } [TimeZoneInfo]::ConvertTimeFromUtc($utc, $zona).ToString('yyyy-MM-dd HH:mm', [cultureinfo]::InvariantCulture) } $desde = [datetime]::UtcNow.AddDays(-$Dias).ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", [cultureinfo]::InvariantCulture) # El país se filtra aquí porque la API solo permite eq y startsWith sobre él. Sin rango de fechas, la consulta puede agotar el tiempo. $uri = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=createdDateTime ge $desde and status/errorCode eq 0&`$top=1000" $inicios = while ($uri) { $pagina = Invoke-MgGraphRequest -Method GET -Uri $uri $pagina.value $uri = $pagina.'@odata.nextLink' } $filas = foreach ($s in $inicios) { if ([int]$s.status.errorCode -ne 0) { continue } $pais = "$($s.location.countryOrRegion)".Trim() if ($pais -and $pais -in $PaisesPermitidos) { continue } [pscustomobject]@{ Fecha = Format-FechaLocal $s.createdDateTime Nombre = $s.userDisplayName Cuenta = $s.userPrincipalName Pais = if ($pais) { $pais } else { 'Desconocido' } Ciudad = $s.location.city DireccionIP = $s.ipAddress Aplicacion = $s.appDisplayName Cliente = $s.clientAppUsed AccesoCondicional = $s.conditionalAccessStatus } } Write-Host "Inicios de sesión correctos fuera de $($PaisesPermitidos -join ', ') 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 }