<# .SYNOPSIS ¿Tus cuentas de emergencia están excluidas de todas las políticas de acceso condicional habilitadas? .DESCRIPTION Para cada cuenta revisa cada política habilitada y busca la exclusión directa, por grupo o por rol. Las políticas en solo informe no se revisan porque no bloquean, y tampoco las que solo aplican a identidades de carga de trabajo. Una cuenta de emergencia sujeta a una política puede quedar inservible justo cuando la necesitas. Solo lectura: no cambia nada en el tenant. .PARAMETER CuentasEmergencia Nombres principales (UPN) de las cuentas de emergencia. .PARAMETER RutaCsv Ruta del CSV de salida. Si se omite, solo se muestra en pantalla. .EXAMPLE .\get-exclusiones-cuentas-emergencia.ps1 -CuentasEmergencia emergencia1@empresa.example,emergencia2@empresa.example -RutaCsv .\exclusiones.csv .NOTES Permisos: Policy.Read.All, Directory.Read.All Rol mínimo: Lector global (Global 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( [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string[]]$CuentasEmergencia, [string]$RutaCsv ) $ErrorActionPreference = 'Stop' $permisos = @('Policy.Read.All', 'Directory.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' } Connect-MgGraph -Scopes $permisos -NoWelcome function Get-TodasLasPaginas { param([string]$Uri) while ($Uri) { $pagina = Invoke-MgGraphRequest -Method GET -Uri $Uri $pagina.value $Uri = $pagina.'@odata.nextLink' } } $base = 'https://graph.microsoft.com/v1.0' $politicas = @(Get-TodasLasPaginas "$base/identity/conditionalAccess/policies" | Where-Object { $u = $_.conditions.users # Una política sin usuarios incluidos es de identidades de carga de trabajo: no aplica a una cuenta. $_.state -eq 'enabled' -and ($u.includeUsers -or $u.includeGroups -or $u.includeRoles -or $u.includeGuestsOrExternalUsers) }) $filas = foreach ($cuenta in $CuentasEmergencia) { $usuario = $null try { $usuario = Invoke-MgGraphRequest -Method GET -Uri "$base/users/$([uri]::EscapeDataString($cuenta))?`$select=id,displayName,userPrincipalName" } catch { # Solo "no existe" se reporta; un error de permisos no debe parecer un resultado. if ("$_" -notmatch 'NotFound|Not Found|404') { throw } } if (-not $usuario.id) { [pscustomobject]@{ Cuenta = $cuenta; Politica = $null; Resultado = 'CUENTA NO ENCONTRADA'; ExcluidaPor = $null } continue } $membresias = @(Get-TodasLasPaginas "$base/users/$($usuario.id)/transitiveMemberOf") foreach ($p in $politicas) { $u = $p.conditions.users $via = @( if (@($u.excludeUsers) -contains $usuario.id) { 'Directa' } foreach ($m in $membresias) { if ($m.id -and @($u.excludeGroups) -contains $m.id) { "Grupo: $($m.displayName)" } if ($m.roleTemplateId -and @($u.excludeRoles) -contains $m.roleTemplateId) { "Rol: $($m.displayName)" } } ) [pscustomobject]@{ Cuenta = $usuario.userPrincipalName Politica = $p.displayName Resultado = if ($via) { 'Excluida' } else { 'NO EXCLUIDA' } ExcluidaPor = $via -join '; ' } } } $pendientes = @($filas | Where-Object Resultado -ne 'Excluida') Write-Host "Políticas habilitadas revisadas: $($politicas.Count). Hallazgos: $($pendientes.Count)" if ($RutaCsv) { # Excel ejecutaría como fórmula un nombre que empiece con estos caracteres. foreach ($fila in $filas) { foreach ($c in $fila.PSObject.Properties) { if ($c.Value -is [string] -and $c.Value -match '^[=+\-@]') { $c.Value = "'" + $c.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 }