Módulo 5: Scanning Iac And Dependencies

6. Leyendo un hallazgo: arreglar, suprimir o aceptar el riesgo

Descripción

Veintiséis hallazgos combinados —once de Trivy, quince de Checkov, ya mapeados en la lección anterior— no se resuelven todos de la misma forma, ni deberían. Esta lección enseña las tres respuestas legítimas frente a un hallazgo real: arreglar el HCL cuando el arreglo es barato y correcto; suprimir, con una razón escrita y verificable, cuando el hallazgo es real pero fuera de alcance para este proyecto específico; y, en casos que esta lección nombra pero no ejecuta aquí, aceptar el riesgo documentándolo sin ocultarlo. Vas a hacer las dos primeras de verdad, sobre hallazgos reales de la lección 4 y 5, con las herramientas re-corridas para confirmar cada cambio.

Conexión con el módulo

Las lecciones 4 y 5 produjeron una lista larga de trabajo pendiente. Esta lección es donde esa lista deja de ser un inventario y se convierte en decisiones — el mismo tipo de criterio que un equipo de seguridad real aplica todos los días: no todo hallazgo merece el mismo tratamiento, y la peor respuesta posible es ignorar uno en silencio, sin que quede registro de que alguien lo vio y decidió algo al respecto.


Analogía: la excepción firmada, no la alarma apagada

Volviendo a la analogía del inspector de la lección 1: si el inspector marca que la puerta del sótano no tiene una barra de pánico, tienes tres respuestas honestas. Puedes instalarla —el arreglo directo—. Puedes decidir que esa puerta específica da a un patio interior sin salida a la calle, así que la barra de pánico no aporta seguridad real ahí, y escribir esa justificación en el reporte de inspección, firmada, para que el próximo inspector no vuelva a marcarla como sorpresa. O puedes reconocer que sí hace falta, pero no tienes presupuesto este trimestre, y anotarlo como riesgo aceptado, con fecha de revisión. Lo que nunca es una respuesta legítima es desconectar la alarma que detecta la puerta abierta para que el inspector deje de mencionarla — eso no resuelve el riesgo, solo esconde la evidencia de que existe.

Suprimir un hallazgo con #checkov:skip= o .trivyignore, cuando se hace con una razón escrita al lado, es la excepción firmada del inspector — nunca la alarma apagada.


Parte 1 — Arreglar: AWS-0024 / CKV_AWS_28, recuperación a un punto en el tiempo de Shipments

Este es el candidato obvio para un arreglo directo: barato, sin efectos secundarios, y sobre el activo número uno de THREAT-MODEL.md. Recuerda el hallazgo de la lección 4:

AWS-0024 (MEDIUM): Point-in-time recovery is not enabled.

Y su equivalente en Checkov (lección 5):

Check: CKV_AWS_28: "Ensure DynamoDB point in time recovery (backup) is enabled"
	FAILED for resource: aws_dynamodb_table.shipments

El arreglo, en dynamodb.tf

resource "aws_dynamodb_table" "shipments" {
  name         = var.table_name
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "shipmentId"

  attribute {
    name = "shipmentId"
    type = "S"
  }

  point_in_time_recovery {
    enabled = true
  }

  tags = local.common_tags
}

Un bloque nuevo, cuatro líneas, sin tocar ningún atributo existente. terraform validate confirma que el cambio es sintácticamente correcto antes de volver a escanear:

terraform fmt -recursive
terraform validate

Qué esperar (literal):

Success! The configuration is valid.

Re-escaneando: la confirmación de que el arreglo funcionó

trivy config dynamodb.tf

Qué esperar (literal — ejecutado para escribir esta lección, después del arreglo):

dynamodb.tf (terraform)
=======================
Tests: 1 (SUCCESSES: 0, FAILURES: 1)
Failures: 1 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 0, CRITICAL: 0)

AWS-0025 (LOW): Table encryption does not use a customer-managed KMS key.

