Syft en modo Web
Para continuar esta serie de artículos sobre Seguridad orientada a Contenedores, vamos a ofrecer una herramienta muy necesaria, ya mucho más centrada en la parte de Auditorías de Ciberseguridad.
Vamos a ofrecer a nuestros usuarios una herramienta de Generación de SBOM. En este artículo vamos a montar un servicio web para usar la herramienta Syft
¿Qué es un herramienta SBOM ?
Una herramienta SBOM (por sus siglas en inglés, Software Bill of Materials o Lista de Materiales de Software) automatiza la creación y gestión de un inventario detallado con todos los componentes, bibliotecas, dependencias y licencias de terceros que conforman una aplicación.
Las herramientas SBOM actúan como una "etiqueta de ingredientes" para el software y son fundamentales para:
- Seguridad (Gestión de Vulnerabilidades): Permiten escanear e identificar rápidamente qué aplicaciones contienen componentes vulnerables ante nuevas amenazas o brechas de seguridad.
- Cumplimiento y licencias: Verifican el cumplimiento legal de los códigos y bibliotecas de código abierto (open source) para evitar demandas o problemas de derechos de autor.
- Transparencia: Facilitan auditorías y son un requisito legal obligatorio en muchos mercados internacionales.
Syft frente a otras soluciones
Syft (de la empresa Anchore) es un generador de SBOM de código abierto diseñado específicamente para este propósito.
Es la elección perfecta cuando necesitamos la mejor precisión en la transparencia de componentes, flexibilidad de formatos y un proceso de seguridad modular.
Trivy se orienta más en los CVE y Syft se centra en el inventario para los informes frente a auditorias
Syft está orientado a:
- Profundidad en la catalogación: Su arquitectura está optimizada para inspeccionar binarios Go, Rust, y dependencias anidadas en uber-jars de Java que otras herramientas pasan por alto.
- Separación de responsabilidades: Syft se centra exclusivamente en generar el SBOM. Esto permite desacoplar la generación de inventarios del escaneo de vulnerabilidades, permitiéndote usar distintas herramientas para cada tarea.
- Cumplimiento y estandarización: Es compatible de forma nativa con los estándares ISO exigidos por normativas globales, generando salidas ricas en metadatos en formatos como SPDX o CycloneDX.
- Análisis capa por capa: Descomprime imágenes de contenedor capa por capa para comprender la procedencia y el origen del software.
¿Porqué usar Syft?
Syft es superior si la creación de SBOM es tu entregable principal (por auditorías o cumplimiento), requieres metadatos muy detallados o integras herramientas especializadas.
Syft como servicio web
Al igual que hicimos con los otras herramientas (Hadolint y Trivy) vamos a ofrecer otra herramienta vía web a los usuarios de nuestra organización para un uso simplificado y eficiente.

