# CSPM para todos: detectá con Prowler y remediá con Cloud Custodian sin pagar licencias

Monté una cuenta de AWS a propósito mal configurada, la escaneé con Prowler y dejé que Cloud Custodian remediara lo que pudiera solo. Resultado: 10 checks fallando, 5 resueltos automáticamente en menos de 3 minutos, 0 crítico al final. La otra mitad no se pudo resolver sin una persona mirando, y eso no es un problema del proyecto: es la línea real entre lo que un motor de políticas puede tocar y lo que no.

Las plataformas comerciales de CSPM cobran entre $8,000 y $40,000 al año por hacer básicamente esto: decirte qué está mal configurado y, opcionalmente, arreglarlo. El detector y el motor de remediación que usan por debajo son, en su mayoría, capas sobre herramientas open source. Armé el ciclo completo con dos de ellas, Prowler y Cloud Custodian, para ver qué tan lejos llega la versión gratis.

## Qué hace falta para armar esto

CSPM (Cloud Security Posture Management) es la categoría de herramienta que audita tu cuenta cloud contra benchmarks como CIS o PCI-DSS y avisa cuándo algo quedó mal configurado: un bucket público, un security group abierto a `0.0.0.0/0`, un rol con permisos que nadie pidió.

El detector (Prowler) corre más de 600 checks y es gratis. El motor de remediación (Cloud Custodian) es un sistema de políticas declarativas y también es gratis. Lo que falta no es tecnología, es alguien que arme el ciclo: escanear, priorizar, remediar, volver a escanear para confirmar que no rompiste nada.

El flujo completo, de punta a punta:

