Semgrep en modo Web

CiberSeguridad 21 de jun. de 2026

Vamos a crear un nuevo servicio web para otra herramienta muy util semgrep

¿Qué es Semgrep?

Semgrep es una herramienta de análisis estático de código abierto (SAST) y seguridad que permite encontrar vulnerabilidades, errores de lógica y malas prácticas directamente en el código fuente, sin necesidad de compilarlo.

¿Qué la hace diferente?

  • Entiende la semántica: A diferencia de las herramientas tradicionales que usan expresiones regulares (regex), Semgrep comprende la estructura del código y el contexto, lo que reduce drásticamente los "falsos positivos".
  • Personalización rápida: Utiliza reglas simples basadas en el mismo código que ya escribes (usando formato YAML), lo que permite crear filtros personalizados en minutos.
  • Multilenguaje: Soporta más de 30 lenguajes de programación, incluyendo Python, JavaScript/TypeScript, Go, Java, C#, y lenguajes de infraestructura como Terraform.

¿Para qué se utiliza?

  1. Seguridad (SAST / SCA): Detecta credenciales filtradas, inyecciones de código, fallos de autenticación y dependencias vulnerables.
  2. Calidad de software: Identifica código muerto, bucles infinitos o funciones obsoletas.
  3. Automatización (CI/CD): Se integra fácilmente en entornos como GitHub Actions, GitLab CI y Bitbucket para bloquear automáticamente fusiones de código (merges) si el código nuevo contiene fallos.
  4. Revisión local: Los desarrolladores pueden ejecutar escaneos rápidos en su propia terminal antes de subir el código.

Porqué he elegido Semgrep

Semgrep es la joya de la corona del análisis estático moderno (SAST).

Es el estándar moderno para SAST porque es ridículamente rápido y ligero.

Semgrep busca vulnerabilidades directamente en el código fuente actual (como inyecciones SQL, XSS, malas configuraciones o funciones inseguras).

Es increíblemente rápido, no requiere compilar el código y sus reglas cubren desde fallos de seguridad graves (OWASP Top 10) hasta buenas prácticas de calidad de código.

Siguiendo la arquitectura de los artículos anteriores vamos a montar un backend que se ejecutará en el puerto 7000 para evitar conflictos con los otros servicios web.

El servidor clonará el repositorio, ejecutará Semgrep con la suite de reglas por defecto (p/default), exportará los resultados en JSON, limpiará el disco y el frontend generará un reporte descargable en Markdown (.md) para que puedas revisarlo facilmente y usarlo para generar los informes finales en cualquier otro formato (PDF, .doc ó .odt)

Semgrep como servicio Web

Como siempre, nos creamos un nuevo directorio en el que crear nuestros tres ficheros para el proyecto:

├── server.js          # Backend en Node.js (Clona + Semgrep + Limpieza)
├── index.html         # Interfaz web interactiva con visor de vulnerabilidades
└── Dockerfile         # Contenedor con Node.js, Git y Semgrep (vía Python/Pip)

El contenido de los ficheros es el siguiente:

server.js

const express = require('express');
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');

const app = express();
const PORT = 7000; // Puerto asignado para Semgrep SAST

app.use(express.json());
app.use(express.static(path.join(__dirname)));

