Saltar al contenido
Microsoft scripts

Scripts de Azure

Cumplimiento de Azure Policy por suscripción

Resume por suscripción cuántos recursos y políticas no cumplen tus asignaciones de Azure Policy, con el porcentaje de cumplimiento.

Antes de correrloEl porcentaje solo cuenta recursos compatibles y no compatibles; ignora exentos y otros estados. Una suscripción sin asignaciones aparece como "Sin recursos evaluados". Te ayuda a documentar el estado, no prueba que cumplas una norma. Si algo no se puede consultar, queda marcado en el reporte como "No se pudo consultar" y el script termina con error para que no lo tomes por completo.

Ejemplo en México

Maquiladora de autopartes en Querétaro, 800 personas

Una auditoría del corporativo pide evidencia del estado de las políticas en sus cuatro suscripciones.

.\get-cumplimiento-azure-policy.ps1 -RutaCsv .\policy.csv

Entrega una tabla con el porcentaje por suscripción como evidencia y prioriza la que está más baja.

Cómo implementarlo

  1. Corre el script con una cuenta que tenga Lector en las suscripciones.
  2. Ordena el resultado por PorcentajeCumplimiento, de menor a mayor.
  3. En el portal, abre Azure Policy > Cumplimiento en la suscripción con menor porcentaje para ver qué asignación falla.
  4. Corrige los recursos o ajusta la asignación, y lleva el resultado a tu junta de gobierno de TI.
Ver el código completo de get-cumplimiento-azure-policy.ps1
<#
.SYNOPSIS
    ¿Qué tanto cumple cada suscripción con las asignaciones de Azure Policy?

.DESCRIPTION
    Consulta el resumen de Policy Insights de cada suscripción y calcula el porcentaje de recursos compatibles. Solo lectura: no cambia nada; el POST de summarize es una consulta.

.PARAMETER Suscripciones
    IDs o nombres de suscripción. Si se omite, usa todas a las que tienes acceso.

.PARAMETER RutaCsv
    Ruta del CSV de salida. Si se omite, solo se muestra en pantalla.

.EXAMPLE
    .\get-cumplimiento-azure-policy.ps1 -RutaCsv .\policy.csv

.NOTES
    Permisos: Lector (Reader) en cada suscripción
    Rol mínimo: Lector (Reader)
    Licencia: Basta una suscripción de Azure; no requiere plan adicional.
    Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en una suscripción de pruebas.
#>
[CmdletBinding()]
param(
    [ValidateNotNullOrEmpty()][string[]]$Suscripciones,
    [string]$RutaCsv
)

$ErrorActionPreference = 'Stop'

if (-not (Get-Command Connect-AzAccount -ErrorAction SilentlyContinue)) {
    throw 'Falta el módulo. Instálalo con: Install-Module Az.Accounts -Scope CurrentUser'
}
if (-not (Get-AzContext)) { $null = Connect-AzAccount }

function InvocarAzure($Metodo, $Ruta, $Cuerpo) {
    $parametros = @{ Method = $Metodo }
    if ($Ruta -like 'http*') { $parametros.Uri = $Ruta } else { $parametros.Path = $Ruta }
    if ($Cuerpo) { $parametros.Payload = $Cuerpo | ConvertTo-Json -Depth 10 }
    # Invoke-AzRestMethod no lanza error con 4xx o 5xx. 429 es una limitación temporal: se reintenta.
    for ($intento = 1; ; $intento++) {
        $respuesta = Invoke-AzRestMethod @parametros
        if ($respuesta.StatusCode -ne 429 -or $intento -ge 4) { break }
        Start-Sleep -Seconds 30
    }
    if ($respuesta.StatusCode -ge 400) { throw "Azure respondió $($respuesta.StatusCode) en ${Ruta}: $($respuesta.Content)" }
    if ($respuesta.Content) { $respuesta.Content | ConvertFrom-Json -AsHashtable }
}

function ListarPaginado($Ruta) {
    while ($Ruta) {
        $pagina = InvocarAzure GET $Ruta
        $pagina.value
        $Ruta = $pagina.nextLink
    }
}

$activas = @(ListarPaginado '/subscriptions?api-version=2022-12-01' | Where-Object {
    $_.state -notin 'Disabled', 'Deleted' -and
    (-not $Suscripciones -or $_.subscriptionId -in $Suscripciones -or $_.displayName -in $Suscripciones)
})
if (-not $activas) { throw 'No hay suscripciones activas a las que tengas acceso con ese filtro.' }
$noEncontradas = @($Suscripciones | Where-Object { $_ -and $_ -notin $activas.subscriptionId -and $_ -notin $activas.displayName })
if ($noEncontradas) { throw "No se encontró o no está activa: $($noEncontradas -join ', '). Revisa -Suscripciones." }
$nombres = @{}
foreach ($s in $activas) { $nombres[$s.subscriptionId] = $s.displayName }

$sinConsultar = @()
$filas = @(foreach ($s in $activas) {
    try {
        $resumen = InvocarAzure POST "/subscriptions/$($s.subscriptionId)/providers/Microsoft.PolicyInsights/policyStates/latest/summarize?api-version=2024-10-01"
    } catch {
        Write-Warning "No se pudo consultar $($s.displayName): $_"
        $sinConsultar += $s.displayName
        continue
    }
    $resultados = @($resumen.value)[0].results
    $conteo = @{}
    foreach ($detalle in @($resultados.resourceDetails)) { if ($detalle) { $conteo[$detalle.complianceState] = $detalle.count } }
    $compatibles = [int]$conteo['compliant']
    $noCompatibles = [int]$conteo['noncompliant']
    $evaluados = $compatibles + $noCompatibles
    [pscustomobject]@{
        Suscripcion          = $s.displayName
        SuscripcionId        = $s.subscriptionId
        RecursosCompatibles  = $compatibles
        RecursosNoCompatibles = [int]$resultados.nonCompliantResources
        PoliticasNoCompatibles = [int]$resultados.nonCompliantPolicies
        PorcentajeCumplimiento = if ($evaluados) { [Math]::Round(100 * $compatibles / $evaluados) } else { 'Sin recursos evaluados' }
    }
})

Write-Host "Suscripciones revisadas: $($filas.Count)"

if ($sinConsultar) {
    Write-Host "  No se pudo consultar: $(@($sinConsultar).Count)"
    $filas += @($sinConsultar | ForEach-Object { [pscustomobject]@{ Suscripcion = $_; SuscripcionId = ''; RecursosCompatibles = ''; RecursosNoCompatibles = ''; PoliticasNoCompatibles = ''; PorcentajeCumplimiento = 'No se pudo consultar' } })
}
if ($filas.Count -eq 0) {
    Write-Host 'Sin resultados.'
    # Sin esto, el CSV de una corrida anterior parecería el resultado de esta.
    if ($RutaCsv) { Set-Content -Path $RutaCsv -Value '' -Encoding utf8BOM }
} elseif ($RutaCsv) {
    # 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
}

# El reporte ya quedó escrito; el error avisa que está incompleto.
if ($sinConsultar) { throw "Reporte incompleto, no se pudo consultar: $(@($sinConsultar) -join ', ')" }

Relacionados