Creamos un directorio para dejar los ficheros necesarios para nuestro proyecto y dentro de él creamos tres ficheros:
├── server.js # Backend en Node.js (El que ejecuta Syft)
├── index.html # Interfaz web con buscador y tabla de componentes
└── Dockerfile # Contenedor con Node.js + Syft instaladoY el contenido de los tres ficheros que componen nuestro servicio web.
server.js
const express = require('express');
const { spawn } = require('child_process');
const path = require('path');
const app = express();
const PORT = 5000; // Puerto 5000 para no chocar con los anteriores
app.use(express.json());
app.use(express.static(path.join(__dirname)));
app.post('/sbom', (req, res) => {
const { imageName } = req.body;
if (!imageName) {
return res.status(400).json({ error: 'Debes introducir el nombre de una imagen.' });
}
// Ejecutamos syft apuntando a la imagen y solicitando formato JSON
const syft = spawn('syft', [imageName, '-o', 'json']);
let stdout = '';
let stderr = '';
syft.stdout.on('data', (data) => { stdout += data; });
syft.stderr.on('data', (data) => { stderr += data; });
syft.on('close', (code) => {
if (code !== 0 && !stdout) {
return res.status(500).json({ error: 'Error al ejecutar Syft', details: stderr });
}
try {
const results = JSON.parse(stdout || '{}');
res.json({ success: true, artifacts: results.artifacts || [] });
} catch (e) {
res.status(500).json({ error: 'Error al procesar el reporte SBOM.' });
}
});
});
app.listen(PORT, () => {
console.log(`Syft SBOM Web corriendo en http://localhost:${PORT}`);
});index.html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Syft SBOM Generator</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-slate-900 text-slate-100 min-h-screen flex flex-col justify-between font-sans">
<header class="border-b border-slate-800 bg-slate-900/50 backdrop-blur py-4 px-6">
<div class="max-w-5xl mx-auto flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight text-teal-400 flex items-center gap-3">
<span class="text-4xl sm:text-5xl">📋</span> Syft UI <span class="text-slate-500 font-normal text-lg sm:text-xl ml-1">(SoloConLinux)</span>
</h1>
<span class="text-xs font-mono bg-slate-800 text-slate-400 px-2.5 py-1 rounded-md border border-slate-700 self-start sm:self-auto">
SBOM Analyzer
</span>
</div>
</header>
<main class="flex-grow max-w-5xl w-full mx-auto p-6 space-y-6">
<div class="bg-slate-850 border border-slate-800 rounded-xl p-5 shadow-xl space-y-4">
<label class="block text-sm font-medium text-slate-300">Introduce la imagen Docker para catalogar sus componentes (SBOM):</label>
<div class="flex flex-col sm:flex-row gap-3">
<input type="text" id="image-name" placeholder="ej. alpine:latest, python:3.11-slim, postgres:16" class="flex-grow bg-slate-950 border border-slate-700 rounded-lg px-4 py-2.5 text-slate-200 placeholder-slate-600 focus:outline-none focus:border-teal-500 font-mono text-sm">
<button id="btn-scan" class="bg-teal-600 hover:bg-teal-500 text-white font-medium py-2.5 px-6 rounded-lg transition-all duration-200 cursor-pointer shadow-lg shadow-teal-600/20 active:scale-[0.99] shrink-0">
Generar SBOM
</button>
</div>
</div>
<div class="bg-slate-850 border border-slate-800 rounded-xl p-5 shadow-xl space-y-4">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
<h2 class="text-sm font-medium text-slate-300">Componentes Detectados:</h2>
<div class="flex items-center gap-3 w-full sm:w-auto">
<input type="text" id="table-search" placeholder="Filtrar componentes..." class="hidden bg-slate-950 border border-slate-800 rounded-md px-3 py-1.5 text-xs text-slate-300 placeholder-slate-600 focus:outline-none focus:border-teal-500 w-full sm:w-64">
<button id="btn-download" class="hidden bg-slate-800 hover:bg-slate-700 text-teal-400 font-medium py-1.5 px-3 rounded-md text-xs transition-all border border-slate-700 cursor-pointer flex items-center gap-1.5 whitespace-nowrap">
📥 Exportar SBOM
</button>
</div>
</div>
<div class="overflow-x-auto border border-slate-800 rounded-lg bg-slate-950 min-h-[150px] flex flex-col justify-center">
<div id="status-message" class="text-center p-8 text-slate-500 font-mono text-sm">
Introduce una imagen y haz clic en "Generar SBOM".
</div>
<table id="sbom-table" class="w-full text-left border-collapse hidden">
<thead>
<tr class="border-b border-slate-800 bg-slate-900/50 text-xs font-bold text-slate-400 uppercase tracking-wider">
<th class="p-3">Nombre del Paquete</th>
<th class="p-3">Versión</th>
<th class="p-3">Tipo/Ecosistema</th>
<th class="p-3">Licencia</th>
</tr>
</thead>
<tbody id="table-body" class="text-xs font-mono text-slate-300 divide-y divide-slate-800/40">
</tbody>
</table>
</div>
</div>
</main>
<footer class="text-center py-4 text-xs text-slate-600 border-t border-slate-800">
Syft Web Wrapper
</footer>
<script>
const btnScan = document.getElementById('btn-scan');
const btnDownload = document.getElementById('btn-download');
const imageNameInput = document.getElementById('image-name');
const searchInput = document.getElementById('table-search');
const statusMessage = document.getElementById('status-message');
const sbomTable = document.getElementById('sbom-table');
const tableBody = document.getElementById('table-body');
let allArtifacts = [];
let fullSbomJson = null;
// Acción: Lanzar escaneo
btnScan.addEventListener('click', async () => {
const imageName = imageNameInput.value.trim();
if(!imageName) return alert('Por favor, escribe el nombre de una imagen.');
btnScan.disabled = true;
btnScan.innerText = 'Catalogando...';
sbomTable.classList.add('hidden');
searchInput.classList.add('hidden');
btnDownload.classList.add('hidden');
statusMessage.classList.remove('hidden');
statusMessage.innerHTML = '<span class="text-teal-400 animate-pulse">Syft está indexando binarios, paquetes del SO y dependencias de lenguaje...</span>';
try {
const response = await fetch('/sbom', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ imageName })
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Error generando el SBOM');
// Guardamos el JSON completo para la descarga y las dependencias para la tabla
fullSbomJson = data;
allArtifacts = data.artifacts;
// Mostramos el botón de exportación si hay datos
if (allArtifacts.length > 0) {
btnDownload.classList.remove('hidden');
}
renderTable(allArtifacts);
} catch (error) {
statusMessage.innerHTML = `<div class="text-red-400 font-medium">❌ Error: ${error.message}</div>`;
} finally {
btnScan.disabled = false;
btnScan.innerText = 'Generar SBOM';
}
});
// Función: Pintar la tabla de componentes
function renderTable(artifacts) {
tableBody.innerHTML = '';
if (artifacts.length === 0) {
statusMessage.innerHTML = '<span class="text-slate-500">No se encontraron componentes indexables.</span>';
return;
}
statusMessage.classList.add('hidden');
sbomTable.classList.remove('hidden');
searchInput.classList.remove('hidden');
artifacts.forEach(pkg => {
const licenses = pkg.licenses ? pkg.licenses.join(', ') : 'No declarada';
const row = `
<tr class="hover:bg-slate-900/40 transition-colors">
<td class="p-3 text-slate-200 font-bold">${pkg.name}</td>
<td class="p-3 text-teal-400">${pkg.version}</td>
<td class="p-3"><span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 border border-slate-700 text-slate-400">${pkg.type}</span></td>
<td class="p-3 text-slate-400 max-w-xs truncate" title="${licenses}">${licenses}</td>
</tr>
`;
tableBody.innerHTML += row;
});
}
// Acción: Filtrado en tiempo real en la barra de búsqueda
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
const filtered = allArtifacts.filter(pkg =>
pkg.name.toLowerCase().includes(query) ||
pkg.type.toLowerCase().includes(query) ||
pkg.version.toLowerCase().includes(query)
);
renderTable(filtered);
statusMessage.classList.add('hidden');
sbomTable.classList.remove('hidden');
});
// Acción: Descargar archivo JSON del SBOM
btnDownload.addEventListener('click', () => {
if (!fullSbomJson) return;
const jsonString = JSON.stringify(fullSbomJson, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
// Limpiar caracteres extraños del nombre de la imagen para el archivo
const imageName = imageNameInput.value.trim().replace(/[:/]/g, '-');
const a = document.createElement('a');
a.href = url;
a.download = `sbom-${imageName}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
</script>
</body>
</html>version 2.0 con "exportador"
Dockerfile
FROM node:20-alpine
# Instalar curl para descargar Syft
RUN apk add --no-cache curl
# Descargar e instalar el binario oficial de Syft
RUN curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
WORKDIR /app
# Inicializar e instalar Express
COPY server.js index.html ./
RUN npm init -y && npm install express
EXPOSE 5000
CMD ["node", "server.js"]Contruir nuestra imagen de syft-web
Llega el momento de crear la imagen y el proceso es tan sencillo como ejecutar el comando:
docker build -t syft-web .Iniciar el servicio Syft-Web
Al igual que hicimos con los otras herramientas, cada uno de los contenedores lo iniciaremos en otro puerto para que no choquen entre ellos, en nuestro caso lo iniciaremos en el puerto 5000, con lo que una vez lo tengas iniciado puedes acceder a el usando la url: http://localhost:5000
Para poder acceder a cualquier imagen ya sea local, remota o de algun registryde nuestra organización podemos iniciarlo compartiendo el socket
Para iniciarlo simplemente ejecutamos lo siguiente:
docker run -d \
-p 5000:5000 \
-v /var/run/docker.sock:/var/run/docker.sock \
--name syft-web \
syft-webUso: Generación y Exportación del SBOM
Con el contenedor iniciado tan solo tenemos que indicar la imagen (local, remota, registry interno, etc) de la que deseamos generar el SBOM.

En la captura que os muestro, he solicitado crear el SBOM para la imagen postgres:16
En la herramienta que he creado, se puede usar un filtro para poder localizar algún paquete en particular que querramos localizar.
Además disponemos de un botón Exportar SBOMpara exportar todo el resultado en formato JSON:

Con esta herramienta se facilita a los grupos responsables de CiberSeguridad el entregar los SBOM de las imágenes cuando se solicite en una auditoría.