app.post('/scan', (req, res) => {
    const { repoUrl, username, token } = req.body;

    if (!repoUrl) {
        return res.status(400).json({ error: 'La URL del repositorio es obligatoria.' });
    }

    // 1. Configurar URL con credenciales si existen
    let cloneUrl = repoUrl;
    if (token) {
        try {
            const urlObj = new URL(repoUrl);
            urlObj.username = username || token;
            if (username) urlObj.password = token;
            cloneUrl = urlObj.toString();
        } catch (e) {
            return res.status(400).json({ error: 'Formato de URL inválido.' });
        }
    }

    // 2. Directorio temporal único
    const repoId = crypto.randomBytes(16).toString('hex');
    const targetDir = path.join('/tmp', `semgrep-${repoId}`);

    const cleanUp = () => {
        if (fs.existsSync(targetDir)) {
            fs.rmSync(targetDir, { recursive: true, force: true });
        }
    };

    // 3. Clonar repositorio (Depth 1 es suficiente para SAST del estado actual)
    const gitClone = spawn('git', ['clone', '--depth', '1', cloneUrl, targetDir]);

    gitClone.on('close', (code) => {
        if (code !== 0) {
            cleanUp();
            return res.status(500).json({ error: 'Error al clonar el repositorio. Verifica la URL y permisos.' });
        }

        // 4. Ejecutar Semgrep usando el juego de reglas estándar de la comunidad
        // Forzamos formato JSON y evitamos telemetría externa
        const semgrep = spawn('semgrep', [
            'scan',
            '--config', 'p/default',
            '--json',
            '--quiet',
            targetDir
        ], { maxBuffer: 1024 * 1024 * 10 }); // Buffer amplio para proyectos grandes

        let stdout = '';
        semgrep.stdout.on('data', (data) => { stdout += data; });

        semgrep.on('close', () => {
            let scanResults = { results: [] };
            try {
                if (stdout) {
                    scanResults = JSON.parse(stdout);
                }
            } catch (e) {
                console.error("Error parseando JSON de Semgrep", e);
            }

            // 5. Limpieza total inmediata del código fuente clonado
            cleanUp();

            // Mapeamos solo lo necesario para aliviar la carga del frontend
            res.json({ success: true, findings: scanResults.results || [] });
        });
    });
});

