Saltar al contenido
Microsoft scripts

Scripts de Azure

Recomendaciones no saludables de Defender for Cloud

Lista los recursos con recomendaciones no saludables de Microsoft Defender for Cloud, de mayor a menor severidad. Las de Foundational CSPM, que es gratis, no requieren plan de pago.

Antes de correrloSin plan de pago funciona con las recomendaciones de Foundational CSPM (Microsoft Cloud Security Benchmark) en la experiencia del portal de Azure. Defender CSPM y los planes de protección de cargas agregan recomendaciones que aquí no aparecerán si no los tienes. En entornos administrados desde el portal de Microsoft Defender no hay plan gratuito: las recomendaciones de postura ahí requieren Defender CSPM. Un resultado vacío también puede significar que Defender for Cloud no está habilitado.

Ejemplo en México

Distribuidora de alimentos en Culiacán, 220 personas

No tienen presupuesto para planes de pago este año, pero quieren saber qué corregir primero con lo que ya incluye Azure.

.\get-recomendaciones-defender-for-cloud.ps1 -Severidad High -RutaCsv .\defender.csv

Saca la lista de severidad alta de Foundational CSPM y la atiende por grupo de recursos sin contratar nada.

Cómo implementarlo

  1. Confirma que Defender for Cloud esté habilitado en las suscripciones.
  2. Corre el script con -Severidad High para empezar por lo más grave.
  3. Abre la Liga de cada fila para ver cómo corregirla en el portal.
  4. Asigna responsables y fechas, y vuelve a correr el reporte para medir el avance.
Ver el código completo de get-recomendaciones-defender-for-cloud.ps1
<#
.SYNOPSIS
    ¿Qué recomendaciones de Microsoft Defender for Cloud tengo sin atender y cuáles son más graves?

.DESCRIPTION
    Lee las evaluaciones de Defender for Cloud en Resource Graph y deja las de estado Unhealthy. Solo lectura: no aplica correcciones.

.PARAMETER Severidad
    Severidades que quieres ver. Si se omite, muestra todas.

.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-recomendaciones-defender-for-cloud.ps1 -Severidad High, Medium -RutaCsv .\defender.csv

.NOTES
    Permisos: Lector de seguridad (Security Reader) en cada suscripción
    Rol mínimo: Lector de seguridad (Security Reader)
    Licencia: Basta una suscripción de Azure con Foundational CSPM, que es gratis, en la experiencia del portal de Azure. Las recomendaciones específicas de un plan aparecen solo con ese plan de pago.
    Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en una suscripción de pruebas.
#>
[CmdletBinding()]
param(
    [ValidateSet('High', 'Medium', 'Low')][string[]]$Severidad,
    [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 }

function ConsultarGrafo($Consulta) {
    $ids = @($activas.subscriptionId)
    # Resource Graph admite hasta 1,000 suscripciones por consulta.
    for ($i = 0; $i -lt $ids.Count; $i += 1000) {
        $lote = @($ids[$i..([Math]::Min($i + 999, $ids.Count - 1))])
        $cuerpo = @{ query = $Consulta; subscriptions = $lote; options = @{ '$top' = 1000 } }
        do {
            $pagina = InvocarAzure POST '/providers/Microsoft.ResourceGraph/resources?api-version=2024-04-01' $cuerpo
            $pagina.data
            $cuerpo.options['$skipToken'] = $pagina.'$skipToken'
        } while ($pagina.'$skipToken')
    }
}

$severidades = @{ High = 'Alta'; Medium = 'Media'; Low = 'Baja' }
$orden = @{ High = 0; Medium = 1; Low = 2 }
$consulta = 'securityresources | where type =~ "microsoft.security/assessments" | project id, name, resourceGroup, subscriptionId, properties'

$filas = @(ConsultarGrafo $consulta |
    Where-Object {
        $_.properties.status.code -eq 'Unhealthy' -and
        (-not $Severidad -or $_.properties.metadata.severity -in $Severidad)
    } |
    Sort-Object { if ($orden.ContainsKey("$($_.properties.metadata.severity)")) { $orden["$($_.properties.metadata.severity)"] } else { 3 } } |
    ForEach-Object {
        $p = $_.properties
        [pscustomobject]@{
            Severidad     = if ($severidades["$($p.metadata.severity)"]) { $severidades["$($p.metadata.severity)"] } else { $p.metadata.severity }
            Recomendacion = $p.displayName
            Suscripcion   = $nombres[$_.subscriptionId]
            GrupoRecursos = $_.resourceGroup
            Recurso       = ($_.id -split '/providers/Microsoft\.Security/assessments/')[0]
            Liga          = $p.links.azurePortal
        }
    })

Write-Host "Recomendaciones no saludables: $($filas.Count)"
$filas | Group-Object Severidad | ForEach-Object { Write-Host "  $($_.Name): $($_.Count)" }

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
}

Relacionados