![](https://cdn.hashnode.com/uploads/covers/65c385adb3ed131c0c948bd1/59c0492f-6c07-43e9-b6f4-1d9e36fc27bb.jpg align="center")

## La infraestructura de prueba

Para tener algo real que escanear, armé con Terraform una infra chica con problemas típicos de configuración:

| Recurso | Problema | Severidad |
| --- | --- | --- |
| S3 Bucket | Público, sin encriptación, sin versionado | Critical |
| Security Group | SSH/RDP/HTTP abiertos a `0.0.0.0/0` | Critical |
| IAM Role | `AmazonS3FullAccess` + `AmazonEC2FullAccess` | High |
| EC2 | IMDSv1 habilitado | High |
| EBS | Volumen sin encriptar | Medium |
| VPC | Sin Flow Logs | Medium |

Cada recurso lleva el tag `Project=cspm-demo`. Ese tag es lo que Prowler usa para escanear solo esta demo y no toda la cuenta, y lo que Cloud Custodian usa para saber qué puede tocar. Sin un tag de aislamiento consistente, no le daría un motor de remediación automática a nada.

Voy a crearla...

![](https://cdn.hashnode.com/uploads/covers/65c385adb3ed131c0c948bd1/79d73193-66d2-416a-80c8-563ec9b59ba7.png align="center")

## Detección: 10 fallas de 96 checks

```bash
docker-compose run --rm prowler aws \
  --resource-tag Project=cspm-demo \
  --services ec2 s3 iam vpc \
  --severity critical high \
  --output-formats html csv json-ocsf
```

![](https://cdn.hashnode.com/uploads/covers/65c385adb3ed131c0c948bd1/c5a23741-287f-4e39-a511-dc971b332cf2.png align="center")

El scan inicial dio 10 de 96 checks fallando (10.42%), con 4 críticas repartidas entre EC2 (6 fallas: 2 críticas, 4 altas) y S3 (4 fallas: 2 críticas, 2 altas): security groups con SSH y RDP abiertos a internet, bucket sin Block Public Access, IMDSv1, permisos IAM excesivos.

Un detalle que me hizo perder una hora antes de confiar en estos números: el CSV que exporta Prowler usa `;` como separador, y varios campos (Risk, Remediation) traen texto con saltos de línea propios. Contar hallazgos con `wc -l` o `grep -c FAIL` sobre ese archivo cuenta líneas físicas, no filas lógicas, y te da un número completamente inflado. Lo correcto es parsear el CSV como CSV:

```python
import csv
with open(archivo, newline='') as fh:
    rows = list(csv.reader(fh, delimiter=';'))
    header, data = rows[0], rows[1:]
    idx = header.index('STATUS')
    failed = sum(1 for row in data if row[idx] == 'FAIL')
```

## Remediación: lo que Cloud Custodian resolvió solo

Cloud Custodian es un motor de políticas: cada política es un YAML que dice "buscá recursos que cumplan esta condición, aplicales esta acción". No hay lógica custom que mantener, solo declarás la regla.

Esta cierra el puerto 22 en cualquier security group de la demo que lo tenga abierto a internet:

```yaml
policies:
  - name: close-ssh-security-groups
    resource: aws.security-group
    filters:
      - tag:Project: cspm-demo
      - type: ingress
        Cidr: {value: "0.0.0.0/0"}
        Ports: [22]
    actions:
      - type: remove-permissions
        ingress: matched
```

```bash
docker-compose run --rm custodian run \
  -s /custodian/output \
  /custodian/policies/remediation-policies.yml
```

![](https://cdn.hashnode.com/uploads/covers/65c385adb3ed131c0c948bd1/56072723-244e-49b7-8600-fb29bd96ac78.png align="center")

Con el set completo de políticas, en menos de 3 minutos Cloud Custodian cerró SSH y RDP abiertos a internet, y activó Block Public Access en el bucket S3. Tres remediaciones, tres recursos tocados.

Ojo con un filtro de S3 que me dio un falso negativo la primera vez que lo corrí: usar `type: value` contra `PublicAccessBlockConfiguration.BlockPublicAcls` no funciona, porque ese campo no viene poblado en el recurso base que trae Custodian. La política corría, no encontraba nada que remediar, y el bucket seguía público. El filtro correcto hace la llamada real a la API:

```yaml
filters:
  - tag:Project: cspm-demo
  - type: check-public-block
```

## Slack: una notificación por cada cosa que se tocó

Cada remediación dispara un mensaje a un incoming webhook de Slack con el recurso afectado, y al final del ciclo se manda un resumen con el antes/después. No es un extra decorativo: es la diferencia entre enterarte de que algo se arregló solo o tener que ir a revisar logs para saberlo.

Cloud Custodian no tiene un action simple para "mandame esto a un webhook de Slack". El action `notify` existe, pero está pensado para SNS/SQS + c7n-mailer, no para pegarle directo a una URL. Lo que sí funciona es el action genérico `webhook`, y su campo `body` no es un template de texto con variables `{nombre}`: es una expresión JMESPath que se evalúa contra los recursos que matchearon la policy.

```yaml
actions:
  - type: remove-permissions
    ingress: matched
  - type: webhook
    url: "${SLACK_WEBHOOK_URL}"
    batch: true
    body: "{text: join('', ['🔧 Cloud Custodian cerró SSH (22) en ', to_string(length(resources)), ' security group(s): ', join(', ', resources[].GroupId)])}"
```

La URL del webhook no está hardcodeada en el YAML que se versiona. El archivo real usa el placeholder `${SLACK_WEBHOOK_URL}`, y un script chico (`render-policies.sh`) lo sustituye leyendo `.env` (gitignored) justo antes de correr `custodian run`, generando un `.rendered.yml` que tampoco se commitea. El secreto nunca toca git.

![](https://cdn.hashnode.com/uploads/covers/65c385adb3ed131c0c948bd1/e05406f0-3b8b-4030-8ce8-cbd1cc2dfc1f.png align="center")

## Validación: 50% de mejora, 0 críticas

Re-escaneé con el mismo comando de Prowler para confirmar que la remediación funcionó y no rompió nada:

```plaintext
ANTES:   10.42% failed (10 checks) — 4 críticas
DESPUÉS:  5.21% failed (5 checks)  — 0 críticas

5 vulnerabilidades resueltas automáticamente (50% de mejora)
```

## Lo que Cloud Custodian no pudo resolver solo

De las 10 fallas iniciales, 5 quedaron pendientes. Tres categorías explican por qué, y ninguna es una limitación de la herramienta.

**IMDSv1 en una instancia corriendo.** Cambiar `http_tokens` de `optional` a `required` requiere stop/start de la instancia. Automatizar eso significa automatizar downtime, así que lo dejé fuera del scope de remediación automática a propósito.

**EBS sin encriptar.** No existe una operación para encriptar un volumen ya creado in-place. La única vía es snapshot → copiar el snapshot encriptado → recrear el volumen, que de nuevo implica downtime y coordinación. No es algo para correr sin supervisión.

**Permisos IAM excesivos.** Achicar `AmazonS3FullAccess` a algo mínimo requiere saber qué acciones de S3 usa realmente esa instancia. Eso es conocimiento de negocio, no algo que un motor de políticas pueda inferir. Automatizar esto a ciegas rompe cosas.

La automatización resuelve bien lo mecánico y reversible: reglas de firewall, flags de configuración, políticas de acceso. Todo lo que implica downtime o contexto de negocio sigue necesitando a alguien mirando. Eso no es una falla del sistema, es el límite correcto para ponerle a algo que corre sin supervisión.

## Cómo probarlo

El código está en [github.com/safernandez666/POC-CSPM](https://github.com/safernandez666/POC-CSPM), separado en dos scripts porque cumplen roles distintos.

`run-cspm.sh` es el demo de este repo: además del ciclo CSPM, crea y destruye con Terraform la infraestructura vulnerable de juguete que usé para tener algo que escanear. Es para probar el proyecto, no para tu cuenta real.

`cspm-automate.sh` es el pipeline en sí, sin Terraform, sin crear ni destruir nada: Prowler escanea, se analiza, Cloud Custodian remedia, Prowler valida, Slack avisa. Es el que correrías contra tu propia cuenta AWS con tu propia infraestructura ya existente. Los dos scripts comparten la misma lógica desde una librería común (`cspm-pipeline.sh`), para no tener dos copias del mismo bug dando vueltas.

Antes de correr `cspm-automate.sh` contra una cuenta real:

*   Usá una cuenta de testing primero, no producción directamente.
    
*   Cambiá el tag `Project=cspm-demo` en `cloud-custodian-policies/remediation-policies.yml` por el que uses vos para identificar qué puede tocar Cloud Custodian.
    
*   Corré `custodian run --dryrun` antes de dejarlo remediar de verdad, para ver qué matchearía sin tocar nada.
    

Este es el primero de una serie sobre el ciclo completo. El siguiente entra en detalle en los resultados de Prowler y cómo priorizar qué remediar primero.