AWS-0024 ya no aparece. Solo queda AWS-0025 (cifrado con clave administrada por el cliente), un hallazgo distinto, sin relación con la recuperación a un punto en el tiempo. Tests: 1 en vez de Tests: 2 — la confirmación numérica de que el arreglo resolvió exactamente el hallazgo que se proponía resolver, ni más ni menos.

checkov -d . --compact --quiet 2>&1 | grep "CKV_AWS_28\|Passed checks"

Qué esperar (literal):

Passed checks: 114, Failed checks: 14, Skipped checks: 0

CKV_AWS_28 desapareció de la lista de fallidos —113 pasados subió a 114, 15 fallidos bajó a 14—, confirmando el mismo arreglo desde el ángulo del segundo escáner, con su propio motor, de forma completamente independiente.


Parte 2 — Suprimir con Checkov: CKV_AWS_144, replicación entre regiones

Este es el candidato correcto para suprimir, no arreglar: "Ensure that S3 bucket has cross-region replication enabled". Andes Cargo opera en una sola región (us-east-1, fija desde aws-core-services-guide) — replicar el bucket de manifiestos a una segunda región agregaría un recurso, un costo recurrente, y una superficie operativa nueva, sin resolver ningún riesgo de seguridad real para un sistema que, por diseño, todavía no necesita tolerancia a falla de región completa.

El comentario de supresión — y el detalle que solo se descubre corriendo el comando de verdad

La sintaxis documentada de Checkov es un comentario #checkov:skip=<ID>:<razón>. Lo que no siempre queda claro sin probarlo es exactamente dónde tiene que vivir ese comentario. La primera vez que se escribió esta lección, el comentario se puso arriba de la línea resource:

#checkov:skip=CKV_AWS_144:Single-region deployment by design (us-east-1 only)
resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name
  tags   = var.tags
}

Esto no suprimió el chequeo. Un re-escaneo mostró CKV_AWS_144 seguía apareciendo como FAILED, Skipped checks: 0. La ubicación correcta, confirmada corriendo el comando —no asumida de la documentación—, es dentro del bloque, como la primera línea del cuerpo del resource:

resource "aws_s3_bucket" "this" {
  # checkov:skip=CKV_AWS_144:Single-region deployment by design (us-east-1 only); cross-region replication is out of scope for this $0 lab
  bucket = var.bucket_name
  tags   = var.tags
}

Confirmando la supresión

checkov -d . --compact --quiet 2>&1 | grep "Passed checks\|CKV_AWS_144"

Qué esperar (literal — ejecutado para escribir esta lección, con el comentario en la ubicación correcta):

Passed checks: 114, Failed checks: 13, Skipped checks: 1

CKV_AWS_144 ya no aparece en la lista de FAILED — pero tampoco cuenta como Passed. El conteo de Skipped checks subió de 0 a 1, una tercera categoría, distinta de pasar o fallar: el chequeo se evaluó, se identificó una supresión documentada, y se excluyó del resultado final sin pretender que el bucket sí tiene replicación entre regiones. Es la diferencia exacta entre la excepción firmada y la alarma apagada de la analogía de esta lección — el registro de que alguien vio el hallazgo y decidió algo, en vez de que el hallazgo simplemente desapareciera.

Nota sobre el reporte JSON: si generas el reporte con checkov -d . -o json, vas a encontrar el conteo agregado en summary.skipped (confirmado: 1 en esta corrida), pero el arreglo results.skipped_checks puede llegar vacío para un chequeo evaluado dentro de un módulo local, según la versión exacta de Checkov. Si necesitas la razón exacta de una supresión, por escrito y a prueba de qué tan bien la exporte cada versión de la herramienta, la fuente confiable es siempre el comentario dentro del propio archivo HCL — nunca dependas solo del reporte generado para reconstruir por qué se tomó una decisión.


Parte 3 — Suprimir con Trivy: AWS-0089, registro de acceso del bucket

