Saltar al contenido
Microsoft scripts

Scripts de Azure

Cuentas de almacenamiento con configuración débil

Señala cuentas de almacenamiento que permiten acceso público a blobs, aceptan TLS anterior a 1.2 o permiten autenticarse con clave compartida.

Antes de correrloBloquear la clave compartida rompe aplicaciones que usan claves o SAS firmadas con clave: pruébalo antes. Que AccesoPublicoBlob esté permitido no significa que un contenedor sea público; significa que se puede hacer público.

Ejemplo en México

Aseguradora regional en Monterrey, 400 personas

Una revisión interna de seguridad pide confirmar que ninguna cuenta con pólizas escaneadas quede abierta.

.\get-almacenamiento-inseguro.ps1 -RutaCsv .\almacenamiento.csv

Identifica una cuenta de pruebas con acceso público y varias con clave compartida, y arma un plan para migrar a Microsoft Entra ID.

Cómo implementarlo

  1. Corre el script y atiende primero AccesoPublicoBlob en Permitido.
  2. Sube el TLS mínimo a 1.2 en las cuentas marcadas, después de confirmar que tus aplicaciones lo soportan.
  3. Antes de bloquear la clave compartida, revisa qué aplicaciones usan claves o SAS firmadas con clave.
  4. Aplica los cambios por etapas y vuelve a correr el reporte para dejar la evidencia.
Ver el código completo de get-almacenamiento-inseguro.ps1
<#
.SYNOPSIS
    ¿Qué cuentas de almacenamiento permiten acceso público, TLS bajo o clave compartida?

.DESCRIPTION
    Revisa tres propiedades de cada cuenta y aplica la interpretación de Microsoft Learn cuando vienen vacías: TLS vacío equivale a 1.0 y clave compartida vacía equivale a permitida. Solo lectura: no cambia nada.

.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-almacenamiento-inseguro.ps1 -RutaCsv .\almacenamiento.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 }

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')
    }
}

$consulta = 'resources | where type =~ "microsoft.storage/storageaccounts" | project id, name, resourceGroup, subscriptionId, location, properties'

$filas = @(foreach ($cuenta in ConsultarGrafo $consulta) {
    $p = $cuenta.properties
    $blobPublico = $p.allowBlobPublicAccess -eq $true
    $tls = if ($p.minimumTlsVersion) { $p.minimumTlsVersion } else { 'TLS1_0 (valor por omisión)' }
    $tlsBajo = $tls -notin 'TLS1_2', 'TLS1_3'
    $claveCompartida = $p.allowSharedKeyAccess -ne $false
    if ($blobPublico -or $tlsBajo -or $claveCompartida) {
        [pscustomobject]@{
            Suscripcion       = $nombres[$cuenta.subscriptionId]
            GrupoRecursos     = $cuenta.resourceGroup
            Cuenta            = $cuenta.name
            Region            = $cuenta.location
            AccesoPublicoBlob = if ($blobPublico) { 'Permitido' } else { 'Bloqueado' }
            TlsMinimo         = $tls
            TlsBajo           = if ($tlsBajo) { 'Sí' } else { 'No' }
            ClaveCompartida   = if ($claveCompartida) { 'Permitida' } else { 'Bloqueada' }
            Id                = $cuenta.id
        }
    }
})

Write-Host "Cuentas con algún hallazgo: $($filas.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
}