Axios, DPRK, e um PoC feito numa tarde: Como a Elastic Security apanhou um dos maiores ataques de supply chain no npm

Olá a todos!

A 31 de março de 2026, versões comprometidas do axios foram publicadas no npm. Um atacante tinha tomado conta de uma conta de maintainer, mudado o email para um endereço ProtonMail que controlava, e publicado em silêncio duas versões maliciosas: a 1.14.1 e a 0.30.4. Não injectaram código directamente no axios — isso seria demasiado óbvio. Em vez disso, adicionaram uma dependência fantasma chamada plain-crypto-js que corria um postinstall hook para instalar malware multiplataforma. Elegante no pior sentido da palavra. E a atribuição? A Microsoft Threat Intelligence disse que foi um actor norte-coreano. DPRK.

O que me fez parar quando li esta história não foi o ataque em si — infelizmente já estamos a ficar habituados a este padrão. Foi como foi descoberto. Um engenheiro da Elastic, Joe Desimone, construiu um proof of concept numa tarde de sexta-feira depois de chegar de avião, a funcionar no seu laptop, usando IA para ler diffs. E foi isso que apanhou um dos maiores supply chain compromises em memória recente no npm.

Mas há mais: a Elastic abriu o código e publicou ele no github. E alguém pegou nessa ideia, removeu a dependência do Cursor, e adaptou-a para correr com qualquer LLM local via vLLM ou llama.cpp. Chama-se supply-chain-monitor-localai, está no GitHub, e podem instalá-lo hoje.

Vamos ao princípio.

O contexto: Março de 2026 foi um mês terrível para supply chain

Para perceber porque é que este ataque ao axios é tão significativo, precisamos de recuar três semanas. Março de 2026 foi, sem exagero, um dos meses mais negros para a segurança da cadeia de fornecimento de software.

Tudo começou com o Trivy. Se não conhecem, o Trivy é um scanner de segurança open source da Aqua Security, muito usado em pipelines de CI/CD para analisar imagens Docker e detectar vulnerabilidades. Um grupo chamado TeamPCP comprometeu o aquasecurity/trivy-action do GitHub Actions — sim, comprometeram uma ferramenta de segurança — e injectaram um credential stealer que ia buscar secrets dos pipelines de CI/CD de quem usasse aquela action. A quantidade de credenciais roubadas foi enorme.
O que acontece quando roubas as credenciais de publicação de muita gente? Usas-as. Quatro dias depois, o LiteLLM foi comprometido — precisamente porque o TeamPCP tinha roubado as credenciais de publicação PyPI do LiteLLM através do pipeline envenenado do Trivy. As versões maliciosas do LiteLLM eram agressivas: SSH keys, cloud credentials, API keys, wallet data, tudo.
Dias depois, o pacote Python da Telnyx sofreu a mesma sorte. O código malicioso injectado no _client.py escondia payloads dentro de ficheiros WAV usando esteganografia, usava obfuscação base64, instalava um persistence implant no Windows disfarçado de msbuild.exe, e exfiltrava tudo para um C2 hardcoded.

Foi a cascata Trivy → LiteLLM → Telnyx. E depois chegou o axios.

Joe Desimone, investigador de segurança na Elastic, estava a seguir tudo isto. O Trivy deixou-o genuinamente preocupado — “up at night”, como ele próprio disse. A Elastic tem muitos developers. Os clientes da Elastic confiam neles para os proteger. E estava a tornar-se evidente que o modelo de confiança actual dos package registries estava a ser ultrapassado pelos atacantes.
Então, naquela sexta-feira de finais de março, depois de uma viagem de avião nocturna de São Francisco, em vez de ir dormir, sentou-se e construiu a v0.0.1 de um monitor de supply chain.

A ferramenta: Um PoC que apanhou o que ninguém mais estava a ver

A ideia central é simples. Tão simples que, ao ler, a primeira reacção é “porque é que isto não existe há anos?”

