GitLeaks en modo Web
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:

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 GitleaksVeamos 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-webNos 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:
Si lo comprobamos con nuestra herramienta Web:

Ahora comprobemos otro repositorio distinto creado tambien para detección de fugas:
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.