app.listen(PORT, () => {
    console.log(`Semgrep SAST 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>Semgrep SAST Scanner</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-violet-400 flex items-center gap-3">
                <span class="text-4xl sm:text-5xl">🔍</span> Semgrep UI <span class="text-slate-500 font-normal text-lg sm:text-xl ml-1"></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">
                SAST Code Analyzer by SoloConLinux
            </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">
            <div class="space-y-2">
                <label class="block text-sm font-medium text-slate-300">URL del Repositorio Git a analizar (Calidad y Seguridad):</label>
                <input type="text" id="repo-url" placeholder="https://github.com/usuario/mi-proyecto.git" class="w-full 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-violet-500 font-mono text-sm">
            </div>

            <div class="pt-2">
                <label class="inline-flex items-center cursor-pointer gap-2 text-xs text-slate-400 hover:text-slate-200 transition-colors">
                    <input type="checkbox" id="chk-auth" class="rounded bg-slate-950 border-slate-700 text-violet-500 focus:ring-0">
                    ¿Es un repositorio privado? Requiere credenciales
                </label>

                <div id="auth-fields" class="hidden grid grid-cols-1 sm:grid-cols-2 gap-4 mt-3 p-4 bg-slate-900/50 border border-slate-800 rounded-lg">
                    <div class="space-y-1">
                        <label class="block text-xs font-medium text-slate-400">Usuario (Opcional):</label>
                        <input type="text" id="username" placeholder="ej. git" class="w-full bg-slate-950 border border-slate-800 rounded-md px-3 py-2 text-xs text-slate-200 focus:outline-none focus:border-violet-500">
                    </div>
                    <div class="space-y-1">
                        <label class="block text-xs font-medium text-slate-400">Token de Acceso / Contraseña:</label>
                        <input type="password" id="token" placeholder="Token personal de acceso" class="w-full bg-slate-950 border border-slate-800 rounded-md px-3 py-2 text-xs text-slate-200 focus:outline-none focus:border-violet-500">
                    </div>
                </div>
            </div>

            <button id="btn-scan" class="w-full bg-violet-600 hover:bg-violet-500 text-white font-medium py-2.5 px-6 rounded-lg transition-all duration-200 cursor-pointer shadow-lg shadow-violet-600/20 active:scale-[0.99]">
                Escanear Código Fuente
            </button>
        </div>

        <div class="bg-slate-850 border border-slate-800 rounded-xl p-5 shadow-xl space-y-3">
            <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">Vulnerabilidades y Errores Detectados:</h2>
                <button id="btn-download" class="hidden bg-slate-800 hover:bg-slate-700 text-violet-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 Auditoría (.md)
                </button>
            </div>
            
            <div id="result-box" class="bg-slate-950 p-4 rounded-lg border border-slate-800 min-h-[150px] font-mono text-sm overflow-x-auto text-slate-400">
                Configura un repositorio y haz clic en "Escanear Código Fuente".
            </div>
        </div>
    </main>

    <footer class="text-center py-4 text-xs text-slate-600 border-t border-slate-800">
        SoloConLinux · Semgrep Web Wrapper
    </footer>

    <script>
        const chkAuth = document.getElementById('chk-auth');
        const authFields = document.getElementById('auth-fields');
        const btnScan = document.getElementById('btn-scan');
        const btnDownload = document.getElementById('btn-download');
        const resultBox = document.getElementById('result-box');
        const repoUrlInput = document.getElementById('repo-url');

        let currentFindings = [];

        chkAuth.addEventListener('change', (e) => {
            authFields.classList.toggle('hidden', !e.target.checked);
        });

        btnScan.addEventListener('click', async () => {
            const repoUrl = repoUrlInput.value.trim();
            const username = document.getElementById('username').value.trim();
            const token = document.getElementById('token').value.trim();

            if (!repoUrl) return alert('Por favor, introduce la URL del repositorio Git.');

            btnScan.disabled = true;
            btnScan.innerText = 'Analizando código con Semgrep...';
            btnDownload.classList.add('hidden');
            resultBox.innerHTML = '<span class="text-violet-400 animate-pulse">Clonando repositorio y ejecutando reglas SAST de OWASP y Calidad...</span>';

            try {
                const response = await fetch('/scan', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        repoUrl,
                        username: chkAuth.checked ? username : '',
                        token: chkAuth.checked ? token : ''
                    })
                });

                const data = await response.json();
                if (!response.ok) throw new Error(data.error || 'Error durante el escaneo');

                currentFindings = data.findings || [];

                if (currentFindings.length === 0) {
                    resultBox.innerHTML = '<div class="text-emerald-400 font-medium">✨ ¡Felicidades! Código limpio. No se han detectado vulnerabilidades ni malas prácticas de código.</div>';
                } else {
                    btnDownload.classList.remove('hidden');
                    resultBox.innerHTML = `<div class="text-violet-400 font-bold mb-3 font-sans text-xs">⚠️ Se detectaron ${currentFindings.length} anomalías de seguridad o calidad:</div>`;
                    
                    currentFindings.forEach(find => {
                        // Limpiamos la ruta temporal del archivo para que sea legible
                        const cleanPath = find.path.replace(/^\/tmp\/semgrep-[a-f0-9]+\//, '');
                        const severityColor = find.extra.severity === 'ERROR' ? 'bg-red-900 border-red-700 text-red-200' : 'bg-amber-900 border-amber-700 text-amber-200';

                        const card = `
                            <div class="mb-4 p-4 rounded-lg border border-slate-800 bg-slate-900/60 space-y-2 text-xs">
                                <div class="flex items-center justify-between flex-wrap gap-2">
                                    <div>
                                        <span class="px-2 py-0.5 rounded text-[10px] font-black border ${severityColor} mr-2">${find.extra.severity || 'WARNING'}</span>
                                        <span class="text-slate-200 font-bold text-sm break-all">${find.check_id}</span>
                                    </div>
                                    <span class="text-slate-500 font-sans text-[11px]">Categoría: ${find.extra.metadata?.category || 'SCA'}</span>
                                </div>
                                <p class="text-slate-300 font-sans leading-relaxed bg-slate-950/40 p-2 rounded border border-slate-800/40">${find.extra.message}</p>
                                <div class="grid grid-cols-1 md:grid-cols-2 gap-2 text-slate-400 pt-1 font-mono text-[11px]">
                                    <div><b>Archivo:</b> <span class="text-violet-400 break-all">${cleanPath}</span></div>
                                    <div><b>Línea de Impacto:</b> <span class="text-slate-200">${find.start.line}</span></div>
                                </div>
                                <div class="bg-slate-950 p-2 rounded border border-slate-800 text-slate-300 font-mono mt-1 overflow-x-auto whitespace-pre"><code>${find.extra.lines || 'Código no disponible'}</code></div>
                            </div>
                        `;
                        resultBox.innerHTML += card;
                    });
                }

            } catch (error) {
                resultBox.innerHTML = `<div class="text-red-400 font-medium">❌ Error: ${error.message}</div>`;
            } finally {
                btnScan.disabled = false;
                btnScan.innerText = 'Escanear Código Fuente';
            }
        });

        // Descarga de reporte amigable en Markdown
        btnDownload.addEventListener('click', () => {
            if (currentFindings.length === 0) return;

            const repoUrl = repoUrlInput.value.trim();
            const dateStr = new Date().toLocaleString();

            let mdContent = `# 🛡️ Reporte de Auditoría de Código Estático (SAST) - Semgrep\n`;
            mdContent += `* **Repositorio Evaluado:** ${repoUrl}\n`;
            mdContent += `* **Fecha de Ejecución:** ${dateStr}\n`;
            mdContent += `* **Total de Hallazgos:** ${currentFindings.length}\n\n`;
            mdContent += `---\n\n`;
            mdContent += `## Desglose de Defectos y Vulnerabilidades\n\n`;

            currentFindings.forEach((find, index) => {
                const cleanPath = find.path.replace(/^\/tmp\/semgrep-[a-f0-9]+\//, '');
                mdContent += `### Hallazgo #${index + 1}: [${find.extra.severity || 'WARNING'}] ${find.check_id}\n`;
                mdContent += `* **Ubicación:** \`${cleanPath}\` (Línea ${find.start.line})\n`;
                mdContent += `* **CWE Asociado:** ${find.extra.metadata?.cwe || 'No listado'}\n`;
                mdContent += `* **Descripción:** ${find.extra.message}\n\n`;
                mdContent += `#### Código Afectado:\n`;
                mdContent += `\`\`\`text\n${find.extra.lines}\n\`\`\`\n`;
                mdContent += `_ _ _\n\n`;
            });

            const blob = new Blob([mdContent], { type: 'text/markdown;charset=utf-8' });
            const url = URL.createObjectURL(blob);
            const safeRepoName = repoUrl.split('/').pop().replace('.git', '') || 'sast-report';
            
            const a = document.createElement('a');
            a.href = url;
            a.download = `sast-semgrep-${safeRepoName}.md`;
            document.body.appendChild(a);
            a.click();
            
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        });
    </script>
</body>
</html>

Dockerfile

FROM node:20-alpine

# Instalar Git, Curl, Python3 y herramientas de compilación para Semgrep
RUN apk add --no-cache git curl python3 py3-pip build-base python3-dev

# Crear un entorno virtual de Python e instalar Semgrep de forma global
RUN python3 -m venv /opt/semgrep-venv && \
    /opt/semgrep-venv/bin/pip install --no-cache-dir semgrep && \
    ln -s /opt/semgrep-venv/bin/semgrep /usr/local/bin/semgrep

WORKDIR /app

# Instalar Express en el proyecto backend
COPY server.js index.html ./
RUN npm init -y && npm install express

# Exponer el puerto asignado para la herramienta
EXPOSE 7000

CMD ["node", "server.js"]

Creación de la imagen semgrep-web

El proceso no puede ser más sencillo usando docker:

docker build -t semgrep-web .

Uso del contenedor e Informes

Iniciaremos el contenedor con el servicio web que estará accesible en el puerto 7000 en la url: http://localhost:7000

Para ello ejecutaremos la siguiente instrucción:

docker run -d -p 7000:7000 --name semgrep-web semgrep-web

Al igual que en las otras herramientas que he creado anteriormente, podemos usar repositorios públicos o privados. En caso necesarios usaremos usuario/contraseña o usuario/token para la autenticación en el repositorio.

Existen repositorios que podemos usar para comprobar si nuestra herramienta web funciona correctamente como por ejemplo:

GitHub - semgrep/semgrep-pro-tests: example test cases for DeepSemgrep
example test cases for DeepSemgrep. Contribute to semgrep/semgrep-pro-tests development by creating an account on GitHub.

Apuntamos nuestro servicio web a ese repositorio y comprobamos que funciona correctamente:

Además el informe en formato .md es útil para la revisión off-line del mismo y la corrección del código afectado.

Con este cierro (por ahora) el kit de herramientas Web para análisis de QA y CiberSeguridad.

Etiquetas

Luis GuLo

🐧 SysAdmin GNU/Linux - 🐳 Docker - 🖥️ Bash Scripting - 🐪 Perl - 🐬 MySQL - 👥 Formador de TI - 👥 Formador de SysAdmin's - 💢 Ansible - ☁️ Cloud Computing - ❤️ Debian GNU/Linux