<# .SYNOPSIS ¿Qué usuarios marca Microsoft Entra ID Protection como en riesgo? .DESCRIPTION Lista los usuarios en riesgo o confirmados como comprometidos, con su nivel y el último cambio de riesgo. Te da una lista priorizada para restablecer contraseñas y revocar sesiones. Solo lectura: no cambia nada en el tenant. .PARAMETER RutaCsv Ruta del CSV de salida. Si se omite, solo se muestra en pantalla. .PARAMETER IncluirResueltos Incluye también a los usuarios remediados, descartados o confirmados como seguros. .PARAMETER ZonaHoraria Zona horaria para mostrar las fechas. Por defecto, Central Standard Time (Mexico). .EXAMPLE .\get-usuarios-en-riesgo.ps1 -RutaCsv .\riesgo.csv .NOTES Permisos: IdentityRiskyUser.Read.All Rol mínimo: Lector de seguridad (Security Reader) Licencia: Microsoft Entra ID P2 Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas. #> [CmdletBinding()] param( [string]$RutaCsv, [switch]$IncluirResueltos, [string]$ZonaHoraria = 'Central Standard Time (Mexico)' ) $ErrorActionPreference = 'Stop' $permisos = @('IdentityRiskyUser.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) } $uri = 'https://graph.microsoft.com/v1.0/identityProtection/riskyUsers' $usuarios = while ($uri) { $pagina = Invoke-MgGraphRequest -Method GET -Uri $uri $pagina.value $uri = $pagina.'@odata.nextLink' } $nivel = @{ high = 'Alto'; medium = 'Medio'; low = 'Bajo'; hidden = 'Oculto'; none = 'Ninguno' } $estado = @{ atRisk = 'En riesgo'; confirmedCompromised = 'Confirmado comprometido'; remediated = 'Remediado' dismissed = 'Descartado'; confirmedSafe = 'Confirmado seguro'; none = 'Ninguno' } $orden = @{ high = 0; medium = 1; low = 2 } $filas = foreach ($u in $usuarios) { if (-not $IncluirResueltos -and $u.riskState -notin 'atRisk', 'confirmedCompromised') { continue } [pscustomobject]@{ Nombre = $u.userDisplayName Cuenta = $u.userPrincipalName Nivel = if ($nivel[$u.riskLevel]) { $nivel[$u.riskLevel] } else { $u.riskLevel } Estado = if ($estado[$u.riskState]) { $estado[$u.riskState] } else { $u.riskState } Detalle = $u.riskDetail UsuarioBorrado = if ($u.isDeleted) { 'Sí' } else { 'No' } UltimoCambio = Format-FechaLocal $u.riskLastUpdatedDateTime Prioridad = if ($null -ne $orden[$u.riskLevel]) { $orden[$u.riskLevel] } else { 3 } } } $filas = $filas | Sort-Object Prioridad, Nombre | Select-Object -ExcludeProperty Prioridad -Property * Write-Host "Usuarios en el reporte: $(@($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 }