GitLeaks en modo Web

CiberSeguridad 21 de jun. de 2026

Vamos a ir completando nuestra "Suite Web" de ciberseguridad de uso interno.

Ahora le toca el turno a gitleaks una herramienta para auditar posibles fugas de contraseñas, secretos, tokens, etc en los respositorios de código.

¿Qué es GitLeaks?

Gitleaks es una herramienta de seguridad de código abierto diseñada paraescanear repositorios de código y detectar información sensible filtrada por error, como claves API, contraseñas, tokens de autenticación y claves privadas.

¿Qué hace exactamente?

  • Análisis de historial: Escanea todo el historial de confirmaciones (commits) de Git para encontrar secretos que hayan sido expuestos en el pasado y sigan en el repositorio.
  • Detección inteligente: Utiliza un motor basado en expresiones regulares (regex) combinado con cálculos de entropía para identificar cadenas de caracteres que parezcan contraseñas o tokens.
  • Prevención activa: Se puede integrar en los flujos de trabajo (pipelines) de integración continua (CI/CD) para bloquear una subida de código si se detecta un secreto antes de que llegue a producción.

Podemos usarlo como un comando mediate la CLI (Command Line Inteface), pero también lo podemos ofrecer como un servicio web dentro de nuestra organización para facilitar el analisis de "fugas" a algunos responsables de equipos que quieran auditar sus repositorios.

El servicio web que vamos a crear, escaneara un repositorio ya existente y detecta si hay alguna "fuga actual de contraseñas, secretos, tokens, etc".

Esto permite actuar de forma inmediante y corregirlo, pero deberás forzar a que en el futuro no se suban nada que pueda comprometer la seguridad interna, para ello te recomiendo el artículo:

GIT con un poco de seguridad
Tanto si trabajas tu solo en tus propios proyectos personsales, como si trabajas en grupo hay que tener en cuenta siempre algunas medidas mínimas de seguridad cuanto trabajamos con repositorios GIT. Aparte de lo seguro que sea tu código, una de las primeras cosas que debes de configurar en cualquier

Gitleaks como servicio web

Vamos a seguir la misma filosofía que las otras herramientas web que hemos creado para usarse con Docker.
Necesitaremos crear un directorio en el que vamos a almacenar todos tres ficheros que necesitamos para crear nuestro proyecto:

├── server.js          # Backend en Node.js (Clona + ejecuta Gitleaks + limpia)
├── index.html         # Interfaz web con campos de autenticación y reporte
└── Dockerfile         # Contenedor con Node.js, Git y el binario de Gitleaks

Veamos el contenido de cada uno de los ficheros:

server.js

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

const app = express();
const PORT = 5500; // Usamos el puerto 6000 para nuestra suite

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. Construir la URL de clonado con o sin credenciales de forma segura
    let cloneUrl = repoUrl;
    if (token) {
        try {
            const urlObj = new URL(repoUrl);
            if (username) {
                urlObj.username = username;
                urlObj.password = token;
            } else {
                urlObj.username = token; // Muchos servicios como GitHub aceptan el token directamente como usuario
            }
            cloneUrl = urlObj.toString();
        } catch (e) {
            return res.status(400).json({ error: 'Formato de URL de repositorio inválido.' });
        }
    }

    // 2. Crear un directorio temporal único para el clonado
    const repoId = crypto.randomBytes(16).toString('hex');
    const targetDir = path.join('/tmp', `repo-${repoId}`);

    // Función auxiliar para limpiar el directorio pase lo que pase
    const cleanUp = () => {
        if (fs.existsSync(targetDir)) {
            fs.rmSync(targetDir, { recursive: true, force: true });
        }
    };

    // 3. Clonar el repositorio (usamos depth 1 si solo queremos el estado actual, u omitimos para escanear todo el historial)
    // Dejamos que escanee todo el historial por defecto ya que los secretos suelen estar en commits antiguos
    const gitClone = spawn('git', ['clone', cloneUrl, targetDir]);

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

        // 4. Ejecutar Gitleaks en el directorio clonado generando un reporte JSON
        const reportPath = path.join('/tmp', `report-${repoId}.json`);
        const gitleaks = spawn('gitleaks', ['detect', '--source', targetDir, '--report-path', reportPath, '--no-git=false']);

        gitleaks.on('close', (gitleaksCode) => {
            let leaks = [];
            
            // Si el código es 0 significa que no hay fugas. Si es 1, significa que encontró fugas (leaks).
            if (fs.existsSync(reportPath)) {
                try {
                    leaks = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
                    fs.unlinkSync(reportPath); // Borrar reporte temporal
                } catch (e) {
                    console.error("Error leyendo reporte de Gitleaks", e);
                }
            }

            // 5. Limpieza absoluta del código clonado inmediatamente
            cleanUp();

            res.json({ success: true, leaks });
        });
    });
});

