Módulo 5: Scanning Iac And Dependencies

7. Manos a la obra: agregando el escaneo al pipeline heredado

Descripción

Todo lo que hiciste en las lecciones 4 a 6 corrió en tu terminal, a mano. Esta lección lo convierte en un gate automático: un step nuevo en ci.yml —el mismo archivo que cicd-and-gitops-on-aws-guide, Módulo 3, dejó terminado con nueve steps— que corre trivy config en cada Pull Request, antes de que nadie tenga que acordarse de correrlo a mano. Vas a correrlo con act, de verdad, contra un runner Docker real — y vas a ver el gate fallar primero, sobre un hallazgo real que ni siquiera sabías que seguía ahí, antes de verlo pasar.

Nota de aislamiento. Esta lección usa un laboratorio nuevo y desechable en tu directorio de trabajo temporal — nunca el andes-cargo-infra/ que vienes acumulando desde el Módulo 1. git init, un ci.yml extendido, y act corriendo contra Docker son operaciones que tocan .git/ y el sistema de archivos de formas que no quieres mezclar con tu proyecto principal — el mismo patrón de aislamiento que ya usaste en el Módulo 3, lección 7, para el experimento de secretos filtrados.

Conexión con el módulo

La lección 6 dejó nueve hallazgos de Trivy, con dos supresiones documentadas. Esta lección toma exactamente ese resultado —el mismo trivy config, la misma configuración— y lo convierte en la pieza que faltaba para que "escanear antes de aplicar" deje de depender de que un humano se acuerde de hacerlo.


Analogía retomada: el inspector, ahora parado en la puerta

Hasta ahora, invitaste al inspector a caminar la casa cuando te acordabas. Esta lección lo pone parado en la puerta principal, con instrucciones de no dejar pasar a nadie que no haya sido revisado primero — cada vez que alguien intenta traer un cambio nuevo a la casa (cada Pull Request), el inspector revisa antes de que el cambio entre, no después.


Paso 1 — El laboratorio aislado

mkdir andes-cargo-infra-m5-ci-lab && cd andes-cargo-infra-m5-ci-lab
git init -b main

Copia el HCL completo, ya endurecido por las lecciones 4 a 6 de este módulo (los archivos de la raíz, modules/, lambda/, y el .trivyignore de la lección 6), y el .actrc/pr-event.json que ya conoces de cicd-and-gitops-on-aws-guide:

cat > .actrc <<'EOF'
-P ubuntu-latest=catthehacker/ubuntu:act-latest
EOF
git add -A && git commit -m "Bootstrap CI lab for M5.7"

Paso 2 — Extendiendo ci.yml: dos steps nuevos

Este es el ci.yml heredado de cicd-and-gitops-on-aws-guide, Módulo 3, con dos steps nuevos insertados después de Terraform format check y antes de Terraform init — el escaneo no necesita que Terraform esté inicializado, así que corre lo antes posible, fallando rápido sobre HCL crudo antes de gastar tiempo en pasos más caros:

name: ci

on:
  pull_request:
    branches: [main]

jobs:
  terraform-checks:
    runs-on: ubuntu-latest
    env:
      AWS_ACCESS_KEY_ID: test
      AWS_SECRET_ACCESS_KEY: test
      AWS_DEFAULT_REGION: us-east-1
      AWS_ENDPOINT_URL: http://host.docker.internal:4566
    steps:
      - name: Check out andes-cargo-infra
        uses: actions/checkout@v4

      - name: Set up Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.15.8"

      - name: Terraform format check
        run: terraform fmt -check -recursive

      - name: Install Trivy
        run: |
          curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.74.0

      - name: IaC security scan (Trivy)
        run: trivy config --exit-code 1 --severity CRITICAL,HIGH .

      - name: Terraform init
        run: terraform init -input=false

      - name: Terraform validate
        run: terraform validate

      - name: Install awslocal
        run: pip3 install --quiet --break-system-packages awscli awscli-local

      - name: Confirm the runner can reach LocalStack on the host
        continue-on-error: true
        run: awslocal s3 ls

      - name: Install tflocal
        run: pip3 install --quiet --break-system-packages terraform-local

      - name: Terraform plan
        run: tflocal plan -input=false -no-color | tee plan-output.txt

      - name: Publish the plan to the job summary
        run: |
          {
            echo "## Terraform plan — andes-cargo-infra"
            echo '```'
            cat plan-output.txt
            echo '```'
          } >> "$GITHUB_STEP_SUMMARY"

