# Trivy — vulnerabilidades de imagen e IaC

!!! info "Qué problema resuelve"
    Trivy es un escáner de seguridad multipropósito. En este pipeline se usa
    para dos cosas **distintas**, con dos comandos distintos:

    - `trivy image`: busca CVEs conocidas en los paquetes que terminan
      dentro de la imagen Docker final (el sistema operativo base, y las
      librerías de Node.js instaladas).
    - `trivy config`: busca **misconfiguraciones** en los manifiestos de
      Kubernetes (YAML) — no vulnerabilidades de código, sino configuración
      insegura (contenedor como root, sin límites de recursos, etc.).

    Son preguntas distintas: "¿esta imagen tiene código con bugs de
    seguridad conocidos?" contra "¿esta manera de desplegar el contenedor
    es insegura, aunque el código adentro esté perfecto?".

## Dónde corre cada uno en el pipeline

```mermaid
flowchart LR
    A[checkout] --> B["trivy config<br/>(frontend.yaml)"]
    B --> C[docker login]
    C --> D["docker build<br/>(push: false, load: true)"]
    D --> E["trivy image --severity CRITICAL<br/>--exit-code 1"]
    E -- "CRITICAL con fix" --> X["❌ pipeline detenido<br/>no se sube la imagen"]
    E -- "sin CRITICAL" --> F["trivy image --severity HIGH<br/>--exit-code 0 (informativo)"]
    F --> G["docker push"]
```

El escaneo de imagen corre **después de construir la imagen, antes de
subirla al registry** — por eso el build ahora usa
`push: false, load: true` (la imagen queda en el daemon Docker del runner,
pero no sale de ahí hasta pasar el gate de CRITICAL). El escaneo de
manifiestos (`trivy config`) no depende de la imagen, así que corre antes,
junto a las otras validaciones del código.

## Umbral de severidad (y por qué)

| Severidad | Comportamiento | Por qué |
|---|---|---|
| `CRITICAL` | Bloquea (`--exit-code 1`) | Si existe un fix disponible para una CVE crítica, no tiene sentido publicar la imagen igual |
| `HIGH` | Informa, no bloquea (`--exit-code 0`) | Mientras aprendemos a leer los reportes, HIGH se revisa pero no frena el flujo — bloquear de entrada en HIGH hubiera parado el pipeline en el primer intento real (ver ejemplo abajo) |

Ambos steps usan `--ignore-unfixed`: si Trivy no tiene un `FixedVersion`
para reportar, bloquear o hasta advertir no ayuda en nada — no hay acción
posible más que esperar a que el mantenedor del paquete publique un
parche.

!!! tip "Por qué no escanear todo junto con un solo umbral"
    Trivy permite pedir `--severity CRITICAL,HIGH` en una sola corrida,
    pero el `--exit-code` se aplica igual a toda la corrida — no se puede
    decir "bloqueá en CRITICAL, pero en HIGH solo avisá" en un solo
    comando. Por eso son dos steps separados, cada uno con su propio
    umbral y su propio `--exit-code`.

## Ejemplo real: un CRITICAL que sí bloqueaba

Antes de integrar este step, construimos la imagen real de
`workloads/ecommerce` y corrimos Trivy contra ella para validar el
pipeline. Encontró esto:

```text
Node.js (node-pkg)
Total: 1 (CRITICAL: 1)

tar (7.5.15 → 7.5.19)  CVE-2026-59873  CRITICAL
tar: node-tar: Denial of Service via crafted gzip bomb
```

**El detalle importante no era el CVE en sí, sino dónde vivía:**
`/usr/local/lib/node_modules/npm/node_modules/tar/` — ese `tar` no es una
dependencia de ARI Shopping, es el que trae **empaquetado el propio
`npm`** dentro de la imagen base `node:24.18.0-bookworm-slim`. El
`Dockerfile` original hacía `FROM ${NODE_IMAGE} AS runner` para el stage
final, heredando el Node.js completo — con `npm`, `npx` y `corepack`
incluidos — aunque en producción el contenedor solo ejecuta
`node server.js` y **nunca** invoca `npm`.

!!! danger "No se arregla solo subiendo la versión de Node"
    Antes de tocar el Dockerfile probamos si un patch más nuevo de la
    imagen base ya traía el `tar` corregido: `node:24.19.0-bookworm-slim`
    trae `npm` con `tar@7.5.16` — sigue por debajo del `7.5.19` con el
    fix. El problema no es "Node desactualizado", es que el runtime de
    producción no necesita `npm` para nada.

**Fix aplicado** (`workloads/ecommerce/Dockerfile`, stage `runner`):

```diff
+ RUN rm -rf \
+       /usr/local/lib/node_modules/npm \
+       /usr/local/lib/node_modules/corepack \
+       /usr/local/bin/npm \
+       /usr/local/bin/npx \
+       /usr/local/bin/corepack
```

Después del fix: **0 CRITICAL**, y de paso las HIGH fixable bajaron de 21
a 15 (varias venían de dependencias de ese mismo `npm` empaquetado, no de
la app). Se validó que la imagen sigue arrancando y respondiendo
`GET /api/health` con `200` después de sacar `npm`.

