Principais conclusões
- A proveniência do npm comprova qual repositório, commit e fluxo de trabalho geraram um pacote, e não que o código seja seguro para execução.
- A lacuna de atestado é a diferença entre o que a proveniência comprova e o que você realmente precisa saber antes de instalar algo.
- Um fluxo de trabalho sequestrado ou um commit malicioso ainda gera uma proveniência válida, e a maioria dos pacotes do npm não contém nenhuma proveniência.
- A recompilação de pacotes a partir de código-fonte verificado em um ambiente fortificado afasta a raiz de confiança do pipeline do editor.
Se você tem dado uma olhada no npmjs.com ultimamente, provavelmente já percebeu isso. Na parte inferior de muitas páginas de pacotes, agora há uma seção de proveniência informando que o pacote foi compilado e assinado no GitHub Actions, com um link direto para o commit de onde ele veio. Parece um selo. Dá a impressão de que o problema foi resolvido.
Muitas equipes consideram essa certificação como a linha de chegada e, sinceramente, eu entendo isso. O Provenance foi um verdadeiro avanço para o ecossistema. Mas ele responde a uma questão muito mais restrita do que a maioria das pessoas imagina, e o espaço entre a questão que ele responde e aquela com a qual você realmente se preocupa é exatamente onde muitos dos recentes ataques ao npm têm se concentrado.
O que o npm provenance realmente comprova
O npm introduziu a proveniência em abril de 2023, e a chegada da publicação confiável em julho de 2025 tornou o processo praticamente automático. Quando um mantenedor publica a partir do GitHub Actions por meio da publicação confiável, a CLI do npm gera um atestado de proveniência por padrão, sem a necessidade de nenhum parâmetro. Esse atestado é uma declaração SLSA assinada, registrada no registro público de transparência do Sigstore, vinculando o tarball a um repositório de código-fonte, a um commit específico e ao fluxo de trabalho que o compilou. Se você estiver do lado do consumidor, o comando `npm audit signatures` verifica esses atestados em toda a sua árvore de instalação.
Essas informações são úteis. Antes do sistema de proveniência, o link para o repositório na página de um pacote era o que o mantenedor digitava em package.json. Nada confirmava isso e nada o vinculava a um commit. E como o comando `npm publish` empacota tudo o que estiver no diretório de trabalho do editor, um arquivo tar poderia conter resultados de compilação, arquivos avulsos ou algo muito pior que nunca existiu no Git. Para os pacotes que utilizam o `provenance`, ele fecha essa brecha. Você sabe que o arquivo tar é resultado de uma execução específica de CI baseada em um commit específico.
O que você não sabe é se alguma coisa naquela sequência merecia a sua confiança.
A lacuna na certificação
É útil dar um nome a isso, então vou chamá-lo de “lacuna de atestado”. A lacuna de atestado é a diferença entre o que um atestado de proveniência comprova (qual repositório, commit e fluxo de trabalho de compilação produziram um pacote) e o que você realmente precisa saber antes de instalá-lo, ou seja, se esse artefato é seguro e foi compilado fielmente a partir de um código-fonte que você se sentiria à vontade para executar.
A proveniência é uma afirmação sobre a origem. A segurança é uma afirmação sobre o conteúdo. Um atestado pode ser criptograficamente perfeito e, mesmo assim, não dizer nada sobre o que o código faz quando chega ao seu node_modules pasta. Se um invasor conseguir inserir seu código no commit ou na compilação, ele obtém um atestado válido sem nenhum custo.
Como um pacote passa na verificação de proveniência e ainda assim acaba te prejudicando
O caminho mais simples é um mantenedor comprometido. Se alguém assumir o controle de uma conta do GitHub com acesso de push a um projeto, não precisará mexer no npm de forma alguma. Basta enviar um commit, e o fluxo de trabalho de lançamento é executado exatamente como foi projetado, fazendo com que a nova versão seja lançada com a proveniência apontando fielmente para o commit do invasor. Todas as verificações são aprovadas, pois nada na publicação em si foi incomum. O problema está no commit, e a proveniência nunca foi criada para avaliar commits.
A segunda rota passa pelo próprio pipeline de compilação. A publicação confiável troca tokens de longa duração por tokens de curta duração gerados pelo fluxo de trabalho de CI, o que representa uma grande melhoria. Isso também significa que a identidade do fluxo de trabalho se torna a credencial. A análise de Andrew Nesbitt sobre vulnerabilidades em pacotes Python do GitHub Actions identificou casos em que o invasor acabou ficando com um token válido gerado pelo fluxo de trabalho real do projeto — a única situação em que a publicação confiável não pode fazer nada. O ecossistema é diferente, mas a mecânica se aplica diretamente ao npm.
A terceira rota é aquela que as pessoas costumam esquecer. Sua compilação de lançamento carrega suas próprias dependências — como bundlers, transpiladores e plug-ins —, todas resolvidas na hora no executor de CI. Se alguma delas estiver comprometida quando a tarefa de lançamento for executada, sua carga maliciosa pode acabar no seu dist pasta e enviá-la dentro de um pacote cuja proveniência seja irrepreensível. O atestado indica o seu commit. Ele não garante a integridade das centenas de pacotes que sua etapa de compilação instalou durante o processo de criação desse arquivo tar.
A falta de proveniência é um sinal que ninguém está percebendo
Essa próxima parte me incomoda ainda mais. A funcionalidade de proveniência é opcional, e a maior parte do registro não a ativou. Ela nem mesmo está disponível para pacotes compilados a partir de repositórios de código-fonte privados. E npm install instalará sem problemas uma versão sem atestado, mesmo que todas as versões anteriores desse pacote tivessem um.
Agora pense em como funcionam as grandes campanhas de worms. O Shai-Hulud e as ondas que se seguiram visavam tokens do npm e credenciais do GitHub para que pudessem republicar pacotes legítimos automaticamente. Alguém que possua um token roubado pode publicar de qualquer lugar, o que geralmente significa que não há execução de CI nem atestado de publicação. Em teoria, isso é um indício claro. Quando o PyPI elaborou sua análise do comprometimento da Ultralytics, observou que a segunda onda de lançamentos maliciosos se destacou por não apresentar atividade correspondente no repositório nem atestados de publicação.
Isso funciona se alguém estiver procurando pela origem. Na maioria das configurações do npm, ninguém está. Um pacote que de repente perde sua proveniência parece exatamente igual aos milhares que nunca a tiveram, e sua instalação ocorre sem nenhum problema.
A compilação a partir do código-fonte preenche uma lacuna diferente
Se a proveniência indica onde algo foi compilado, a próxima pergunta óbvia é se você mesmo pode compilá-lo e obter o mesmo resultado. Essa é a ideia por trás de uma recompilação independente. Em vez de confiar no tarball que o editor disponibilizou, você pega o código-fonte do qual ele afirma ter se originado, compila-o novamente em um ambiente sob seu controle e trata o resultado dessa compilação como o que será instalado. O pipeline do editor deixa de ser a fonte de confiança.
Essa foi a abordagem que adotamos com o SecureChain. Os pacotes são recompilados a partir de código-fonte verificado em ambientes fortificados e isolados, sem binários herdados e sem artefatos opacos do upstream. Cada um deles também é analisado em busca de malware conhecido e padrões de código suspeitos, e qualquer item que não passe na inspeção é bloqueado antes de chegar ao laptop de um desenvolvedor ou a um executador de CI. Ele funciona como um substituto direto para o registro público do npm, de modo que o npm, o Yarn e o pnpm se comportam exatamente como sua equipe espera, e pode ser posicionado a montante do Artifactory, do Nexus ou do GitHub Packages, caso você já utilize essa configuração para o proxy de pacotes.
O SecureChain também fornece proveniência SLSA Nível 3, o que pode parecer que estou minando meu próprio argumento. Mas não é o caso. A proveniência só tem significado na medida em que a compilação que ela descreve também o tenha. A proveniência do upstream indica que a infraestrutura de integração contínua (CI) do mantenedor produziu um pacote, e uma CI comprometida produz pacotes sem problemas. A proveniência associada a uma recompilação independente indica que o artefato saiu de um ambiente de compilação reforçado, a partir de código-fonte que foi verificado antes de ser incluído. É o mesmo formato SLSA em ambos os casos, mas atesta uma compilação completamente diferente.
O que uma reconstrução não pode fazer por você
A recompilação tem limites, e vale a pena ser franco a respeito deles. Se o código malicioso estiver presente no próprio código-fonte, uma recompilação fiel irá reproduzi-lo fielmente. É por isso que a recompilação e a verificação de malware precisam andar juntas. Uma verifica se o artefato corresponde ao seu código-fonte, e a outra verifica se o código-fonte é algo que você deseja que seja executado em sua máquina. Nenhuma das duas perguntas, por si só, resolve o problema.
Há também uma complicação que afeta todo o ecossistema. Nem todo pacote do npm é compilado corretamente a partir do que está em seu repositório. Alguns incluem arquivos que nunca foram submetidos ao controle de versão, outros dependem de configurações de compilação que ninguém jamais documentou e alguns poucos nem sequer têm código-fonte público. Isso representa um problema para quem tenta verificar o registro em grande escala e é um argumento válido para exigir mais dos editores, e não menos.
Por onde eu começaria
A maneira mais econômica de começar é executar o comando `npm audit signatures` na integração contínua (CI), para que você possa, pelo menos, verificar quais das suas dependências são atestadas e quais não são. Se você mesmo publica pacotes, mude para a publicação confiável, para que suas próprias versões tenham proveniência comprovada. Isso custa muito pouco e torna o ecossistema um pouco mais fácil de entender.
Em seguida, tenha uma conversa mais aprofundada com sua equipe sobre onde, na verdade, está depositada a sua confiança. Atualmente, na maioria dos projetos em JavaScript, ela está depositada em milhares de pipelines de CI e contas do npm de desconhecidos, e a proveniência apenas indica quais são. Transferir essa confiança para uma fonte que você controla — uma que recompila, verifica e continua corrigindo os pacotes muito tempo depois que seus mantenedores deixam o projeto — é uma posição muito mais sólida. É nessa última parte que o Suporte de Ciclo de Vida Ilimitado da TuxCare entra em ação, assim que um pacote chega ao fim de sua vida útil.
É importante ter a proveniência, e eu gostaria de ver muito mais registros fazendo uso dela. Ela apenas indica de onde um pacote veio, e o que mantém seus segredos de CI protegidos é o que está dentro dele.