Dos decisiones de diseño en el step IaC security scan (Trivy), ninguna arbitraria:

  • --exit-code 1 — sin este flag, trivy config siempre termina con código 0, sin importar cuántos hallazgos encuentre; es un comportamiento pensado para uso interactivo en terminal, no para un gate de CI. Con --exit-code 1, el proceso termina con código distinto de cero si encuentra al menos un hallazgo de la severidad indicada — el mecanismo que hace que GitHub Actions marque el step, y el job completo, como fallido.
  • --severity CRITICAL,HIGH — acota el gate a lo más urgente. Los hallazgos MEDIUM/LOW de la lección 4 (recuperación a un punto en el tiempo ya arreglada, cifrado con clave administrada por el cliente) siguen existiendo y siguen siendo visibles si corres trivy config sin este flag, pero no bloquean el merge — la misma distinción entre "arreglar", "suprimir" y "aceptar sin bloquear" de la lección 6, ahora expresada como una decisión de diseño del propio gate.

Paso 3 — Primera corrida: el gate falla, sobre un hallazgo real

act pull_request -e .github/act-events/pr-event.json -j terraform-checks

Qué esperar (literal — ejecutado para escribir esta lección, con Docker real y la imagen catthehacker/ubuntu:act-latest):

[ci/terraform-checks] ⭐ Run Main IaC security scan (Trivy)
[ci/terraform-checks]   | 2026-08-14T16:46:50Z	INFO	[misconfig] Misconfiguration scanning is enabled
[ci/terraform-checks]   | 2026-08-14T16:46:50Z	INFO	[checks-client] Need to update the checks bundle
[ci/terraform-checks]   | 2026-08-14T16:46:50Z	INFO	[checks-client] Downloading the checks bundle...
[ci/terraform-checks]   | Report Summary
[ci/terraform-checks]   | ┌───────────────────────────┬───────────┬───────────────────┐
[ci/terraform-checks]   | │          Target           │   Type    │ Misconfigurations │
[ci/terraform-checks]   | ├───────────────────────────┼───────────┼───────────────────┤
[ci/terraform-checks]   | │ .                         │ terraform │         0         │
[ci/terraform-checks]   | ├───────────────────────────┼───────────┼───────────────────┤
[ci/terraform-checks]   | │ dynamodb.tf               │ terraform │         0         │
[ci/terraform-checks]   | ├───────────────────────────┼───────────┼───────────────────┤
[ci/terraform-checks]   | │ lambda.tf                 │ terraform │         0         │
[ci/terraform-checks]   | ├───────────────────────────┼───────────┼───────────────────┤
[ci/terraform-checks]   | │ modules/s3-bucket/main.tf │ terraform │         1         │
[ci/terraform-checks]   | ├───────────────────────────┼───────────┼───────────────────┤
[ci/terraform-checks]   | │ secrets.tf                │ terraform │         0         │
[ci/terraform-checks]   | └───────────────────────────┴───────────┴───────────────────┘
[ci/terraform-checks]   | modules/s3-bucket/main.tf (terraform)
[ci/terraform-checks]   | =====================================
[ci/terraform-checks]   | Tests: 1 (SUCCESSES: 0, FAILURES: 1)
[ci/terraform-checks]   | Failures: 1 (HIGH: 1, CRITICAL: 0)
[ci/terraform-checks]   | AWS-0132 (HIGH): Bucket does not encrypt data with a customer managed key.
[ci/terraform-checks]   | See https://avd.aquasec.com/misconfig/aws-0132
[ci/terraform-checks]   |  modules/s3-bucket/main.tf:1-5
[ci/terraform-checks]   |    via s3.tf:29-36 (module.shipment_docs_bucket)
[ci/terraform-checks]   ❌  Failure - Main IaC security scan (Trivy) [2.029882458s]
[ci/terraform-checks] exitcode '1': failure
[ci/terraform-checks] ⭐ Run Complete job
[ci/terraform-checks]   ✅  Success - Complete job
[ci/terraform-checks] 🏁  Job failed
Error: Job 'terraform-checks' failed

