Saltar al contenido
Microsoft scripts

Scripts de Entra

Inicios de sesión fuera de México

Lista los inicios de sesión correctos de los últimos días que vienen de un país no permitido o desconocido. Si tu empresa opera solo en México, cada fila merece una revisión.

Antes de correrloLa API v1.0 entrega inicios interactivos y federados correctos, no los no interactivos, así que no ves todo el uso de tokens. El país se calcula por la IP: una VPN o un proveedor con salida en otro país dan falsos positivos, y un atacante con VPN en México no aparece. Se pueden consultar hasta 30 días.

Ejemplo en México

Distribuidora de alimentos en Culiacán, 90 personas

Toda la operación está en Sinaloa, pero alguien de Finanzas recibió un correo raro y el director quiere saber si alguien entró desde otro país.

.\get-inicios-sesion-fuera-de-mexico.ps1 -Dias 14 -RutaCsv .\fuera-de-mexico.csv

Aparecen dos inicios desde otro país en la cuenta de Finanzas, sin viajes registrados. Revoca las sesiones, cambia la contraseña y activa MFA para esa área.

Cómo implementarlo

  1. Abre PowerShell 7 con el módulo Microsoft.Graph.Authentication instalado.
  2. Corre el script con -Dias, -PaisesPermitidos y -RutaCsv e inicia sesión con una cuenta con el rol Lector de informes.
  3. Agrupa el CSV por Cuenta y Pais, y marca lo que no corresponda a un viaje conocido.
  4. Confirma con la persona; si no fue ella, restablece su contraseña, revoca sus sesiones y considera una política de acceso condicional por ubicación.
Ver el código completo de get-inicios-sesion-fuera-de-mexico.ps1
<#
.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
}