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:
- Faz polling ao changelog API do PyPI e ao feed CouchDB
_changesdo npm para detectar novos releases - Filtra contra uma watchlist dos top 15.000 pacotes por volume de downloads
- Descarrega directamente dos registries as versões antiga e nova — sem
pip install, semnpm install, sem execução de código - Gera um diff em markdown
- Manda o diff ao LLM com um prompt que diz o que procurar
- 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, oupreuninstall. - 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.