El gate falló de verdad — el pipeline entero se detiene aquí, antes de llegar a terraform init. Con --severity CRITICAL,HIGH, los seis hallazgos MEDIUM/LOW que quedaron después de la lección 6 no bloquean nada — pero AWS-0132 (cifrado del bucket sin clave administrada por el cliente) sí es HIGH, y todavía no tenía ninguna decisión documentada. Este es, exactamente, el comportamiento correcto de un gate: encontró algo real, no arreglado ni suprimido, y detuvo el pipeline en el punto exacto donde lo encontró — no en terraform plan, no en terraform apply, sino en el primer paso capaz de verlo.


Paso 4 — La decisión, documentada, igual que la lección 6

Andes Cargo decide: una clave KMS administrada por el cliente para este bucket tiene un costo real en una cuenta AWS de verdad, y no hay una vía $0 confirmada en LocalStack para probar el enforcement de esa protección adicional (la misma clase de límite que ya viste en el Módulo 2, lección 6, sobre IAM Policy Enforcement). La decisión es suprimir, con razón escrita — no arreglar ahora, no ignorar en silencio:

cat >> .trivyignore <<'EOF'

# AWS-0132: S3 bucket does not encrypt data with a customer-managed KMS key.
# Uses AWS-managed keys (SSE-S3 default). A CMK has a real per-month cost in AWS
# and no LocalStack Hobby equivalent -- out of scope for this $0 lab. Revisit if
# this project ever targets real AWS.
AWS-0132
EOF

git add -A && git commit -m "Extend .trivyignore with AWS-0132 (documented decision)"

Paso 5 — Segunda corrida: el gate pasa

act pull_request -e .github/act-events/pr-event.json -j terraform-checks

Qué esperar (literal — ejecutado para escribir esta lección, con la misma imagen Docker):

[ci/terraform-checks] ⭐ Run Main IaC security scan (Trivy)
[ci/terraform-checks]   | Report Summary
[ci/terraform-checks]   | ┌───────────────────────────┬───────────┬───────────────────┐
[ci/terraform-checks]   | │          Target           │   Type    │ Misconfigurations │
[ci/terraform-checks]   | ├───────────────────────────┼───────────┼───────────────────┤
[ci/terraform-checks]   | │ .                         │ terraform │         0         │
[ci/terraform-checks]   | ├───────────────────────────┼───────────┼───────────────────┤
[ci/terraform-checks]   | │ dynamodb.tf               │ terraform │         0         │
[ci/terraform-checks]   | ├───────────────────────────┼───────────┼───────────────────┤
[ci/terraform-checks]   | │ lambda.tf                 │ terraform │         0         │
[ci/terraform-checks]   | ├───────────────────────────┼───────────┼───────────────────┤
[ci/terraform-checks]   | │ modules/s3-bucket/main.tf │ terraform │         0         │
[ci/terraform-checks]   | ├───────────────────────────┼───────────┼───────────────────┤
[ci/terraform-checks]   | │ secrets.tf                │ terraform │         0         │
[ci/terraform-checks]   | └───────────────────────────┴───────────┴───────────────────┘
[ci/terraform-checks]   ✅  Success - Main IaC security scan (Trivy) [1.922404458s]
[ci/terraform-checks] ⭐ Run Main Terraform init
...
[ci/terraform-checks] ⭐ Run Complete job
[ci/terraform-checks]   ✅  Success - Complete job
[ci/terraform-checks] 🏁  Job succeeded