Trivy no lee comentarios #checkov:skip= — usa su propio mecanismo, un archivo .trivyignore en la raíz del proyecto, con un ID de chequeo por línea. El candidato: AWS-0089, registro de acceso deshabilitado en andes-cargo-shipment-docs. Configurar el registro de acceso de S3 exige declarar un segundo bucket de destino, con su propia política de retención y costo — una pieza operativa completa, fuera del alcance de este laboratorio $0, y parcialmente cubierta por otro control ya nombrado en esta guía: CloudTrail (Módulo 7), que registra las llamadas a la API sobre este bucket, aunque no el detalle a nivel de objeto que un log de acceso de S3 ofrecería.

cat > .trivyignore <<'EOF'
# AWS-0089: S3 bucket logging disabled on andes-cargo-shipment-docs.
# Access logging targets a second bucket + its own retention/cost policy, out of scope
# for this $0 LocalStack lab. Detection of unauthorized access already covered by
# CloudTrail (Module 7 of this guide). Revisit if this project ever targets real AWS.
AWS-0089
EOF

Cada línea que no empieza con # es un ID de chequeo a ignorar — el formato es deliberadamente simple, una lista plana, sin sintaxis anidada.

trivy config .

Qué esperar (literal — ejecutado para escribir esta lección, con el arreglo de la Parte 1 y las dos supresiones aplicadas):

Report Summary

┌───────────────────────────┬───────────┬───────────────────┐
│          Target           │   Type    │ Misconfigurations │
├───────────────────────────┼───────────┼───────────────────┤
│ .                         │ terraform │         0         │
├───────────────────────────┼───────────┼───────────────────┤
│ dynamodb.tf               │ terraform │         1         │
├───────────────────────────┼───────────┼───────────────────┤
│ lambda.tf                 │ terraform │         1         │
├───────────────────────────┼───────────┼───────────────────┤
│ modules/s3-bucket/main.tf │ terraform │         6         │
├───────────────────────────┼───────────┼───────────────────┤
│ secrets.tf                │ terraform │         1         │
└───────────────────────────┴───────────┴───────────────────┘

De once hallazgos a nueve. dynamodb.tf bajó de dos a uno (el arreglo de la Parte 1). modules/s3-bucket/main.tf bajó de siete a seis (AWS-0089, ignorado por .trivyignore; AWS-0086/0087/0091/0093/0094 y AWS-0132 siguen presentes — el .trivyignore de esta lección solo apunta a AWS-0089, no a los demás). lambda.tf y secrets.tf no cambiaron, porque esta lección no tocó ningún hallazgo de esos dos archivos.


Actualizando RISK-MAP.md: dos decisiones documentadas, ninguna escondida

Si mantienes RISK-MAP.md (Módulo 1, lección 8) al día, este es exactamente el tipo de actualización que corresponde después de esta lección — no solo "arreglado", sino cómo se decidió cada caso:

+ | - | AWS-0024/CKV_AWS_28 | (nuevo, M5) | Point-in-time recovery on Shipments | Add `point_in_time_recovery` block | M5.6 | Resolved: `dynamodb.tf` updated, confirmed via `trivy config` and `checkov -d` |
+ | - | CKV_AWS_144 | (nuevo, M5) | S3 cross-region replication | Documented suppression (single-region by design) | M5.6 | Suppressed via `#checkov:skip=CKV_AWS_144`, reason in HCL comment |
+ | - | AWS-0089 | (nuevo, M5) | S3 access logging disabled | Documented suppression (out of $0 scope, partial CloudTrail coverage) | M5.6 | Suppressed via `.trivyignore`, reason documented in file |

Tres filas, tres decisiones distintas, cada una con su mecanismo de verificación citado — la misma disciplina de evidencia que THREAT-MODEL.md exigió desde el Módulo 1.


Sobre "aceptar el riesgo": el tercer camino, nombrado