**Lección:** sacar herramientas que la imagen de producción no necesita
en runtime no es solo "buena práctica" en abstracto — reduce
directamente la superficie que Trivy (y un atacante) tienen para
encontrar algo.

## Las HIGH que quedan (ejemplo real, sin arreglar todavía)

Después del fix, `trivy image --severity HIGH` sigue reportando (de
forma informativa, no bloqueante) CVEs reales en dependencias que sí son
de la app — la mayoría en `next` (15.5.10, con fixes disponibles en
15.5.16+ y 15.5.21+ según el CVE), además de `nanoid`, `postcss` y
`sharp`. Esto queda pendiente de revisar como una actualización de
dependencias normal, no como una emergencia de seguridad — es exactamente
para eso que HIGH no bloquea en esta primera vuelta: da visibilidad sin
frenar el flujo mientras se decide cuándo priorizar el bump.

## Cómo priorizar qué arreglar primero

1. **CRITICAL con fix disponible** — ya bloquea el pipeline, así que en
   la práctica no se acumulan.
2. **HIGH en una dependencia que el runtime realmente carga** (como
   `next`, que corre en cada request) — más prioridad que una HIGH en
   una herramienta de build que ni siquiera llega a la imagen final.
3. **HIGH sin ruta de explotación realista** (ej. una librería que solo
   se usa en un script de generación, no en el server) — se puede
   posponer con criterio, documentando por qué.
4. **MEDIUM/LOW** — se revisan en lote, no una por una.

La pregunta que más ayuda a priorizar no es "¿qué tan grave dice la
CVSS que es?", sino "¿este paquete corre en el proceso que atiende
tráfico real, o es una herramienta de build que ni siquiera debería estar
en la imagen final?" — el propio ejemplo de arriba (`npm` dentro de la
imagen de producción) es el caso de manual del segundo.

## Escaneo de manifiestos Kubernetes (`trivy config`)

Corre contra `workloads/ecommerce/frontend.yaml` (el `Deployment` +
`Service` real del frontend) con `--severity CRITICAL,HIGH,MEDIUM`, en
modo informativo (`--exit-code 0`) por ahora.

Hallazgos reales de este manifiesto, hoy:

| Severidad | Regla | Qué significa |
|---|---|---|
| HIGH | [KSV-0014](https://avd.aquasec.com/misconfig/ksv-0014) | El filesystem raíz del contenedor no es de solo lectura |
| HIGH | [KSV-0118](https://avd.aquasec.com/misconfig/ksv-0118) | No se define `securityContext` — Kubernetes usa el default, que permite privilegios de root |
| MEDIUM | [KSV-0012](https://avd.aquasec.com/misconfig/ksv-0012) | El contenedor puede correr como root (aunque la imagen ya defina `USER nextjs` en el Dockerfile, Kubernetes no lo está *forzando* vía `runAsNonRoot`) |
| MEDIUM | [KSV-0001](https://avd.aquasec.com/misconfig/ksv-0001) | El contenedor puede escalar sus propios privilegios (falta `allowPrivilegeEscalation: false`) |
| MEDIUM | [KSV-0104](https://avd.aquasec.com/misconfig/ksv-0104) | No hay perfil de Seccomp configurado |
| MEDIUM | [KSV-0117](https://avd.aquasec.com/misconfig/ksv-0117) | El `containerPort: 80` es un puerto privilegiado (<1024) |
| MEDIUM | [KSV-0125](https://avd.aquasec.com/misconfig/ksv-0125) | La imagen viene de un registry que Trivy no reconoce como "de confianza" por defecto (es autoalojado: `gitea.cruzcloud.net`) |

!!! warning "Lo que este scan NO detecta todavía"
    El pedido original incluía "falta de resource limits" y "falta de
    readiness/liveness probes" como ejemplos de misconfiguración a
    buscar. En la práctica, `trivy config` sí tiene reglas para límites
    de recursos (`KSV-0011` CPU, `KSV-0018` memoria) pero las clasifica
    como **LOW**, por debajo del piso `MEDIUM` que usa este step — y no
    tiene ninguna regla propia para probes de liveness/readiness (eso lo
    cubren otras herramientas, como `kube-score` o `kube-linter`, que no
    forman parte de esta primera integración). Si más adelante se quiere
    cubrir ese hueco específico, es una herramienta aparte, no una opción
    de configuración de Trivy.

Ninguno de estos hallazgos bloquea el pipeline todavía — son reales, pero
corregirlos (agregar `securityContext`, `resources.limits`, etc. a
`frontend.yaml`) es un cambio de GitOps que conviene revisar con calma,
no como reacción automática a un scan.

## Falsos positivos y excepciones

Cuando un hallazgo de Trivy no aplica (por ejemplo, KSV-0125 marcando el
registry propio como "no confiable" — que es exactamente lo esperado en
un lab self-hosted), se documenta con un archivo `.trivyignore` en la
raíz del repo:

```text
# KSV-0125: gitea.cruzcloud.net es nuestro registry self-hosted,
# no un registry público de terceros. Excepción intencional.
KSV-0125
```

Igual que con Gitleaks, cada línea de `.trivyignore` debe poder
justificarse — no es un lugar para silenciar hallazgos incómodos sin
revisarlos primero.