Cero misconfiguraciones de severidad CRITICAL/HIGH en cada uno de los cinco archivos evaluados, y Job succeeded. El pipeline completo —diez steps, terminando en terraform plan publicado como resumen del job, exactamente como lo dejó cicd-and-gitops-on-aws-guide— corrió de punta a punta, con el gate de seguridad nuevo integrado sin fricción.


Antes y después, uno al lado del otro

Corrida 1 (Paso 3)Corrida 2 (Paso 5)
.trivyignoreAWS-0089 únicamenteAWS-0089 + AWS-0132
Hallazgos HIGH/CRITICAL sin suprimir1 (AWS-0132)0
Resultado del step❌ Failure✅ Success
Resultado del job🏁 Job failed🏁 Job succeeded
¿Llegó a terraform init?No

La diferencia entre ambas corridas no es que el bucket cambió — sigue exactamente igual, sin clave KMS administrada por el cliente. Lo que cambió es que el equipo decidió, por escrito, que ese hallazgo específico es un riesgo aceptado por ahora, y el gate respeta esa decisión documentada en vez de bloquear indefinidamente algo que ya se evaluó conscientemente.


Errores comunes

Colocar el step de escaneo después de Terraform init, sin razón para el orden (de eficiencia perdida). Qué pasa: alguien agrega el step de Trivy al final del workflow, después de terraform plan, razonando que "el orden no importa mientras esté en algún lado". Cómo detectarlo: si un cambio con un hallazgo HIGH sin suprimir corre terraform init, terraform validate, la conexión a LocalStack, y terraform plan completos —minutos de trabajo— antes de fallar por algo que se podría haber detectado en segundos, sobre HCL crudo, sin ninguna de esas dependencias. Cómo corregirlo: el escaneo de configuración no necesita ningún estado de Terraform inicializado — colócalo lo más temprano posible, exactamente como esta lección, para fallar rápido y no gastar minutos de runner en un cambio que de todos modos no va a pasar el gate.

Olvidar --exit-code 1 y sorprenderse de que el step "pasa" a pesar de hallazgos HIGH. Qué pasa: alguien copia el comando trivy config . de la lección 4, sin el flag --exit-code, y lo pega directamente en el step de ci.yml. Cómo detectarlo: el step muestra los hallazgos en la salida, pero el ícono es ✅, no ❌, y el job sigue corriendo hasta el final sin importar cuántos hallazgos CRITICAL haya. Cómo corregirlo: trivy config sin --exit-code está pensado para exploración interactiva, no para un gate — siempre agrega --exit-code 1 (o el valor que prefieras) cuando el resultado del comando tiene que determinar si un job de CI pasa o falla.

Probar el nuevo step solo con git diff, sin correrlo de verdad con act. Qué pasa: alguien revisa visualmente el YAML del step nuevo, confirma que "se ve bien", y lo commitea sin ejecutarlo contra un runner real. Cómo detectarlo: si tu confianza en que el step funciona viene solo de leer el archivo, no de una corrida real con código de salida observado. Cómo corregirlo: la única forma honesta de confirmar que un gate de CI realmente bloquea lo que debería bloquear es correrlo — como esta lección hizo dos veces, viendo el Job failed real antes del Job succeeded real. Un YAML que "se ve bien" puede tener un typo en el nombre del flag, una indentación incorrecta que YAML interpreta de forma distinta a la esperada, o cualquier otro error silencioso que solo una corrida real revela.


Ejercicios

Ejercicio 1 — Explica por qué el gate de esta lección usa --severity CRITICAL,HIGH y no exige cero hallazgos de cualquier severidad. A un compañero que propone que el gate debería fallar ante cualquier hallazgo, sin importar la severidad, dale la razón de diseño de esta lección.

Ver solución

Un gate que bloquea el merge ante cualquier hallazgo, sin importar severidad, convertiría cada LOW —como AWS-0025, cifrado de DynamoDB con clave administrada por AWS, un hallazgo real pero de bajo impacto— en un bloqueador obligatorio para todo el equipo, todo el tiempo, sin distinguir "esto puede esperar" de "esto no puede fusionarse". La lección 6 ya estableció que no todo hallazgo merece el mismo tratamiento — acotar el gate automático a CRITICAL/HIGH refleja esa misma disciplina a nivel de pipeline: lo urgente bloquea automáticamente, lo demás queda visible (corriendo trivy config . sin el filtro de severidad, a mano o en un reporte separado) pero no detiene el flujo de trabajo del equipo.