app.listen(PORT, () => {
    console.log(`Gitleaks 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>Gitleaks Secret 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-amber-400 flex items-center gap-3">
                <span class="text-4xl sm:text-5xl">🔑</span> Gitleaks 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">
                Secret Detector 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 (GitHub, GitLab, Bitbucket...):</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-amber-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-amber-500 focus:ring-0">
                    ¿Es un repositorio privado? Requiere autenticación
                </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-amber-500">
                    </div>
                    <div class="space-y-1">
                        <label class="block text-xs font-medium text-slate-400">Token de Acceso Personal / Password:</label>
                        <input type="password" id="token" placeholder="ghp_xxxx o contraseña" 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-amber-500">
                    </div>
                </div>
            </div>

            <button id="btn-scan" class="w-full bg-amber-600 hover:bg-amber-500 text-white font-medium py-2.5 px-6 rounded-lg transition-all duration-200 cursor-pointer shadow-lg shadow-amber-600/20 active:scale-[0.99]">
                Escanear Repositorio
            </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">Secretos y Fugas Detectadas:</h2>
                <button id="btn-download" class="hidden bg-slate-800 hover:bg-slate-700 text-amber-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 Informe (.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 arriba y haz clic en "Escanear Repositorio".
            </div>
        </div>
    </main>

    <footer class="text-center py-4 text-xs text-slate-600 border-t border-slate-800">
        SoloConLinux · Gitleaks 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 currentLeaks = [];

        // Mostrar / Ocultar campos de credenciales
        chkAuth.addEventListener('change', (e) => {
            authFields.classList.toggle('hidden', !e.target.checked);
        });

        // Lanzar análisis
        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, ingresa la URL del repositorio Git.');

            btnScan.disabled = true;
            btnScan.innerText = 'Escaneando historial Git...';
            btnDownload.classList.add('hidden');
            resultBox.innerHTML = '<span class="text-amber-400 animate-pulse">Clonando repositorio de manera segura y analizando commits...</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 análisis');

                currentLeaks = data.leaks || [];

                if (currentLeaks.length === 0) {
                    resultBox.innerHTML = '<div class="text-emerald-400 font-medium">✨ ¡Excelente! No se encontraron secretos expuestos ni credenciales en el historial de este repositorio.</div>';
                } else {
                    btnDownload.classList.remove('hidden');
                    resultBox.innerHTML = `<div class="text-red-400 font-bold mb-3 font-sans text-xs">⚠️ ¡PELIGRO! Se detectaron ${currentLeaks.length} posibles fugas de información confidencial:</div>`;
                    
                    currentLeaks.forEach(leak => {
                        const card = `
                            <div class="mb-4 p-4 rounded-lg border border-red-950 bg-red-950/20 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 bg-red-900 border border-red-700 text-red-200 mr-2">LEAK</span>
                                        <span class="text-slate-200 font-bold text-sm">${leak.RuleID || 'Secret Detected'}</span>
                                    </div>
                                    <span class="text-slate-500 font-sans text-[11px]">Confianza: Alta</span>
                                </div>
                                <div class="grid grid-cols-1 md:grid-cols-2 gap-2 text-slate-400 border-t border-slate-800/60 pt-2 font-mono">
                                    <div><b>Archivo:</b> <span class="text-slate-300 break-all">${leak.File || 'N/A'}</span></div>
                                    <div><b>Línea:</b> <span class="text-slate-300">${leak.StartLine || 'N/A'}</span></div>
                                    <div><b>Commit:</b> <span class="text-slate-500 break-all">${leak.Commit ? leak.Commit.substring(0, 8) : 'N/A'}</span></div>
                                    <div><b>Autor:</b> <span class="text-slate-400 font-sans">${leak.Author || 'Desconocido'}</span></div>
                                </div>
                                <div class="bg-slate-950 p-2 rounded border border-slate-800 text-red-400/90 font-mono mt-1 overflow-x-auto break-all">
                                    <b>Fragmento de coincidencia:</b> ${leak.Match || 'Oculto o no disponible'}
                                </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 Repositorio';
            }
        });

        // Generar y descargar el reporte amigable en Markdown (.md)
        btnDownload.addEventListener('click', () => {
            if (!currentLeaks || currentLeaks.length === 0) return;

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

            let mdContent = `# 🛡️ Informe de Auditoría de Seguridad - Gitleaks\n`;
            mdContent += `* **Repositorio:** ${repoUrl}\n`;
            mdContent += `* **Fecha del Análisis:** ${dateStr}\n`;
            mdContent += `* **Estado:** ⚠️ SE DETECTARON FUGAS DE INFORMACIÓN\n`;
            mdContent += `* **Total de Incidentes:** ${currentLeaks.length}\n\n`;
            mdContent += `---\n\n`;
            mdContent += `## Detalle de los hallazgos\n\n`;

            currentLeaks.forEach((leak, index) => {
                mdContent += `### Hallazgo #${index + 1}: Fuga del tipo [${leak.RuleID || 'Desconocido'}]\n`;
                mdContent += `* **Archivo:** \`${leak.File || 'N/A'}\` (Línea ${leak.StartLine || 'N/A'})\n`;
                mdContent += `* **Commit Hash:** \`${leak.Commit || 'N/A'}\`\n`;
                mdContent += `* **Autor del Commit:** ${leak.Author || 'Desconocido'}\n`;
                mdContent += `* **Fecha:** ${leak.Date || 'No disponible'}\n\n`;
                mdContent += `#### Fragmento Comprometido:\n`;
                mdContent += `\`\`\`text\n${leak.Match || 'No disponible'}\n\`\`\`\n`;
                mdContent += `\n*Nota de remediación: Se aconseja invalidar/rotar este secreto inmediatamente.*\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', '') || 'repositorio';
            
            const a = document.createElement('a');
            a.href = url;
            a.download = `reporte-fugas-${safeRepoName}.md`;
            document.body.appendChild(a);
            a.click();
            
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        });
    </script>
</body>
</html>

Versión 2.1 con botón para generar informe en formato .md (Markdown)

Dockerfile

FROM node:20-alpine

# Instalar Git, Curl y dependencias del sistema operativo
RUN apk add --no-cache git curl openssh-client

# Descargar e instalar el binario oficial de Gitleaks (versión linux alpine64/amd64)
# Automatizamos la descarga directa del binario estático
RUN curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.18.2/gitleaks_8.18.2_linux_x64.tar.gz | tar -xz -C /usr/local/bin gitleaks

WORKDIR /app

# Copiar archivos e instalar dependencias del backend
COPY server.js index.html ./
RUN npm init -y && npm install express

EXPOSE 5500

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

Crear la imagen de gitleaks-web

Como siempre es realmente sencillo, desde el directorio en que tenemos nuestros ficheros del proyecto ejecutamos:

docker build -t gitleaks-web .

Uso y Obtención del Informe

Para usar la imagen y lanzar el contenedor con el servicio web simplemente ejecutaremos:

docker run -d -p 5500:5500 --name gitleaks-web gitleaks-web

Nos conectaremos al puerto 5500 para poder acceder al servicio web: http://localhost:5500

Si es un repositorio público podemos descargar y analizarlo sin ningun problema, pero si es uno de tipo privado y tenemos las credenciales ó un token de acceso, podemos marcar y usar autenticación para chequear el repositorio.

Repositorios de Pruebas de Fugas

Existen varios repositorios que se utilizan para validar las herramientas de detección de fugas.

Vamos a usar alguno de ellos para comprobar el correcto funcionamiento de nuestra herramienta Web:

GitHub - leaktk/fake-leaks: Things that would cause a git leaks scan to freak out
Things that would cause a git leaks scan to freak out - leaktk/fake-leaks

Si lo comprobamos con nuestra herramienta Web:

Ahora comprobemos otro repositorio distinto creado tambien para detección de fugas:

GitHub - Plazmaz/leaky-repo: Benchmarking repo for secrets scanning
Benchmarking repo for secrets scanning. Contribute to Plazmaz/leaky-repo development by creating an account on GitHub.

Nuestro servicio Web funciona correctamente detectando fugas:

Realizamos pruebas con repositorios privados (con usuario/clave y con usuario/Token) y revisamos si hay fugas en ellos:

El código de index.html se ha mejorado para incluir un botón que permite descargar el informe completo en formato Markdown que permite su lectura con cualquier editor de texto y su posible transformación a otros formatos (PDF, doc, odt, etc)

Descarga del informe en formato Markdown

Ejemplo de como se ve el informe del resultado del fichero .md generado, usando un visor de Markdown:

Perfecto disponemos de otra herramienta web más que simplifica las tareas del grupo de trabajo de ciberseguridad o las del resposable de un grupo de desarrollo detectando fugas.

Etiquetas

Luis GuLo

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