Saltar al contenido
Microsoft scripts

Scripts de Entra

Auditoría de acceso condicional

Lista las políticas de acceso condicional con su estado y sus exclusiones con nombre. Señala las apagadas o en solo informe, porque esas no protegen a nadie aunque existan.

Antes de correrloUsa el permiso amplio Directory.Read.All porque getByIds no acepta otro de menor privilegio. Las aplicaciones excluidas se muestran con su identificador. No evalúa si el diseño de las políticas es correcto, solo te muestra el estado.

Ejemplo en México

Aseguradora regional en Puebla, 300 personas

Para una auditoría interna tienen que documentar sus controles de acceso, y el responsable anterior dejó varias políticas de prueba sin explicar.

.\get-auditoria-acceso-condicional.ps1 -RutaCsv .\acceso-condicional.csv

Ve que la política que exige MFA sigue en solo informe y que excluye a un grupo de 40 personas. La enciende, reduce el grupo y conserva el CSV para documentar el antes y el después.

Cómo implementarlo

  1. Abre PowerShell 7 con el módulo Microsoft.Graph.Authentication instalado.
  2. Corre el script con -RutaCsv e inicia sesión con una cuenta con el rol Lector global.
  3. Revisa las filas con Alerta distinta de Activa, que aparecen primero en el CSV.
  4. Revisa cada exclusión, sobre todo las marcadas como no encontrado, y decide cuáles quitar o qué política encender.
Ver el código completo de get-auditoria-acceso-condicional.ps1
<#
.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
}