Esta lección ejecutó dos de las tres respuestas legítimas. La tercera —aceptar el riesgo sin arreglarlo ni suprimirlo del reporte de la herramienta— es la correcta cuando un hallazgo es real, no se puede resolver ahora (por costo, por dependencia externa, por decisión de negocio pendiente), pero tampoco corresponde ocultarlo del reporte de escaneo: quieres que siga apareciendo como FAILED cada vez que alguien corra el escáner, como recordatorio activo, con la justificación viviendo en un documento aparte (un ticket, un registro de riesgos, RISK-MAP.md mismo) en vez de en un comentario de supresión. AWS-0132/CKV_AWS_145 (cifrado del bucket con clave administrada por el cliente) es exactamente ese caso, por ahora: nombrado, justificado, sin arreglar todavía, visible en cada corrida sin bloquear nada. Es un estado que puede quedarse así indefinidamente, o convertirse en una supresión documentada el día que un gate automático exija una respuesta binaria pasa/falla — vas a ver exactamente esa segunda situación en la lección 7, cuando este mismo hallazgo se encuentre con un pipeline que no admite términos medios.


Errores comunes

Poner el comentario #checkov:skip= arriba del resource en vez de adentro (el error real de esta lección, verificado corriendo el comando). Qué pasa: alguien copia un ejemplo de un tutorial o de la documentación sin verificar contra su propia versión de Checkov, y el comentario queda arriba de la línea resource. Cómo detectarlo: si tu Skipped checks sigue en 0 después de agregar el comentario, y el ID que intentaste suprimir sigue apareciendo en la lista de FAILED. Cómo corregirlo: mueve el comentario dentro del bloque resource, como la primera línea de su cuerpo — exactamente el patrón verificado en la Parte 2 de esta lección.

Suprimir un hallazgo sin escribir ninguna razón, solo el ID (de atajo silencioso). Qué pasa: alguien, apurado, escribe #checkov:skip=CKV_AWS_144 sin nada después de los dos puntos, o directamente sin los dos puntos. Cómo detectarlo: si tu comentario de supresión no tiene ningún texto legible después del ID. Cómo corregirlo: la razón no es opcional en el espíritu de esta lección, aunque la sintaxis técnica de Checkov la acepte vacía — sin razón escrita, un revisor seis meses después no tiene forma de saber si la supresión sigue siendo válida o si alguien la puso para "hacer pasar" un pipeline sin pensarlo. Cada supresión de esta lección lleva su razón en la misma línea.

Confundir un hallazgo suprimido con un hallazgo resuelto (de lectura del reporte). Qué pasa: alguien ve que CKV_AWS_144 ya no aparece en FAILED y asume que el bucket ahora sí tiene replicación entre regiones. Cómo detectarlo: si tu entendimiento del estado real de infraestructura viene solo del conteo de Passed/Failed, sin revisar Skipped por separado. Cómo corregirlo: Skipped checks: 1 es una categoría distinta de Passed — el bucket sigue sin replicación entre regiones; lo que cambió es que el equipo documentó, por escrito, por qué eso es una decisión aceptable para este proyecto, no que el riesgo dejó de existir.


Ejercicios

Ejercicio 1 — Reproduce el error de ubicación del comentario, a propósito. En un archivo .tf de prueba, escribe un #checkov:skip= arriba del resource (la ubicación incorrecta) y corre Checkov. Confirma que el chequeo sigue fallando, y después corrige la ubicación.

Ver solución
# main.tf de prueba
#checkov:skip=CKV_AWS_18:test
resource "aws_s3_bucket" "foo" {
  bucket = "foo-bucket"
}
checkov -d . --compact --quiet 2>&1 | grep "Skipped checks\|CKV_AWS_18"

Deberías ver Skipped checks: 0 y CKV_AWS_18 todavía en la lista de FAILED. Ahora mueve el comentario adentro del bloque:

resource "aws_s3_bucket" "foo" {
  # checkov:skip=CKV_AWS_18:test
  bucket = "foo-bucket"
}