Ejercicio 2 — Predice qué pasaría si alguien intentara mezclar un hallazgo AWS-0132 de un bucket distinto al de esta lección. Si Andes Cargo agregara un segundo bucket S3 en el futuro, también sin clave KMS administrada por el cliente, ¿la entrada AWS-0132 del .trivyignore de esta lección lo suprimiría también?

Ver solución

— y es un detalle importante para entender bien el mecanismo. .trivyignore suprime por ID de chequeo, no por recurso específico ni por archivo — una entrada AWS-0132 en el archivo suprime cualquier ocurrencia de ese chequeo en todo el proyecto, sobre cualquier bucket S3 que lo dispare. Si Andes Cargo agregara un segundo bucket con el mismo problema, .trivyignore lo suprimiría automáticamente también, sin ninguna alerta nueva — lo cual puede ser exactamente lo que quieres (si la decisión de negocio aplica a todos los buckets del proyecto) o un riesgo real si la decisión solo tenía sentido para el bucket original. Vale la pena revisar .trivyignore cada vez que se agrega un recurso nuevo del mismo tipo, precisamente por este alcance amplio.

Ejercicio 3 — Diseña el mensaje de commit que documentaría correctamente el Paso 4 de esta lección, siguiendo la disciplina de mensajes de commit descriptivos que ya viste en cicd-and-gitops-on-aws-guide. Sin mirar el comando exacto de esta lección, escribe un mensaje de commit de una línea que explique qué cambió y por qué, no solo "update .trivyignore".

Ver solución

Algo como: Suppress AWS-0132 (S3 CMK encryption): out of $0 lab scope, documented in .trivyignore — un mensaje que, leído seis meses después sin abrir el diff, ya comunica qué se suprimió y la razón de fondo. Comparado con "update .trivyignore" —el mensaje que no dice nada—, la diferencia es la misma disciplina que cicd-and-gitops-on-aws-guide exigió para cada commit de esa guía: el mensaje es documentación, no solo un identificador de cambio.


Resumen y siguiente paso

En esta lección agregaste dos steps nuevos a ci.yml —instalar Trivy, correr trivy config --exit-code 1 --severity CRITICAL,HIGH .— colocados antes de terraform init para fallar rápido sobre HCL crudo. Corriste el pipeline completo con act dos veces, contra Docker real: la primera, el gate falló de verdad sobre un hallazgo HIGH real (AWS-0132) que todavía no tenía decisión documentada; la segunda, después de suprimirlo con una razón escrita en .trivyignore —la misma disciplina de la lección 6—, el pipeline completo corrió de punta a punta con Job succeeded.

Antes de avanzar deberías poder: explicar por qué el escaneo va antes de terraform init en el orden de steps; escribir el flag --exit-code de memoria y explicar qué pasa sin él; y describir, con evidencia de las dos corridas de esta lección, la diferencia entre un gate que bloquea y uno que solo informa.

La lección 8, el proyecto que cierra este módulo, integra el escaneo completo —Trivy y Checkov, sobre el andes-cargo-infra/ real de este módulo, no el laboratorio aislado de esta lección— en un reporte de postura de seguridad con evidencia SARIF/JSON, el entregable final de este módulo.

Recursos

  1. trivy.dev — Exit Code — documentación oficial del flag --exit-code y su uso en pipelines de CI.
  2. nektosact.com — User Guide — referencia completa de act, ya usada en cicd-and-gitops-on-aws-guide.
  3. Este curso, Módulo 5, lección 6 — la disciplina de "arreglar, suprimir o aceptar" que esta lección aplica a nivel de pipeline.
  4. cicd-and-gitops-on-aws-guide, Módulo 3, lección 8 — el ci.yml original de nueve steps que esta lección extiende sin reescribir.