
Scanner para o ataque à cadeia de suprimentos keyv/cacheable: detecta pacotes npm comprometidos, verifica hashes de payload e encontra implantes de persistência nos modos repo e host.
English | Português
Scanner standalone para o incidente de supply chain keyv/cacheable ("Shai-Hulud: Here We Go Again", 04/ago/2026) — 440+ pacotes npm comprometidos por um worm auto-propagante que rouba credenciais de cloud/CI e planta persistência com dead-man's switch.
O output do scanner é em inglês (findings, ações, relatório HTML). Esta página traduz o contexto e o passo a passo para o público brasileiro; os comandos são idênticos aos da versão em inglês.
Detecta, em minutos e sem instalar nada:
package-lock.json, npm-shrinkwrap.json, yarn.lock (v1 e Berry), pnpm-lock.yaml e bun.lock — incluindo dependências transitivas, com a cadeia completa (ex.: eslint → file-entry-cache → flat-cache → [email protected]);node_modules (nome + hash SHA-256 dos artefatos conhecidos);SUSPECT, nunca confirmadas sem hash;.claude/settings.json e .vscode/tasks.json, artefatos temporários (bun-dl-*);keyv/cacheable foi comprometida; o atacante publicou versões novas com um gancho "preinstall": "node setup.mjs" — código que roda antes de o pacote ser instalado, com os privilégios de quem rodou npm install.setup.mjs baixa o runtime Bun do GitHub e executa o payload nele — evasão contra ferramentas que só monitoram processos node.Math_Symbol.js (~728 KB, ofuscado) rouba credenciais: metadados de instância AWS, chaves AWS/GCP/Azure, tokens Vault, service accounts de Kubernetes, secrets de GitHub Actions, tokens npm, e faz varredura genérica por regex atrás de chaves privadas e bearer tokens em disco.npm install/npm ci com lifecycle scripts habilitados desde 04/08/2026 09:35 UTC. Com --ignore-scripts, o gancho não executou..claude/settings.json (SessionStart) e .vscode/tasks.json (folderOpen) que executam o loader ao abrir a pasta clonada — sem npm install, sem instalar nada. Isso inclui quem clonou o repositório para investigar o incidente e agentes de codificação com IA que abriram o diretório — um dos primeiros casos públicos de hooks de agente de IA (.claude/) usados como vetor de supply chain.O implante instala um "vigia" (gh-token-monitor) mantido vivo por LaunchAgent (macOS) ou serviço systemd de usuário + loginctl enable-linger (Linux). A cada 60 segundos ele valida o token GitHub roubado contra a API. Enquanto o token funciona, nada acontece. Quando a resposta vira 4xx — ou seja, no instante em que você revoga o token — ele executa via eval o conteúdo de ~/.config/gh-token-monitor/handler: um comando arbitrário definido remotamente pelo atacante. A análise pública não sabe o que ele contém — pode ser destruição de dados, reimplante, ransomware ou nada. O risco não é avaliável; por isso a ordem de resposta é absoluta.
Três propriedades que mudam a resposta:
host.[email protected] saiu com atestação SLSA passando. Provenance atesta integridade de build, não de fonte: o workflow legítimo compilou código já trojanizado.package.json (o gancho preinstall) e em dois arquivos novos adicionados ao pacote (setup.mjs, Math_Symbol.js).eslint → file-entry-cache → flat-cache → keyv. Por isso o scanner mostra a cadeia em cada achado.O scan.mjs tem as seguintes características — importantes para quem está respondendo a um incidente de supply chain:
npm install. Audite scan.mjs inteiro em 15 minutos antes de rodar.--update (baixar manifesto de IOCs novo), explícita e opcional.docker run --network=none ou máquina isolada: basta copiar scan.mjs + iocs.json.Requisito: Node.js ≥ 18 (qualquer máquina com npm já tem). Baixe os dois arquivos — scan.mjs + iocs.json — e pronto: não há instalação.
Heads-up: se você clonou este repositório inteiro, a pasta
fixtures/contém IOCs inertes usados nos testes (nomes e versões reais, conteúdo dummy — não há malware). O scanner a ignora automaticamente e avisa no output; achados vindos dela só aparecem se você a escanear de propósito.
Há dois modos de execução que respondem perguntas diferentes e é isso que define onde rodar:
repo lê lockfiles e node_modules — e lockfiles estão no git, então pode ser centralizado: uma pessoa varre todos os repositórios da empresa.host procura o implante (watcher, LaunchAgent/systemd, hooks de IDE), que mora na máquina onde o código executou — isso não está no git e não dá para centralizar.node scan.mjs repo /pasta/com/todos/os/repositorios --json=resultado.json --html=relatorio.html
Responde "quais projetos estão expostos" em minutos, sem envolver ninguém. Aceita vários caminhos; varre subdiretórios (monorepos e workspaces inclusos).
Para cada projeto com achado, identifique quem mexeu nele desde 04/08 09:35 UTC (git log, logs de CI). Essas pessoas rodam, na máquina delas:
node scan.mjs # diretório atual + host, em ~30 segundos
Está no escopo quem: (a) rodou npm install/npm ci na janela; ou (b) apenas clonou e abriu a pasta no VS Code ou num agente de IA — o vetor B dispensa instalação.
Como o custo é ~30 segundos e o funil pode vazar (clone avulso, projeto pessoal), o comunicado interno mais seguro é: todo desenvolvedor roda node scan.mjs uma vez e envia o --json/--html ao AppSec. O envio é manual por princípio — o scanner não tem telemetria (zero egress).
Prioridade máxima: é onde moram as credenciais mais valiosas. Aqui o scanner tem dois papéis diferentes — um para o passado, outro para o futuro:
Triagem do passado — NÃO escaneie o runner para decidir. A pergunta "este runner foi atingido?" não se responde com scan: se algum job instalou versão afetada sem --ignore-scripts desde 04/08, as credenciais já foram roubadas naquele momento, e o host do runner raramente guarda evidência (runners efêmeros destroem o container ao fim do job; o watcher se apaga em ~24h). Quem responde são os lockfiles do Passo 1 e os logs de CI. Se a resposta for "sim, instalou": recrie o runner e rotacione os secrets dele — runners são efêmeros, não há justificativa para limpá-los.
Prevenção daqui para frente — SIM, rode no pipeline. Adicione o scanner como etapa do build, em modo repo, depois do checkout e antes do npm install. Ele não examina o host do runner — examina o código que vai ser instalado, e o exit code quebra o build antes de o preinstall malicioso ter chance de rodar:
# exemplo (GitHub Actions / GitLab CI — adapte):
- run: node scan.mjs repo . --json # exit 0 limpo · 1 achados · 2 COMPROMISED
- run: npm ci --ignore-scripts # só executa se o passo anterior passou
node scan.mjs # varre o diretório atual + o host
node scan.mjs repo /caminho/a /caminho/b
node scan.mjs host # só persistência/implantes na máquina
node scan.mjs repo . --json=resultado.json --html=relatorio.html
node scan.mjs --update # atualiza iocs.json (única operação com rede)
--html gera um relatório autocontido com timestamp, hostname, versão do manifesto de IOCs e SHA-256 do próprio scanner — serve como anexo de notificação de incidente e trilha de auditoria.
Não revogue nem rotacione nenhuma credencial ainda — é o gatilho da armadilha. A sequência:
1. ISOLAR — corte a rede da máquina. É seguro: sem resposta HTTP não há 4xx, a armadilha não dispara, e a exfiltração para. Não desligue (memória volátil é evidência).
2. PRESERVAR — antes de apagar qualquer coisa (o vigia se autodestrói em ~24h):
mkdir -p /tmp/evidencia && cp -r ~/.config/gh-token-monitor /tmp/evidencia/ 2>/dev/null
cp /tmp/gh-token-monitor.*.log /tmp/evidencia/ 2>/dev/null
shasum -a 256 /tmp/evidencia/* 2>/dev/null
O arquivo handler é o comando do atacante que seria executado — não execute, não cole em um shell; trate como texto inerte. O started_at delimita a janela de exposição (o auditor e o regulador vão pedir).
3. ERRADICAR — mate o processo do watcher primeiro, depois:
# 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
# ambos
rm -rf ~/.config/gh-token-monitor ~/.local/bin/gh-token-monitor.sh /tmp/bun-dl-*
Remova também os hooks maliciosos de .claude/settings.json e .vscode/tasks.json, os arquivos setup.mjs/Math_Symbol.js/math_init.js e limpe os caches (~/.npm/_cacache, pnpm, yarn). Rode node scan.mjs host de novo até zerar.
4. ROTACIONAR — só com todos os hosts limpos e verificados (basta um vigia vivo para o gatilho disparar). Revogue o token npm primeiro (interrompe a propagação do worm); depois GitHub (PATs, deploy keys), AWS/GCP/Azure, Vault, Kubernetes, secrets de CI — e qualquer segredo que estava em disco, porque houve varredura por regex.
5. AUDITAR — o worm age em seu nome: procure repositórios com a descrição Shai-Hulud: Here We Go Again nas suas organizações, versões npm publicadas inesperadamente desde 04/08 (deprecie e avise os consumidores), e uso das credenciais no CloudTrail/Audit Logs dentro da janela do started_at.
Depois: apague node_modules, reinstale de lockfile íntegro com --ignore-scripts. Runners de CI e hosts com execução confirmada: reconstruir do zero, sempre — houve execução de código arbitrário e a lista de artefatos conhecidos não é garantia de completude.
Instituições reguladas (BR): comprometimento confirmado com acesso a credenciais pode acionar obrigações de comunicação (Res. CMN 4.893/2021, Res. BCB 85/2021; LGPD art. 48 se houver dados pessoais). Documente a linha do tempo em UTC —
started_at, detecção, contenção, erradicação, rotação — e confirme prazos com jurídico/compliance.
O incidente está ativo e a lista cresce. Para puxar o manifesto mais recente:
node scan.mjs --update # única operação com acesso à rede
O --update busca o iocs.json deste repositório (securest8/npm-incident-response), nunca de terceiros — a Securest8 é o portão de curadoria. Você recebe o que foi publicado aqui por último.
A lista de pacotes vem do feed público da Wiz; os hashes, domínios C2, IOCs de persistência e versões seguras são estáticos e curados em tools/gen-iocs.mjs. Um snapshot do CSV da Wiz fica em tools/keyv-packages.csv para reprodutibilidade e execução offline.
node tools/gen-iocs.mjs # busca o CSV da Wiz, regenera o iocs.json + atualiza o snapshot
node tools/gen-iocs.mjs --offline # regenera do snapshot commitado, sem rede
node tools/gen-iocs.mjs --allow-shrink # permite contagem menor que a do snapshot (guarda ativa por padrão)
O gerador é idempotente: mantém o manifest_version atual e não reescreve o arquivo quando nada material mudou, e se recusa a escrever um manifesto vazio ou menor (proteção contra um feed truncado/alterado a montante).
Automação: .github/workflows/update-iocs.yml roda o gerador diariamente (e sob demanda) e commita na main só quando os IOCs de fato mudam — assim o --update dos usuários acompanha o feed da Wiz com ~1 dia de atraso, com o histórico auditável nos commits. Troque para um passo de pull request (indicado no workflow) quando o incidente esfriar e você preferir merge manual.
node test/run-tests.mjs # 15 asserções contra fixtures/demo-repo
fixtures/demo-repo é um repositório de teste com IOCs inertes (nomes e versões reais, conteúdo dummy — não contém malware). Ao varrer o repositório do scanner, a pasta fixtures/ é ignorada automaticamente (com aviso no output); os testes a escaneiam passando o caminho explicitamente.
Ferramenta de incidente único, feita para triagem rápida durante a janela ativa do ataque — não substitui Socket, Snyk ou similares. Pesquisa e IOCs: Socket.dev, Wiz Research (CSV público), Kodem Security.
Mantido por Securest8. Licença MIT.
| Nível | Significado | Ação |
|---|
COMPROMISED | Versão maliciosa instalada em node_modules, payload confirmado por hash, ou implante de persistência encontrado | Tratar o host como comprometido; seguir a ordem de resposta — limpar implante antes de rotacionar credenciais |
EXPOSED | Versão maliciosa pinada em lockfile, sem evidência de execução | Fixar versão segura, apagar node_modules, reinstalar com --ignore-scripts |
AT_RISK | Range (^/~) no package.json que admite versão maliciosa | Fixar versão exata ou bloquear no proxy de registro |
SUSPECT | Heurística (nome de arquivo do worm com hash divergente, lifecycle script suspeito) | Inspecionar manualmente — pode ser variante nova ou falso positivo |
INFO | Vetor presente mas sem IOC (ex.: task folderOpen genérica) | Revisar |