Vuelve a correr el mismo comando — Skipped checks: 1, y CKV_AWS_18 desaparece de FAILED. Confirmaste con tus propias manos el mismo comportamiento que documentó esta lección.

Ejercicio 2 — Decide, sin correr ningún comando, si AWS-0025/CKV_AWS_119 (DynamoDB sin KMS CMK) debería arreglarse, suprimirse, o aceptarse. Usando el criterio de esta lección —costo del arreglo, relevancia de negocio, disponibilidad en el laboratorio $0—, argumenta tu elección.

Ver solución

Aceptar, nombrado, no suprimido —el mismo tratamiento que AWS-0132 recibe en el proyecto de la lección 8. Arreglarlo exigiría crear y gestionar una clave KMS administrada por el cliente, con costo recurrente real en una cuenta AWS de verdad y sin equivalente confirmado en LocalStack Hobby — no es un arreglo de una línea como el de la Parte 1 de esta lección. Suprimirlo sin más sería ocultar un hallazgo real sin ninguna razón de alcance tan clara como la de AWS-0089 (ausencia de un bucket de destino) o CKV_AWS_144 (arquitectura de una sola región por diseño). La respuesta correcta es dejarlo visible en cada corrida, documentado como decisión pendiente de costo/beneficio, exactamente el tercer camino que esta lección nombra.

Ejercicio 3 — Escribe la entrada completa de .trivyignore para un hallazgo hipotético AWS-0999 sobre un bucket de logs, con una razón de dos líneas. Sin correr ningún comando, escribe el bloque completo, siguiendo el formato exacto de la Parte 3 de esta lección.

Ver solución
# AWS-0999: bucket de logs sin versionado.
# Los logs son de solo escritura y se rotan cada 30 días vía lifecycle policy;
# versionar objetos que nunca se sobrescriben no aporta protección adicional real.
AWS-0999

El patrón es el mismo que ya usó esta lección para AWS-0089: comentarios # explicando la razón, tantas líneas como haga falta, seguidos del ID exacto del chequeo en su propia línea, sin ningún prefijo adicional.


Resumen y siguiente paso

En esta lección aplicaste las tres respuestas legítimas frente a un hallazgo de escáner: arreglaste AWS-0024/CKV_AWS_28 con un bloque de cuatro líneas en dynamodb.tf, confirmado con ambas herramientas re-corridas; suprimiste CKV_AWS_144 con un comentario documentado —descubriendo, al hacerlo de verdad, que la ubicación correcta del comentario es dentro del bloque resource, no arriba— y AWS-0089 con un .trivyignore documentado; y nombraste el tercer camino, aceptar sin suprimir, reservado para hallazgos reales sin arreglo barato ni justificación de alcance tan clara. El conteo de Trivy bajó de once a nueve hallazgos; el de Checkov, de quince fallidos a trece fallidos más uno omitido con razón.

Antes de avanzar deberías poder: decidir, para cualquier hallazgo nuevo, cuál de las tres respuestas corresponde; escribir un comentario #checkov:skip= en la ubicación correcta sin tener que probarlo dos veces; y explicar la diferencia entre un hallazgo suprimido y uno resuelto sin dudar.

La lección 7 lleva exactamente este mismo escaneo —ahora con nueve hallazgos de Trivy en vez de once— al pipeline heredado de cicd-and-gitops-on-aws-guide, como un step nuevo de ci.yml corrido con act.

Recursos

  1. www.checkov.io — Suppressing and Skipping Policies — sintaxis oficial de #checkov:skip=, incluida la ubicación correcta dentro del bloque.
  2. trivy.dev — Filtering — documentación oficial de .trivyignore y su formato.
  3. Este curso, Módulo 1, lección 8 — RISK-MAP.md, el documento que esta lección actualiza con tres nuevas filas.
  4. Este curso, Módulo 7 (por venir) — CloudTrail, el control detectivo que complementa parcialmente la supresión de AWS-0089 en esta lección.