feat(devsecops): generar SBOM (Syft) de la imagen del frontend

Corre después del gate de CRITICAL de Trivy, sobre la imagen ya
construida. Genera CycloneDX y SPDX, publicados como artifact
sbom-v1.0.X en cada build.

Validado contra la imagen real: 3528 componentes CycloneDX / 228
paquetes SPDX. Documenta en docs/devsecops/sbom.md el propósito
(trazabilidad tipo "log4shell"), diferencia entre formatos, y un
hallazgo real (yarn empaquetado sin usarse, mismo patrón que el npm
sacado en el fix de Trivy).
This commit is contained in:
2026-08-14 21:14:00 -05:00
parent 4aed9f6c5c
commit b6038e06ca
3 changed files with 159 additions and 0 deletions
@@ -0,0 +1,124 @@
# SBOM (Syft) — inventario de software
!!! info "Qué es un SBOM"
SBOM = *Software Bill of Materials* — literalmente, una "lista de
materiales" del software, igual que la lista de ingredientes de un
producto. Es un documento (JSON, en este caso) que enumera **cada
paquete que terminó dentro de la imagen final**: nombre, versión
exacta, de dónde viene (`npm`, `deb`, etc.) y, cuando aplica, su
licencia.
[Syft](https://github.com/anchore/syft) genera ese inventario
inspeccionando la imagen Docker ya construida — no necesita acceso al
código fuente ni a `package.json`, lee directamente lo que quedó
instalado en los layers de la imagen.
## Por qué importa: el escenario "log4shell"
En diciembre de 2021 apareció una vulnerabilidad crítica en Log4j (una
librería de logging de Java) usada, directa o indirectamente, en una
cantidad enorme de software. La pregunta que todo equipo tuvo que
responder en horas, no en días, fue: **"¿nosotros usamos esto, en algún
lugar, aunque sea una dependencia de una dependencia?"**
Sin un SBOM, esa respuesta implica revisar manualmente cada
`package.json`, cada imagen Docker, cada servicio — y confiar en que no
se te escapó una dependencia transitiva de tres niveles de profundidad.
Con un SBOM generado en cada build y guardado como artifact, la respuesta
es una búsqueda de texto sobre un archivo:
```bash
grep -i "nombre-del-paquete-afectado" sbom.cdx.json
```
Si aparece, sabés exactamente en qué versión, y podés cruzarlo contra el
aviso de seguridad para saber si tu versión específica está afectada —
en minutos, no en una auditoría manual del repo completo.
## Qué genera este pipeline
Un step de Syft corre sobre la imagen ya construida (la misma que pasó
el gate de CRITICAL de Trivy) y produce **dos formatos** del mismo
inventario, publicados como artifact del pipeline:
- `sbom.cdx.json` — [CycloneDX](https://cyclonedx.org/), el formato con
mejor soporte en herramientas de consulta/alertas automáticas de CVEs.
- `sbom.spdx.json` — [SPDX](https://spdx.dev/), el estándar ISO, más
orientado a cumplimiento de licencias y trazabilidad legal.
No hay una razón fuerte para elegir solo uno en esta etapa — generar
ambos cuesta segundos y cada formato es mejor para un caso de uso
distinto, así que se publican los dos.
## Resultado real de este build
Corriendo Syft contra la imagen real de `workloads/ecommerce`:
| Formato | Paquetes listados |
|---|---|
| CycloneDX | 3528 componentes |
| SPDX | 228 paquetes |
!!! tip "¿Por qué el número es tan distinto entre formatos?"
No es un error — cada formato tiene un nivel de detalle distinto.
CycloneDX de Syft incluye entradas más granulares (variantes,
sub-paquetes, entradas sin versión resuelta marcadas `UNKNOWN`)
mientras que SPDX agrupa a un nivel más alto. Para "¿tengo este
paquete, sí o no?" cualquiera de los dos sirve; para conteos exactos,
hay que saber cuál se está mirando.
Ejemplo de una entrada real (`sbom.cdx.json`, recortado):
```json
{
"name": "next",
"version": "15.5.10",
"licenses": [{ "license": { "id": "MIT" } }],
"purl": "pkg:npm/[email protected]",
"properties": [
{ "name": "syft:package:language", "value": "javascript" },
{ "name": "syft:package:type", "value": "npm" }
]
}
```
El campo `purl` (*Package URL*) es el identificador estándar que usan
Trivy, Syft, GitHub Advisories y la mayoría de las bases de datos de
CVEs para referirse a "este paquete, en este ecosistema, en esta
versión" — es lo que hace posible cruzar un SBOM contra un aviso de
seguridad de forma automática, sin depender de que el nombre coincida
exactamente en texto libre.
!!! example "El SBOM también sirve para encontrar cosas que sobran"
Revisando el inventario real apareció `[email protected]` — viene
empaquetado en la imagen base de Node igual que el `npm` que ya se
sacó del stage final en el fix de [Trivy](trivy.md). No es una
vulnerabilidad activa hoy, pero es exactamente el tipo de hallazgo
que un SBOM hace visible para revisar después: herramientas que
viajan en la imagen de producción sin que el runtime las necesite.
## Cómo se consulta
El SBOM se publica como artifact del pipeline (`sbom-v1.0.X`, con ambos
archivos) en cada build. Para consultarlo:
1. Descargar el artifact de la corrida del pipeline que te interesa
(o del último build de `main`, para saber qué corre en producción
ahora mismo).
2. Buscar el paquete en cuestión:
```bash
python3 -c "
import json
d = json.load(open('sbom.cdx.json'))
for c in d['components']:
if c['name'] == 'next':
print(c['name'], c['version'])
"
```
(o `grep -A3 '"name": "next"' sbom.cdx.json` si no hay Python a mano).
3. Si el paquete aparece, confirmar la versión contra el aviso de
seguridad para saber si aplica.
No hace falta memorizar el formato — el punto de tener el SBOM ya
generado es no depender de reconstruir esta información bajo presión
el día que aparezca la próxima CVE grande.