O processo funciona assim: monitoriza os registries em tempo real, compara cada nova versão publicada com a anterior, e usa um LLM para analisar o diff e determinar se as mudanças são maliciosas. Se forem, alerta pelo Slack.

Mais concretamente:

  1. Faz polling ao changelog API do PyPI e ao feed CouchDB _changes do npm para detectar novos releases
  2. Filtra contra uma watchlist dos top 15.000 pacotes por volume de downloads
  3. Descarrega directamente dos registries as versões antiga e nova — sem pip install, sem npm install, sem execução de código
  4. Gera um diff em markdown
  5. Manda o diff ao LLM com um prompt que diz o que procurar
  6. Se o veredicto for “malicioso”, dispara um alerta no Slack

O ponto três é importante e merece atenção: os packages são descarregados directamente dos registries via API, nunca instalados. Isto significa que o monitor pode correr com segurança sem risco de executar código malicioso no sistema de análise. Também significa que é completamente agnóstico à plataforma — pode analisar pacotes Linux a partir de Windows.

Para a análise LLM, Desimone usou o Cursor Agent CLI em modo read-only. O prompt é directo: procurar código obfuscado, base64, exec/eval, chamadas de rede inesperadas, esteganografia, mecanismos de persistência, abuso de lifecycle scripts. Responder com Verdict: malicious ou Verdict: benign.

Na segunda-feira à noite, três dias depois de ter construído a ferramenta, o alerta chegou ao Slack:

🚨 Supply Chain Alert: axios 0.30.4
Verdict: MALICIOUS
npm: https://www.npmjs.com/package/axios/v/0.30.4

A primeira reacção de Desimone foi descrença. Verificou. Tornou a verificar. Verificou uma terceira vez. Não era um falso positivo. Era o axios. 50 milhões de downloads por semana. Atribuído à Coreia do Norte.

O que se seguiu foi uma corrida contra o tempo: alertar a equipa de infosec da Elastic, tentar contactar o npm (o email de segurança devolveu bounce, o portal deu erro), abrir um issue no repositório do axios, publicar todos os detalhes no X porque este não era um caso de coordenação silenciosa — era malware activo a instalar-se em máquinas de developers enquanto falavam.

A equipa do axios reagiu rapidamente e puxou os packages. O servidor C2 dos atacantes estava a ser tão bombardeado com requests que estava a cair. Poderia ter sido muito pior.

A versão open source: supply-chain-monitor-localai

A Elastic open-sourceu a ferramenta original em github.com/elastic/supply-chain-monitor. Funciona mas tem uma dependência: o Cursor, que requer uma subscrição paga.

Mitko Katsarski pegou no código e fez um fork que substitui o Cursor por qualquer endpoint compatível com a API OpenAI — o que na prática significa que podem correr isto com um servidor vLLM local, llama.cpp, Ollama, ou qualquer outro LLM self-hosted. O resultado está em github.com/mitkox/supply-chain-monitor-localai e é o que vos vou mostrar a seguir.

A arquitectura é a mesma, mas sem vendor lock-in no LLM. Para quem já tem um servidor vLLM a correr no homelab ou na infraestrutura da empresa, a integração é trivial.

Como instalar e configurar

Pré-requisitos

Vão precisar de Python 3.9 ou superior e de um servidor LLM compatível com a API OpenAI. Vou mostrar o setup com vLLM, mas se tiverem Ollama, funciona da mesma forma — é só apontar o --base-url para o endpoint certo.

1. Clonar o repositório e instalar dependências

git clone https://github.com/mitkox/supply-chain-monitor-localai.git
cd supply-chain-monitor-localai
pip install -r requirements.txt

2. Configurar o servidor LLM local

Se ainda não têm um servidor vLLM a correr, instalem-no e iniciem-no com o modelo da vossa escolha. Para esta tarefa, um modelo de 8B parâmetros corre razoavelmente numa máquina com 16GB de VRAM:

pip install vllm
vllm serve meta-llama/Llama-3.1-8B-Instruct --host 0.0.0.0 --port 8000

Para quem usa Ollama, é ainda mais simples:

ollama run llama3.1

E depois apontam o monitor para http://localhost:11434/v1 com o nome do modelo adequado.

3. Configurar Slack (opcional mas recomendado)

Sem alertas, o monitor corre mas não vos acorda às 2 da manhã quando encontra algo. Se quiserem alertas, criem um ficheiro etc/slack.json:

{
    "url": "https://hooks.slack.com/services/SEU_WEBHOOK_AQUI",
    "bot_token": "xoxb-...",
    "channel": "C01XXXXXXXX"
}

O bot precisa da permissão chat:write no canal de destino.

4. Primeiro teste: analisar releases recentes

Antes de pôr a correr em modo contínuo, façam um teste de uma única passagem para confirmar que tudo está a funcionar:

python monitor.py --once --base-url http://localhost:8000/v1 --model meta-llama/Llama-3.1-8B-Instruct

Isto vai buscar os releases das últimas dezenas de minutos dos top packages do PyPI e npm, analisa-os, e termina. Devem ver output no formato:

2026-04-10 10:01:15 [INFO] Watchlist loaded: 15,000 packages
2026-04-10 10:01:19 [INFO] [pypi] Starting serial: 35,542,068 — polling every 300s
2026-04-10 10:01:19 [INFO] [npm] Starting seq: 42,817,503 — polling every 300s
2026-04-10 10:06:18 [INFO] [pypi] Processing requests 2.32.3 (rank #3)...
2026-04-10 10:07:35 [INFO] [pypi] Verdict for requests 2.32.3: BENIGN

5. Modo contínuo de produção

Quando estiverem confortáveis com o funcionamento, lançam em modo contínuo. Para monitorizar os top 1000 pacotes de ambos os ecossistemas com polling a cada 5 minutos:

python monitor.py --top 1000 --interval 300 --slack \
  --base-url http://localhost:8000/v1 \
  --model meta-llama/Llama-3.1-8B-Instruct

Para monitorização mais abrangente — os top 15.000 pacotes, que é aproximadamente o que a Elastic usou:

python monitor.py --top 15000 --interval 300 --slack \
  --base-url http://localhost:8000/v1 \
  --model meta-llama/Llama-3.1-8B-Instruct

Se quiserem monitorizar apenas npm (por exemplo, se o vosso stack é exclusivamente JavaScript):

python monitor.py --no-pypi --npm-top 5000 --interval 300 --slack \
  --base-url http://localhost:8000/v1 \
  --model meta-llama/Llama-3.1-8B-Instruct

6. Analisar um diff específico manualmente

Uma funcionalidade que uso bastante: o package_diff.py permite comparar duas versões específicas de qualquer pacote sem ter de correr o monitor completo. Útil para investigar manualmente um release suspeito:

# Comparar duas versões do axios no npm
python package_diff.py --npm axios 1.14.0 1.14.1 -o axios_diff.md

# Comparar duas versões do requests no PyPI
python package_diff.py requests 2.31.0 2.32.0 -o requests_diff.md

E depois analisar o diff com o LLM:

python analyze_diff.py axios_diff.md \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --base-url http://localhost:8000/v1

O exit code diz-vos o resultado: 0 = benign, 1 = malicious, 2 = inconclusivo. Pronto para integrar em scripts de CI/CD.

O que o LLM procura exactamente

O prompt que instrui o modelo é um aspecto crítico desta ferramenta. Não é “analisa este código” — é uma instrução precisa sobre indicadores específicos de supply chain compromise:

  • Código obfuscado — base64, exec, eval, XOR encoding, strings codificadas que escondem o que realmente fazem.
  • Chamadas de rede inesperadas — um pacote de parsing de CSV não tem razão para fazer requests HTTP. Qualquer chamada de rede introduzida numa nova versão onde não existia antes é suspeita.
  • Escritas em localizações de startup/persistência — crontabs, registry do Windows, ficheiros de autorun, diretórios de inicialização do sistema.
  • Spawning de processos e comandos shell — especialmente em lifecycle scripts como postinstall, preinstall, ou preuninstall.
  • Esteganografia — esconder payloads em ficheiros de media foi exactamente o que o ataque ao Telnyx usou.
  • Exfiltração de credenciais e tokens — acesso a ficheiros .env, variáveis de ambiente, SSH keys, cloud credentials.
  • Indicadores de typosquatting — nomes de pacotes que se assemelham a populares mas com diferenças subtis.

O que torna esta abordagem interessante é que um LLM pode entender intenção de código de uma forma que regex e análise estática convencional não conseguem. Um atacante pode obfuscar código de mil formas diferentes, cada uma com sintaxe diferente, mas o comportamento subjacente — “estou a tentar enviar as tuas credenciais para um servidor externo” — é reconhecível para um modelo que compreende código.

Um alerta real: como ficaria se o axios fosse detectado hoje

Quando o monitor encontra algo malicioso, o alerta que chega ao Slack tem este aspecto:

🚨 Supply Chain Alert: axios 0.30.4

Rank: #42 of top npm packages
Verdict: MALICIOUS
npm: https://www.npmjs.com/package/axios/v/0.30.4

Analysis summary (truncated):
The new bin/ssl_hotfix.js contains obfuscated code that downloads
and executes a remote payload on postinstall. The plain-crypto-js
dependency added in this version contacts an external IP on port 443
and exfiltrates environment variables including API tokens and cloud
credentials...

Trinta e dois segundos de context. Suficiente para perceber que há um problema, onde está, e o que provavelmente faz. Nenhuma investigação manual necessária para a triagem inicial.

Uma nota honesta sobre limitações

Seria desonesto da minha parte apresentar isto como uma bala de prata. Não é.

O monitor processa releases sequencialmente dentro de cada thread de ecossistema. Durante períodos de alto volume de releases — que acontecem, especialmente no PyPI — há um backlog de processamento. Não é real-time puro.
Precisam de um servidor LLM com recursos suficientes. Um Llama 3.1 8B precisa de uma GPU com pelo menos 10-12GB de VRAM para correr confortavelmente. Num CPU é possível mas lento para este caso de uso.
As watchlists são estáticas — carregadas uma vez no arranque a partir dos datasets de top packages. Se um pacote menos popular for comprometido e não estiver na vossa watchlist, não é detectado.

E há o problema clássico de qualquer sistema de detecção: podem haver falsos negativos. Um atacante suficientemente sofisticado que entenda como os LLMs analisam código pode, teoricamente, construir código malicioso que parece benigno a um modelo. Não é trivial, mas é possível.

Dito isto: é exactamente esta ferramenta, nesta forma imperfeita, que apanhou o axios. O perfeito é inimigo do bom, e “bom” neste contexto apanhou um ataque atribuído à Coreia do Norte.

Sobre o “soak time” e outras medidas complementares

A Elastic partilhou uma recomendação prática que acho que merece destaque separado porque é simples e accionável hoje mesmo: não puxem updates de packages imediatamente. Adicionem soak time.

A ideia é deixar novas versões “assentar” durante um período antes de os vossos builds as adoptarem. Isso dá tempo à comunidade — e a ferramentas como esta — para detectar compromises antes que cheguem às vossas pipelines de CI/CD e máquinas de desenvolvimento.

Os package managers modernos têm suporte nativo para isto:

# npm: mínimo 7 dias antes de aceitar um novo release
npm config set min-release-age 7

# pnpm
pnpm config set minimum-release-age 10080

# yarn
yarn config set npmMinimumReleaseAge 10080

# uv (Python)
uv --exclude-newer "7 days ago"

Não é uma protecção absoluta — um atacante que comprometeu um pacote popular em março e esperou dois meses para activar o payload (cenário possível) passaria por cima disto. Mas para a esmagadora maioria dos ataques oportunistas e das campanhas como a do TeamPCP, a janela de tempo é curta e o soak time funciona.

Outra medida que faz sentido é o lockfile rigoroso. package-lock.json, yarn.lock, poetry.lock — não são burocracia, são uma snapshot do estado conhecido-bom das vossas dependências. Um build que respeita o lockfile não vai puxar automaticamente uma versão comprometida publicada depois do lock ser gerado.

Porque é que isto importa além do axios

Há uma pergunta que fica a pairar depois de ler esta história toda: se uma ferramenta construída numa tarde apanhou o axios, quantos ataques menores estão a passar despercebidos contra pacotes menos populares?

A resposta honesta é que não sabemos. O PyPI publica dezenas de milhares de novos releases por dia. O npm ainda mais. A grande maioria é legítima. Mas a proporção que não é — mesmo que seja uma fracção de um por cento — representa potencialmente centenas de pacotes comprometidos por mês, muitos deles em ecossistemas de nicho onde ninguém está a olhar com atenção.

O que a história do axios demonstra é que os LLMs são genuinamente bons neste tipo de análise. Não na totalidade — como disse, falsos negativos são possíveis — mas bons o suficiente para que um modelo de 8 mil milhões de parâmetros a correr num laptop consiga identificar comportamento malicioso num diff de código em segundos, com uma taxa de falsos positivos que Desimone descreveu como inexistente durante o período de teste.

Isso é novo. E é útil.

O modelo de segurança dos package registries foi construído assumindo que os maintainers são actores de confiança e que o código é inspeccionável por humanos. A primeira assunção está errada — as contas dos maintainers são comprometidas regularmente, e com a proliferação de credenciais roubadas via ataques como o Trivy, a frequência está a aumentar. A segunda assunção é impraticável à escala — nenhuma equipa humana consegue ler todos os diffs de todas as versões de todos os packages populares.

A análise automatizada por LLM não resolve o problema na totalidade. Mas preenche uma lacuna real que existia e que estava a ser explorada.

Em resumo

O ataque ao axios de 31 de março de 2026 foi descoberto por um proof of concept construído num dia, a correr num laptop, três dias antes do ataque acontecer. Isso é simultaneamente notável — a ferramenta funcionou — e perturbante — porque implica que sem ela, o ataque podia ter corrido durante muito mais tempo.

A versão open source com suporte a LLMs locais (github.com/mitkox/supply-chain-monitor-localai) está disponível hoje e podem configurá-la numa tarde. Se têm um servidor LLM a correr — e cada vez mais pessoas têm — a integração é uma questão de apontar o --base-url para o endpoint certo.

Não vos vou dizer que isto resolve o problema de supply chain security. Não resolve. Mas é uma camada de detecção que não existia antes, que a Elastic provou que funciona na prática, e que qualquer equipa com um mínimo de infraestrutura pode adoptar sem custo de licenciamento.

O próximo axios está a acontecer. Pode ser esta semana. Pode já estar a acontecer enquanto escrevem o próximo npm install. A questão não é se, é quando — e se vão ser os primeiros a saber ou os últimos.

Mantenham-se vigilantes, adicionem soak time às vossas pipelines, e considerem seriamente monitorizar as vossas dependências activamente.
Um abraço, Nuno

P.S.: Se querem perceber exactamente o que os packages maliciosos do axios faziam — o RAT multiplataforma, o protocolo C2, como funcionava em Linux, Windows e macOS — a Elastic publicou uma análise técnica detalhada em elastic.co/security-labs/axios-one-rat-to-rule-them-all. Vale a leitura.