Construir un agente de seguridad con LangChain

Prerrequisitos
Antes de arrancar, asegurate de tener instalado lo siguiente:
- Node.js (v14 o superior) y npm para correr la aplicación JavaScript
- Docker para correr el contenedor de Redis
- Git para control de versiones
- Una API key de DeepSeek (conseguila en DeepSeek)
- Conocimiento básico de JavaScript, Express.js y REST APIs
- Familiaridad con conceptos de middleware y autenticación/autorización
Arranquemos este viaje
Una empresa de software importante viene teniendo problemas de disponibilidad últimamente. Tienen una API que usan mucho sus socios externos. Esta API tiene un único endpoint que procesa datos importantes y se los devuelve a los clientes. La API incluye un middleware que autentica y autoriza al cliente; este proceso consume una cierta cantidad de ciclos de cómputo porque la whitelist está guardada en un archivo de texto plano. Como consecuencia, cada vez que un cliente intenta consumir la API, el middleware tiene que leer el archivo y verificar si la IP del cliente está en la whitelist. El problema que enfrenta la empresa es que actores maliciosos vienen intentando vulnerar este middleware con ataques de fuerza bruta, degradando el rendimiento de la API. La empresa nos dio un link donde está el código de la API, para que nos familiaricemos con la aplicación. La estructura de carpetas es la siguiente:
/src
- evil.sh #a script that simulates the brute-force attack
- index.js #the main file of the API
- package-lock.json #npm lock file
- package.json #npm package file
- whitelist.json #a file that contains the list of whitelisted IPsSeguimos instalando las dependencias de npm:
npm installDespués arrancamos la aplicación:
npm startLa aplicación genera la siguiente salida:
> api@1.0.0 start
> node index.js
API listening at http://localhost:3000Estamos listos para ejecutar el script de ataque de fuerza bruta:
chmod +x ./evil.sh && ./evil.shDespués de ejecutar el script de prueba durante un minuto, vemos que la aplicación generó un archivo de log llamado ./login.log, y el contenido se ve así:
2026-02-14T22:02:59.295Z - Login attempt from 192.256.1.4 - SUCCESS
2026-02-14T22:02:59.832Z - Login attempt from 192.256.1.6 - FAILED
2026-02-14T22:03:00.354Z - Login attempt from 192.256.1.3 - SUCCESS
2026-02-14T22:03:00.886Z - Login attempt from 192.256.1.9 - FAILED
2026-02-14T22:03:01.418Z - Login attempt from 192.256.1.9 - FAILED
2026-02-14T22:03:01.947Z - Login attempt from 192.256.1.7 - FAILED
2026-02-14T22:03:02.479Z - Login attempt from 192.256.1.8 - FAILED
2026-02-14T22:03:03.010Z - Login attempt from 192.256.1.9 - FAILED
2026-02-14T22:03:03.539Z - Login attempt from 192.256.1.1 - SUCCESS
2026-02-14T22:03:04.070Z - Login attempt from 192.256.1.10 - FAILEDAhora que entendemos bien la API, la empresa nos dio la responsabilidad de implementar un middleware y un agente de seguridad. El middleware se va a encargar de bloquear ataques de fuerza bruta leyendo una blocklist guardada en una instancia de Redis. La base de datos de Redis va a ser poblada por un agente de seguridad que lee el archivo ./login.log e identifica clientes IP maliciosos cada diez minutos. La empresa no nos dio acceso a la base de datos de Redis porque sus políticas de seguridad son demasiado restrictivas, así que usamos Docker para el desarrollo:
docker run -d --name my-redis -p 6379:6379 redis:latestVerificamos que el contenedor de Docker de Redis esté corriendo:
docker ps -aLa salida se ve así:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
16b1ed8ccd32 redis "docker-entrypoint.s…" 3 minutes ago Up 3 minutes 0.0.0.0:6379->6379/tcp, [::]:6379->6379/tcp my-redis¡Bien! El contenedor está corriendo. Instalemos el paquete npm redis en la API:
npm install redisAhora que tenemos el paquete redis, creamos el archivo ./redis.js, que contiene funciones utilitarias para leer y escribir en la base de datos:
const { createClient } = require("redis");
const client = createClient();
client.on("error", (err) => console.error("Redis Client Error", err));
let isConnected = false;
const connectRedis = async () => {
if (!isConnected) {
await client.connect();
isConnected = true;
}
};
const verifyIfKeyExists = async (key) => {
try {
await connectRedis();
const value = await client.get(key);
return value !== null;
} catch (err) {
console.error("Error verifying key:", err);
throw err;
}
};
const setKeyValue = async (key, value) => {
try {
await connectRedis();
await client.set(key, value);
} catch (err) {
console.error("Error setting key:", err);
throw err;
}
};
module.exports = {
verifyIfKeyExists,
setKeyValue,
connectRedis,
};Ya tenemos una forma de comunicarnos con la base de datos. El siguiente paso es instalar los paquetes para construir nuestro agente de seguridad:
npm install @langchain/core @langchain/deepseek dotenv zodEl proveedor de LLM de la empresa es DeepSeek, y nos dieron una API key para usar. Creamos un archivo ./.env y guardamos la variable de entorno:
DEEPSEEK_API_KEY=your_deepseek_api_key_here¡Es hora de crear nuestro agente! Creemos el archivo agent.js y agreguemos su código:
require("dotenv").config();
const { ChatDeepSeek } = require("@langchain/deepseek");
const { tool } = require("@langchain/core/tools");
const { z } = require("zod");
const { verifyIfKeyExists, setKeyValue } = require("./redis");
const readLogFileTool = tool(
async () => {
const fs = require("fs").promises;
try {
const data = await fs.readFile("./login.log", "utf-8");
return data;
} catch (err) {
console.error("Error reading log file:", err);
throw err;
}
},
{
name: "read_log_file",
description:
"Reads the log file and returns its content as a string. This should be called first to analyze threats.",
},
);
const checkIpTool = tool(
async ({ ip }) => {
const key = `ip:${ip}`;
const exists = await verifyIfKeyExists(key);
return exists ? "IP is blocked" : "IP is not blocked";
},
{
name: "check_ip",
description: "Checks if the given IP address exists in the blocklist.",
schema: z.object({
ip: z.string().describe("The IP address to check."),
}),
},
);
const blockIpTool = tool(
async ({ ip }) => {
const key = `ip:${ip}`;
await setKeyValue(key, "blocked");
return `IP ${ip} has been blocked`;
},
{
name: "block_ip",
description: "Blocks the given IP address by adding it to the blocklist.",
schema: z.object({
ip: z.string().describe("The IP address to block."),
}),
},
);
const tools = [readLogFileTool, checkIpTool, blockIpTool];
const llm = new ChatDeepSeek({
apiKey: process.env.DEEPSEEK_API_KEY,
model: "deepseek-chat",
});
const llmWithTools = llm.bindTools(tools);
const SYSTEM_PROMPT = `You are a security agent designed to monitor and protect an API from brute-force attacks.
Your responsibilities are:
1. Read the log file to analyze potential threats
2. Identify IP addresses that have exceeded the request threshold (more than 5 failed attempts)
3. Check if an IP address is already blocked
4. Block malicious IP addresses
Always start by reading the log file, then analyze it for suspicious activity, and block any threatening IPs.`;
async function runAgent(
input = "Analyze the security logs and block any malicious IPs",
) {
const messages = [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: input },
];
while (true) {
const response = await llmWithTools.invoke(messages);
messages.push(response);
if (!response.tool_calls || response.tool_calls.length === 0) {
return response.content;
}
for (const toolCall of response.tool_calls) {
let toolResult;
try {
const toolFunction = tools.find((t) => t.name === toolCall.name);
if (!toolFunction) {
throw new Error(`Tool ${toolCall.name} not found`);
}
toolResult = await toolFunction.invoke(toolCall.args);
} catch (err) {
toolResult = `Error executing tool: ${err.message}`;
}
messages.push({
tool_call_id: toolCall.id,
content: toolResult,
type: "tool",
name: toolCall.name,
});
}
}
}
module.exports = runAgent;Primero, cargamos nuestras variables de entorno e importamos todos los paquetes utilitarios que vamos a usar para construir nuestro agente. Después, definimos las herramientas del agente: readLogFile, checkIpTool y blockIpTool. Por último, configuramos el LLM, le asociamos (bind) las herramientas creadas, y agregamos un prompt conciso y directo—dejando que el LLM itere hasta que no queden más llamadas a herramientas—para crear el ejecutor del agente. Nuestro agente se ve así, tal como se ilustra en el diagrama de arquitectura de arriba.
Es hora de modificar el punto de entrada de la API. Miremos el archivo ./index.js:
const express = require("express");
const fs = require("fs");
const app = express();
const port = 3000;
const log = (message, type) => {
if (type === "error") {
console.error(message);
} else {
console.log(message);
}
fs.appendFileSync("login.log", `${new Date().toISOString()} - ${message}\n`);
};
const login = (req, res, next) => {
const whitelist = JSON.parse(fs.readFileSync("whitelist.json", "utf-8"));
const clientIp = req.headers["x-forwarded-for"];
if (whitelist.includes(clientIp)) {
log(`Login attempt from ${clientIp} - SUCCESS`, "info");
next();
} else {
log(`Login attempt from ${clientIp} - FAILED`, "error");
res.status(403).send("Forbidden");
}
};
app.use(express.json());
app.post("/task", login, (req, res) => {
if (typeof req.body.task === "string") {
req.body.task = req.body.task.replace(/foo/g, "bar");
}
res.json({ task: req.body.task });
});
app.listen(port, () => {
console.log(`API listening at http://localhost:${port}`);
});Ya existe un middleware que autentica y autoriza al cliente. Necesitamos agregar un segundo, y tiene que ejecutarse primero. Agreguemos el siguiente código:
const express = require("express");
const fs = require("fs");
const { verifyIfKeyExists } = require("./redis");
...
app.use(express.json());
const guardian = async (req, res, next) => {
const clientIp = req.headers["x-forwarded-for"];
const key = `ip:${clientIp}`;
const isBlocked = await verifyIfKeyExists(key);
if (isBlocked) {
log(`Blocked login attempt from ${clientIp}`, "error");
return res.status(403).send("Forbidden");
}
next();
};
...
app.post("/task", guardian, login, (req, res) => {
if (typeof req.body.task === "string") {
req.body.task = req.body.task.replace(/foo/g, "bar");
}
res.json({ task: req.body.task });
});
...Agregamos el middleware guardian. Como verifica la IP directamente desde la memoria primaria, puede manejar el proceso de rechazo más rápido. Ahora necesitamos configurar que nuestro agente de seguridad se ejecute cada diez minutos. Agreguemos el siguiente código:
const express = require("express");
const fs = require("fs");
const { verifyIfKeyExists } = require("./redis");
const runAgent = require("./agent");
...
const runSecurityAgent = async () => {
try {
log("Running security agent...", "info");
const result = await runAgent(
"Analyze security logs and block malicious IPs attempting to access the API",
);
log(`Security agent completed: ${result}`, "info");
} catch (err) {
log(`Security agent error: ${err.message}`, "error");
}
};
runSecurityAgent();
setInterval(runSecurityAgent, 600000);
app.listen(port, () => {
console.log(`API listening at http://localhost:${port}`);
});Corramos la API de nuevo:
npm startDespués el script ./evil.sh:
./evil.shVas a ver las siguientes salidas en la terminal o en el archivo ./login.log:
Running security agent...
Login attempt from 192.256.1.3 - SUCCESS
Blocked login attempt from 192.256.1.6
Blocked login attempt from 192.256.1.7
Login attempt from 192.256.1.4 - SUCCESS
Login attempt from 192.256.1.9 - FAILED
### **Security Actions Taken:**
✅ All 5 malicious IP addresses have been successfully blocked and added to the blocklist
✅ Each IP was checked before blocking to ensure it wasn't already blocked
✅ The blocking prevents further brute-force attempts from these sources
The API is now protected against these specific brute-force attack sources. Regular monitoring should continue to detect any new threats.
Blocked login attempt from 192.256.1.7
Blocked login attempt from 192.256.1.8
Login attempt from 192.256.1.3 - SUCCESS
Blocked login attempt from 192.256.1.6
Login attempt from 192.256.1.2 - SUCCESS
Blocked login attempt from 192.256.1.9Después de que el agente de seguridad termina su primera ejecución, logra identificar las IPs maliciosas y bloquearlas; ya no se ven más intentos de login fallidos en los logs. Desplegamos la nueva funcionalidad, y la empresa quedó contenta con nuestro trabajo.
Reflexión final
¡Felicitaciones por construir tu primer agente de seguridad! Combinaste con éxito un bloqueador rápido en memoria con un analizador inteligente basado en LLM para proteger tu API de ataques de fuerza bruta.
Este proyecto demuestra cómo los agentes de IA pueden ser herramientas prácticas para desafíos de seguridad del mundo real. El diseño modular hace que sea fácil extenderlo con herramientas adicionales y refinar tu lógica de detección de amenazas a medida que tus necesidades evolucionan.
Tené en cuenta que esto es con fines educativos, y hay que hacer ajustes importantes antes de llevarlo a producción, como agregar rate limiting, logging integral, e integración con sistemas de monitoreo de seguridad.
Podés encontrar el código final de nuestra aplicación acá.
¡Feliz codeo! 🚀 Si tenés feedback o querés contribuir para mejorar este proyecto, sos bienvenido. Gracias por seguir este viaje del agente de seguridad hasta el final.