Dependências e pacotes
Versionamento semântico, arquivos de trava e o risco de segurança das dependências transitivas.
Os resultados desta página vêm de testes em containers, com npm 11, pip, Go 1.25 e os registros públicos de cada ecossistema.
Instalação reproduzível no Dockerfile e no CI
# Node: npm ci falha se o package-lock.json não bater com o package.json
COPY package.json package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts# Python: versões exatas e hashes gerados a partir de requirements.in
pip-compile --generate-hashes requirements.in # gera requirements.txt
pip install --require-hashes -r requirements.txt # recusa pacotes diferentes
# Go: o go.sum é conferido em todo download
go mod download
go mod verify # confere os módulos já baixados contra o go.sum
# C#: restaura exatamente o packages.lock.json
dotnet restore --locked-modeO que cada um faz quando algo não bate, nos testes:
| Situação | Resultado |
|---|---|
npm ci com uma dependência no package.json que não está na trava | Parou com erro (EUSAGE), sem instalar nada |
go run com o hash de uma dependência alterado no go.sum | checksum mismatch e SECURITY ERROR |
pip install --require-hashes com um hash errado | THESE PACKAGES DO NOT MATCH THE HASHES |
Verificar vulnerabilidades
npm audit --omit=dev # Node, só dependências de produção
pip-audit -r requirements.txt # Python
govulncheck ./... # Go, só vulnerabilidades alcançáveis
dotnet list package --vulnerable --include-transitive # C#No teste, um projeto com o Express fixado na versão 4.17.0 mostrou no npm audit 7 vulnerabilidades, 4 de severidade alta. As versões atuais da linha 4 corrigem todas. Esse é o retrato de uma dependência que ficou anos sem atualização.
No CI, faça a verificação falhar só a partir de uma severidade escolhida, para não bloquear o trabalho por falhas de baixa gravidade:
npm audit --omit=dev --audit-level=highAtualização automática com Renovate
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"minimumReleaseAge": "3 days",
"packageRules": [
{
"matchUpdateTypes": ["patch"],
"automerge": true
},
{
"matchDepTypes": ["devDependencies"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "dependências de desenvolvimento",
"automerge": true
},
{
"matchUpdateTypes": ["major"],
"dependencyDashboardApproval": true
}
],
"vulnerabilityAlerts": {
"minimumReleaseAge": null
}
}Com essa configuração, correções e atualizações de pacotes de desenvolvimento entram sozinhas se os testes passarem, versões maiores esperam aprovação, e toda versão nova espera três dias antes de ser proposta, exceto as correções de segurança.
Isolar uma dependência atrás de uma interface própria
Trocar uma biblioteca, ou atualizá-la para uma versão maior, fica muito mais barato quando o código da aplicação não a usa diretamente. O padrão é criar uma interface própria, com os termos do seu domínio, e uma única implementação que conhece a biblioteca.
O exemplo isola o envio de e-mails. O resto do código só conhece EnviadorDeEmail; trocar de provedor, ou testar sem enviar nada, mexe num único lugar.
export interface EnviadorDeEmail {
enviar(para: string, assunto: string, corpo: string): Promise<void>;
}
// A única parte do código que conhece o SDK do provedor
export class EnviadorDoProvedor implements EnviadorDeEmail {
private readonly cliente: ClienteDoProvedor;
constructor(cliente: ClienteDoProvedor) {
this.cliente = cliente;
}
async enviar(para: string, assunto: string, corpo: string) {
await this.cliente.messages.send({ to: para, subject: assunto, text: corpo });
}
}
// Para testes: guarda os e-mails em vez de enviar
export class EnviadorFalso implements EnviadorDeEmail {
readonly enviados: { para: string; assunto: string }[] = [];
async enviar(para: string, assunto: string) {
this.enviados.push({ para, assunto });
}
}from dataclasses import dataclass, field
from typing import Protocol
class EnviadorDeEmail(Protocol):
def enviar(self, para: str, assunto: str, corpo: str) -> None: ...
class EnviadorDoProvedor:
"""A única parte do código que conhece o SDK do provedor."""
def __init__(self, cliente):
self.cliente = cliente
def enviar(self, para: str, assunto: str, corpo: str) -> None:
self.cliente.messages.send(to=para, subject=assunto, text=corpo)
@dataclass
class EnviadorFalso:
"""Para testes: guarda os e-mails em vez de enviar."""
enviados: list[tuple[str, str]] = field(default_factory=list)
def enviar(self, para: str, assunto: str, corpo: str) -> None:
self.enviados.append((para, assunto))package email
import "context"
type Enviador interface {
Enviar(ctx context.Context, para, assunto, corpo string) error
}
// EnviadorDoProvedor é a única parte do código que conhece o SDK.
type EnviadorDoProvedor struct {
Cliente ClienteDoProvedor
}
func (e EnviadorDoProvedor) Enviar(ctx context.Context, para, assunto, corpo string) error {
return e.Cliente.Send(ctx, Mensagem{To: para, Subject: assunto, Text: corpo})
}
// EnviadorFalso guarda os e-mails, para testes.
type EnviadorFalso struct {
Enviados []string
}
func (e *EnviadorFalso) Enviar(_ context.Context, para, assunto, _ string) error {
e.Enviados = append(e.Enviados, para+": "+assunto)
return nil
}import java.util.ArrayList;
import java.util.List;
public interface EnviadorDeEmail {
void enviar(String para, String assunto, String corpo);
}
// A única parte do código que conhece o SDK do provedor
final class EnviadorDoProvedor implements EnviadorDeEmail {
private final ClienteDoProvedor cliente;
EnviadorDoProvedor(ClienteDoProvedor cliente) {
this.cliente = cliente;
}
@Override
public void enviar(String para, String assunto, String corpo) {
cliente.messages().send(para, assunto, corpo);
}
}
// Para testes: guarda os e-mails em vez de enviar
final class EnviadorFalso implements EnviadorDeEmail {
final List<String> enviados = new ArrayList<>();
@Override
public void enviar(String para, String assunto, String corpo) {
enviados.add(para + ": " + assunto);
}
}public interface IEnviadorDeEmail
{
Task Enviar(string para, string assunto, string corpo);
}
// A única parte do código que conhece o SDK do provedor
public sealed class EnviadorDoProvedor(ClienteDoProvedor cliente) : IEnviadorDeEmail
{
public Task Enviar(string para, string assunto, string corpo) =>
cliente.Messages.SendAsync(para, assunto, corpo);
}
// Para testes: guarda os e-mails em vez de enviar
public sealed class EnviadorFalso : IEnviadorDeEmail
{
public List<string> Enviados { get; } = [];
public Task Enviar(string para, string assunto, string corpo)
{
Enviados.Add($"{para}: {assunto}");
return Task.CompletedTask;
}
}ClienteDoProvedor representa o SDK de um provedor de e-mail qualquer. Os nomes dos métodos dele variam; o ponto é que só a classe que implementa a interface os conhece.
O que muda de uma linguagem para outra
| Como a interface é declarada | Como a implementação é escolhida | |
|---|---|---|
| Node | interface do TypeScript, verificada só na compilação | Passada no construtor de quem usa, ou por um contêiner de injeção |
| Python | Protocol: qualquer classe com os mesmos métodos serve, sem herança | Passada no construtor; frameworks como FastAPI têm injeção de dependências |
| Go | interface, satisfeita implicitamente por quem tem os métodos | Passada como campo ou parâmetro |
| Java | interface com implements explícito | Injeção de dependências do Spring ou passagem manual |
| C# | interface com implementação explícita | Contêiner de injeção do ASP.NET (AddScoped<IEnviadorDeEmail, EnviadorDoProvedor>()) |
Não vale a pena fazer isso para todas as bibliotecas: um framework web ou um ORM permeiam o sistema inteiro, e envolvê-los cria mais problemas do que resolve. Vale para dependências com um papel claro e limitado, que podem ser trocadas ou que falam com serviços externos: e-mail, pagamentos, armazenamento de arquivos, mensageria, provedores de IA.
Lista de revisão
Ao acrescentar
- O pacote existe, tem o nome exato e é mantido?
- A biblioteca padrão não resolveria?
- Quantas dependências indiretas ele traz?
- A licença é compatível com o projeto?
Ao instalar
- O arquivo de trava está no repositório?
- O CI e o Dockerfile usam a instalação exata (
npm ci,--require-hashes,--locked-mode)? - Os scripts de instalação estão desligados no CI, onde possível?
Ao manter
- Há atualização automática, com testes que a validem?
- Vulnerabilidades são verificadas no CI, com um limite de severidade?
- Dependências com papel limitado e serviços externos ficam atrás de uma interface própria?
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
Ver os riscos das dependências na prática
Quero ver na prática como funcionam as dependências e os riscos delas, num container. Pergunte qual ecossistema eu uso. Vamos em etapas: 1. Instalar um framework popular num projeto vazio e contar quantos pacotes vieram. 2. Declarar um intervalo de versões, instalar sem arquivo de trava e ver qual versão veio. 3. Mudar o arquivo de dependências sem atualizar a trava e rodar a instalação exata. 4. Adulterar um hash no arquivo de trava e tentar instalar. 5. Criar um pacote local com script de instalação e ver o que ele consegue fazer; depois instalar com os scripts desligados. 6. Instalar uma versão antiga de um pacote popular e rodar a verificação de vulnerabilidades. Em cada etapa, peça que eu preveja o resultado antes.
Atualizar uma dependência para uma versão maior
Preciso atualizar uma dependência importante para uma nova versão maior e quero fazer isso com segurança. Pergunte qual dependência, de qual versão para qual, e qual linguagem eu uso. Me guie por: 1. Ler as notas de versão e o guia de migração, e listar as mudanças que afetam o meu código. 2. Encontrar no código todos os pontos que usam a biblioteca. 3. Fazer a atualização numa branch, corrigindo um ponto por vez com os testes rodando. 4. Conferir as dependências indiretas que mudaram junto. 5. Planejar o deploy e a forma de reverter. Se a biblioteca for usada em muitos lugares, sugira como isolá-la atrás de uma interface própria antes.
Revisar as dependências do projeto
Vou colar abaixo o arquivo de dependências do meu projeto e, se tiver, o Dockerfile e o arquivo do CI. Revise com foco em reprodutibilidade e segurança. Verifique, e para cada item diga se está correto, incorreto ou ausente: 1. Há arquivo de trava, e o CI e o Dockerfile usam a instalação exata? 2. Há dependências que a biblioteca padrão substituiria? 3. Há dependências desatualizadas em várias versões maiores? 4. Há dependências de desenvolvimento declaradas como de produção? 5. Os scripts de instalação estão desligados no CI? 6. Há verificação de vulnerabilidades no CI? 7. Há algum nome suspeito, parecido com um pacote popular? Para cada problema, explique o risco e mostre a correção. Arquivos: [cole aqui]