
Escáner para el ataque a la cadena de suministro keyv/cacheable: detecta paquetes npm comprometidos, verifica los hashes de los payloads y encuentra implantes de persistencia en los modos repo y host.
English | Português
Escáner independiente para el incidente de la cadena de suministro de keyv/cacheable ("Shai-Hulud: Here We Go Again", 4 ago 2026) — 440+ paquetes npm comprometidos por un gusano autopropagante que roba credenciales de nube/CI e instala persistencia con un interruptor de hombre muerto.
Detecta, en minutos y sin instalar nada:
package-lock.json, npm-shrinkwrap.json, yarn.lock (v1 y Berry), pnpm-lock.yaml y bun.lock — incluidas las dependencias transitivas, con la cadena completa (p. ej. eslint → file-entry-cache → flat-cache → [email protected]);node_modules (nombre + hash SHA-256 de los artefactos conocidos);SUSPECT, nunca confirmadas sin un hash;.claude/settings.json y .vscode/tasks.json, artefactos temporales (bun-dl-*);keyv/cacheable fue comprometida; el atacante publicó nuevas versiones con un hook "preinstall": "node setup.mjs" — código que se ejecuta antes de que el paquete se instale, con los privilegios de quien haya ejecutado npm install.setup.mjs descarga el runtime de Bun desde GitHub y ejecuta el payload dentro de él — evasión contra herramientas que solo monitorizan procesos node.Math_Symbol.js (~728 KB, ofuscado) roba credenciales: metadatos de instancia de AWS, claves de AWS/GCP/Azure, tokens de Vault, cuentas de servicio de Kubernetes, secretos de GitHub Actions, tokens de npm, además de un barrido regex genérico en busca de claves privadas y bearer tokens en el disco.npm install/npm ci con los scripts de ciclo de vida habilitados desde 2026-08-04 09:35 UTC. Con --ignore-scripts, el hook no se ejecutó..claude/settings.json (SessionStart) y .vscode/tasks.json (folderOpen) que ejecutan el cargador cuando se abre la carpeta clonada — sin npm install, sin instalar nada. Esto incluye a quienes clonaron el repositorio para investigar el incidente y a agentes de codificación de IA que abrieron el directorio — uno de los primeros casos públicos de hooks de agentes de IA (.claude/) usados como vector de cadena de suministro.El implante instala un "watcher" (gh-token-monitor) mantenido vivo por un LaunchAgent (macOS) o un servicio de usuario de systemd + loginctl enable-linger (Linux). Cada 60 segundos valida el token de GitHub robado contra la API. Mientras el token funcione, no pasa nada. Cuando la respuesta se vuelve 4xx — es decir, el momento en que revocas el token — ejecuta, vía eval, el contenido de ~/.config/gh-token-monitor/handler: un comando arbitrario definido remotamente por el atacante. El análisis público no sabe qué contiene — podría ser destrucción de datos, reimplantación, ransomware o nada. El riesgo no es evaluable; por eso el orden de respuesta es absoluto.
Tres propiedades que cambian la respuesta:
host.[email protected] se publicó con una atestación SLSA en regla. La procedencia da fe de la integridad de la compilación, no de la fuente: el workflow legítimo compiló código ya troyanizado.package.json (el hook preinstall) y en dos archivos nuevos añadidos al paquete (setup.mjs, Math_Symbol.js).eslint → file-entry-cache → flat-cache → keyv. Por eso el escáner muestra la cadena en cada hallazgo.scan.mjs tiene las siguientes propiedades — importantes para cualquiera que responda a un incidente de cadena de suministro:
npm install. Audita todo scan.mjs en 15 minutos antes de ejecutarlo.--update (descargar un manifiesto de IOCs nuevo), explícita y opcional.docker run --network=none o una máquina aislada: solo copia scan.mjs + iocs.json.Requisito: Node.js ≥ 18 (cualquier máquina con npm ya lo tiene). Descarga los dos archivos — scan.mjs + iocs.json — y listo: sin instalación.
Aviso: si clonaste este repositorio completo, la carpeta
fixtures/contiene IOCs inertes usados en las pruebas (nombres y versiones reales, contenido ficticio — sin malware). El escáner la omite automáticamente y lo advierte en la salida; los hallazgos de allí solo aparecen si la escaneas a propósito.
Hay dos modos de ejecución que responden a preguntas distintas, y eso es lo que decide dónde ejecutarlo:
repo lee lockfiles y node_modules — y los lockfiles viven en git, por lo que se puede centralizar: una persona escanea todos los repositorios de la empresa.host busca el implante (watcher, LaunchAgent/systemd, hooks de IDE), que vive en la máquina donde se ejecutó el código — eso no está en git y no se puede centralizar.node scan.mjs repo /folder/with/all/the/repos --json=result.json --html=report.html
Responde "qué proyectos están expuestos" en minutos, sin involucrar a nadie. Acepta múltiples rutas; recorre subdirectorios (incluidos monorepos y workspaces).
Para cada proyecto con un hallazgo, identifica quién lo tocó desde 2026-08-04 09:35 UTC (git log, registros de CI). Esas personas ejecutan, en su máquina:
node scan.mjs # current directory + host, in ~30 seconds
Dentro del alcance: cualquiera que (a) haya ejecutado npm install/npm ci en la ventana; o (b) simplemente haya clonado y abierto la carpeta en VS Code o en un agente de IA — el Vector B no necesita instalación.
Como el coste es de ~30 segundos y el embudo puede tener fugas (un clon suelto, un proyecto personal), el mensaje interno más seguro es: cada desarrollador ejecuta node scan.mjs una vez y envía el --json/--html a AppSec. El envío es manual por diseño — el escáner no tiene telemetría (cero egreso).
Máxima prioridad: aquí es donde viven las credenciales más valiosas. Aquí el escáner tiene dos roles distintos — uno para el pasado, otro para el futuro:
Triaje del pasado — NO escanees el runner para decidir. La pregunta "¿este runner fue afectado?" no la responde un escaneo: si algún job instaló una versión afectada sin --ignore-scripts desde el 08-04, las credenciales ya fueron robadas en ese momento, y el host del runner rara vez conserva evidencia (los runners efímeros destruyen el contenedor al final del job; el watcher se autolimpia en ~24 h). Lo que la responde son los lockfiles del Paso 1 y los registros de CI. Si la respuesta es "sí, lo instaló": reconstruye el runner y rota sus secretos — los runners son efímeros, no hay razón para limpiarlos.
Prevención hacia adelante — SÍ, ejecútalo en el pipeline. Añade el escáner como paso de compilación, en modo repo, después del checkout y antes de npm install. No examina el host del runner — examina el código que está a punto de instalarse, y el código de salida hace fallar la compilación antes de que el preinstall malicioso tenga oportunidad de ejecutarse:
# example (GitHub Actions / GitLab CI — adapt):
- run: node scan.mjs repo . --json # exit 0 clean · 1 findings · 2 COMPROMISED
- run: npm ci --ignore-scripts # only runs if the previous step passed
node scan.mjs # scan the current directory + the host
node scan.mjs repo /path/a /path/b
node scan.mjs host # persistence/implants on the machine only
node scan.mjs repo . --json=result.json --html=report.html
node scan.mjs --update # update iocs.json (the only network operation)
| Nivel | Significado | Acción |
|---|---|---|
COMPROMISED | Versión maliciosa instalada en node_modules, payload confirmado por hash, o implante de persistencia encontrado | Trata el host como comprometido; sigue el orden de respuesta — limpia el implante antes de rotar credenciales |
EXPOSED | Versión maliciosa fijada en un lockfile, sin evidencia de ejecución | Fija una versión segura, elimina node_modules, reinstala con --ignore-scripts |
AT_RISK | Rango (^/~) en package.json que admite una versión maliciosa | Fija una versión exacta o bloquéala en el proxy del registro |
SUSPECT | Heurística (nombre de archivo del gusano con hash no coincidente, script de ciclo de vida sospechoso) | Inspecciona manualmente — podría ser una variante nueva o un falso positivo |
INFO | Vector presente pero sin IOC (p. ej., una tarea folderOpen genérica) | Revisar |
--html produce un informe autocontenido con marca de tiempo, nombre de host, versión del manifiesto de IOCs y el SHA-256 del propio escáner — utilizable como adjunto de una notificación de incidente y como pista de auditoría.
No revoques ni rotees ninguna credencial todavía — ese es el gatillo de la trampa. La secuencia:
1. AISLAR — desconecta la máquina de la red. Es seguro: sin respuesta HTTP no hay 4xx, la trampa no se dispara y la exfiltración se detiene. No apagues (la memoria volátil es evidencia).
2. PRESERVAR — antes de eliminar nada (el watcher se autodestruye en ~24 h):
mkdir -p /tmp/evidence && cp -r ~/.config/gh-token-monitor /tmp/evidence/ 2>/dev/null
cp /tmp/gh-token-monitor.*.log /tmp/evidence/ 2>/dev/null
shasum -a 256 /tmp/evidence/* 2>/dev/null
El archivo handler es el comando del atacante que se ejecutaría — no lo ejecutes, no lo pegues en una shell; trátalo como texto inerte. El archivo started_at acota la ventana de exposición (el auditor y el regulador te lo pedirán).
3. ERRADICAR — mata primero el proceso del watcher y luego:
# macOS
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
rm -f ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
# Linux
systemctl --user disable --now gh-token-monitor.service
loginctl disable-linger "$USER"
rm -f ~/.config/systemd/user/gh-token-monitor.service
# both
rm -rf ~/.config/gh-token-monitor ~/.local/bin/gh-token-monitor.sh /tmp/bun-dl-*
Elimina también los hooks maliciosos de .claude/settings.json y .vscode/tasks.json, los archivos setup.mjs/Math_Symbol.js/math_init.js, y limpia las cachés (~/.npm/_cacache, pnpm, yarn). Vuelve a ejecutar node scan.mjs host hasta que salga limpio.
4. ROTAR — solo con cada host afectado limpio y verificado (un solo watcher activo basta para disparar el gatillo). Revoca primero el token de npm (detiene la propagación del gusano); luego GitHub (PAT, claves de deploy), AWS/GCP/Azure, Vault, Kubernetes, secretos de CI — y cualquier secreto que estuviera en el disco, porque hubo un barrido regex.
5. AUDITAR — el gusano actúa en tu nombre: busca repositorios descritos como Shai-Hulud: Here We Go Again en tus organizaciones, versiones npm publicadas inesperadamente desde el 08-04 (desapruébalas y notifica a los consumidores), y uso de las credenciales en CloudTrail/Registros de auditoría dentro de la ventana de started_at.
Después: elimina node_modules, reinstala desde un lockfile limpio con --ignore-scripts. Runners de CI y hosts con ejecución confirmada: reconstruye desde cero, siempre — se ejecutó código arbitrario, y la lista de artefactos conocidos no garantiza completitud.
Instituciones reguladas (BR): un compromiso confirmado con acceso a credenciales puede activar obligaciones de notificación (Res. CMN 4.893/2021, Res. BCB 85/2021; art. 48 de la LGPD si hay datos personales involucrados). Documenta la cronología en UTC —
started_at, detección, contención, erradicación, rotación — y confirma los plazos con Legal/Cumplimiento.
El incidente está activo y la lista crece. Para obtener el manifiesto más reciente:
node scan.mjs --update # the only operation that touches the network
--update obtiene iocs.json de este repositorio (securest8/npm-incident-response), nunca de un tercero — Securest8 es la puerta de curación. Recibes lo último que se haya publicado aquí.
La lista de paquetes proviene del feed público de Wiz; los hashes, los dominios C2, los IOCs de persistencia y las versiones seguras son estáticos y están curados en tools/gen-iocs.mjs. Una instantánea del CSV de Wiz vive en tools/keyv-packages.csv para reproducibilidad y ejecuciones sin conexión.
node tools/gen-iocs.mjs # fetch the latest Wiz CSV, regenerate iocs.json + refresh the snapshot
node tools/gen-iocs.mjs --offline # regenerate from the committed snapshot, no network
node tools/gen-iocs.mjs --allow-shrink # allow a package count lower than the snapshot (guarded by default)
El generador es idempotente: conserva el manifest_version existente y no reescribe el archivo cuando no cambió nada material, y se niega a escribir un manifiesto vacío o reducido (protegiendo contra un feed upstream truncado o alterado).
Automatización: .github/workflows/update-iocs.yml ejecuta el generador a diario (y bajo demanda) y hace commit a main solo cuando los IOCs cambian realmente — así el --update de los usuarios sigue el feed de Wiz en aproximadamente un día, con todo el historial auditable en los commits. Cámbialo a un paso de pull request (anotado en el workflow) cuando el incidente se enfríe y prefieras una fusión manual.
node test/run-tests.mjs # 15 assertions against fixtures/demo-repo
fixtures/demo-repo es un repositorio de prueba con IOCs inertes (nombres y versiones reales, contenido ficticio — sin malware). Al escanear el propio repositorio del escáner, la carpeta fixtures/ se omite automáticamente (con una advertencia en la salida); las pruebas la escanean pasando la ruta explícitamente.
Una herramienta para un solo incidente, construida para un triage rápido durante la ventana activa del ataque — no un reemplazo de Socket, Snyk o similares. Investigación e IOCs: Socket.dev, Wiz Research (CSV público), Kodem Security.
Mantenido por Securest8. Licencia MIT.