<# .SYNOPSIS ¿Qué políticas de acceso condicional están apagadas o en solo informe, y a quién excluyen? .DESCRIPTION Lista cada política con su estado y sus exclusiones de usuarios, grupos, roles y aplicaciones con nombre legible. Señala las apagadas y las que están en modo solo informe, porque ninguna de las dos protege a nadie. Una exclusión que apunta a un objeto borrado se marca como no encontrada. Solo lectura: no cambia nada en el tenant. El POST a getByIds es una consulta. .PARAMETER RutaCsv Ruta del CSV de salida. Si se omite, solo se muestra en pantalla. .PARAMETER ZonaHoraria Zona horaria para mostrar las fechas. Por defecto, Central Standard Time (Mexico). .EXAMPLE .\get-auditoria-acceso-condicional.ps1 -RutaCsv .\acceso-condicional.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( [string]$RutaCsv, [string]$ZonaHoraria = 'Central Standard Time (Mexico)' ) $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' } $zona = [TimeZoneInfo]::FindSystemTimeZoneById($ZonaHoraria) 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' } } 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) } $base = 'https://graph.microsoft.com/v1.0' $politicas = @(Get-TodasLasPaginas "$base/identity/conditionalAccess/policies") $roles = @{} foreach ($d in Get-TodasLasPaginas "$base/roleManagement/directory/roleDefinitions") { $roles[$d.id] = $d.displayName } # getByIds acepta 1000 identificadores por llamada. Los valores que no son GUID (como All) no se consultan. $regexGuid = '^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$' $ids = @($politicas | ForEach-Object { $_.conditions.users.excludeUsers; $_.conditions.users.excludeGroups } | Where-Object { $_ -match $regexGuid } | Select-Object -Unique) $nombres = @{} for ($i = 0; $i -lt $ids.Count; $i += 1000) { $cuerpo = @{ ids = @($ids[$i..([math]::Min($i + 999, $ids.Count - 1))]); types = @('user', 'group') } | ConvertTo-Json foreach ($o in (Invoke-MgGraphRequest -Method POST -Uri "$base/directoryObjects/getByIds" -Body $cuerpo -ContentType 'application/json').value) { $nombres[$o.id] = if ($o.userPrincipalName) { $o.userPrincipalName } else { $o.displayName } } } function Get-Nombres { param($Valores, [hashtable]$Mapa) (@($Valores) | Where-Object { $_ } | ForEach-Object { if ($Mapa[$_]) { $Mapa[$_] } elseif ($_ -match $regexGuid) { "$_ (no encontrado)" } else { $_ } }) -join '; ' } $filas = foreach ($p in $politicas) { $usuarios = $p.conditions.users [pscustomobject]@{ Politica = $p.displayName Estado = $p.state Alerta = switch ($p.state) { 'enabled' { 'Activa' } 'disabled' { 'APAGADA' } 'enabledForReportingButNotEnforced' { 'SOLO INFORME: no bloquea a nadie' } default { "Estado desconocido: $($p.state)" } } ExcluyeUsuarios = Get-Nombres $usuarios.excludeUsers $nombres ExcluyeGrupos = Get-Nombres $usuarios.excludeGroups $nombres ExcluyeRoles = Get-Nombres $usuarios.excludeRoles $roles ExcluyeExternos = if ($usuarios.excludeGuestsOrExternalUsers) { 'Sí' } else { 'No' } ExcluyeApps = (@($p.conditions.applications.excludeApplications) | Where-Object { $_ }) -join '; ' ModificadaEl = Format-FechaLocal $p.modifiedDateTime } } $filas = $filas | Sort-Object @{ Expression = { $_.Estado -eq 'enabled' } }, Politica Write-Host "Políticas: $(@($filas).Count). Sin aplicarse (apagadas o en solo informe): $(@($filas | Where-Object Estado -ne 'enabled').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 }