Gerenciamento de dependências

Este documento descreve as dependências de aplicativos e as práticas recomendadas para gerenciá-las, incluindo monitoramento de vulnerabilidades, verificação de artefatos, redução da pegada de dependência e suporte a builds reproduzíveis.

Uma dependência de software é um software necessário para o funcionamento do aplicativo, como uma biblioteca de software ou um plug-in. A resolução de dependências pode acontecer ao compilar código, criar, executar, fazer o download ou instalar o software.

As dependências podem incluir componentes criados por você, software exclusivo de terceiros e software de código aberto. A abordagem que você usa para gerenciar dependências pode afetar a segurança e a confiabilidade dos seus aplicativos.

Os detalhes da implementação das práticas recomendadas podem variar de acordo com o formato do artefato e as ferramentas usadas, mas os princípios gerais ainda se aplicam.

Dependências diretas e transitivas

Seus aplicativos podem incluir dependências diretas e transitivas:

Dependências diretas
Componentes de software que um aplicativo referencia diretamente.
Dependências transitivas
Componentes de software que as dependências diretas de um aplicativo exigem funcionalmente. Cada dependência pode ter dependências diretas e indiretas próprias, criando uma árvore recursiva de dependências transitivas que afetam o aplicativo.

Diferentes linguagens de programação oferecem níveis diferentes de visibilidade das dependências e dos relacionamentos entre elas. Além disso, algumas linguagens usam gerenciadores de pacotes para resolver a árvore de dependências ao instalar ou implantar um pacote.

No ecossistema do Node.js, os gerenciadores de pacotes npm e yarn usam arquivos de bloqueio para identificar versões de dependência para criar um módulo e as versões de dependência que um gerenciador de pacotes baixa para uma instalação específica do módulo. Em outros ecossistemas de linguagem, como Java, o suporte para introspecção de dependência é mais limitado. Além disso, os sistemas de build precisam usar gerenciadores de dependência específicos para gerenciar dependências de forma sistemática.

Por exemplo, considere o módulo npm glob versão 8.0.2. Você declara dependências diretas para módulos npm no arquivo package.json. No arquivo package.json do glob, a seção dependencies lista as dependências diretas do pacote publicado. A seção devDepdencies lista as dependências para desenvolvimento e testes locais por mantenedores e colaboradores de glob.

  • No site do npm, a página do glob lista as dependências diretas e de desenvolvimento, mas não indica se esses módulos também têm dependências próprias.

  • Você pode encontrar mais informações sobre as dependências de glob no site do Open Source Insights. A lista de dependências do glob inclui dependências diretas e indiretas (transitivas).

    Uma dependência transitiva pode ter várias camadas de profundidade na árvore de dependências. Exemplo:

    1. O glob 8.0.2 tem uma dependência direta do minimatch 5.0.1.
    2. O minimatch 5.0.1 tem uma dependência direta do brace-expression 2.0.1.
    3. O brace-expression 2.0.1 tem uma dependência direta do balanced-match 1.0.2.

Sem visibilidade das dependências indiretas, é muito difícil identificar e responder a vulnerabilidades e outros problemas que se originam de um componente que seu código não referencia diretamente.

Ao instalar o pacote glob, o npm resolve toda a árvore de dependências e salva a lista de versões específicas baixadas no arquivo package.lock.json para que você tenha um registro de todas as dependências. As instalações subsequentes no mesmo ambiente vão recuperar as mesmas versões.

Ferramentas para insights de dependência

Use as ferramentas a seguir para entender suas dependências de código aberto e avaliar a postura de segurança dos seus projetos. Essas ferramentas fornecem informações em vários formatos de pacote.

Insights de segurança no console do Google Cloud
O
Google Cloud fornece insights de segurança para seus artefatos no Cloud Build, no Cloud Run e no GKE, incluindo vulnerabilidades, informações de dependência, lista de materiais de software (SBOM) e origem da build. Outros serviços do Google Cloud também oferecem recursos que melhoram sua postura de segurança em todo o ciclo de vida de desenvolvimento de software. Para saber mais, consulte a visão geral da segurança da cadeia de suprimentos de software.
Ferramentas de código aberto

Há várias ferramentas de código aberto disponíveis, incluindo:

  • Open Source Insights: um site que fornece informações sobre dependências diretas e indiretas conhecidas, vulnerabilidades conhecidas e informações de licença para software de código aberto. O projeto Open Source Insights também disponibiliza esses dados como um conjunto de dadosGoogle Cloud . É possível usar o BigQuery para analisar os dados.

  • Banco de dados de vulnerabilidades de código aberto: um banco de dados de vulnerabilidades pesquisável que agrega vulnerabilidades de outros bancos de dados em um só local.

  • Scorecards: uma ferramenta automatizada que pode ser usada para identificar práticas arriscadas da cadeia de suprimentos de software nos seus projetos do GitHub. Ele faz verificações em repositórios e atribui a cada uma uma pontuação de 0 a 10. Em seguida, use as pontuações para avaliar a postura de segurança do seu projeto.

  • Allstar: um app do GitHub que monitora continuamente organizações ou repositórios do GitHub para verificar a adesão às políticas configuradas. Por exemplo, é possível aplicar uma política à sua organização do GitHub que verifica se há colaboradores fora da organização com acesso de administrador ou push.

Abordagens para incluir dependências

Há vários métodos comuns para incluir dependências no seu aplicativo:

Instalar diretamente de fontes públicas
Instale dependências de código aberto diretamente de repositórios públicos, como Docker Hub, npm, PyPI ou Maven Central. Essa abordagem é conveniente porque você não precisa manter suas dependências externas. No entanto, como você não controla essas dependências externas, sua cadeia de suprimentos de software fica mais propensa a ataques de cadeia de suprimentos de código aberto.
Armazenar cópias de dependências no repositório de origem
Essa abordagem também é conhecida como vendoring. Em vez de instalar uma dependência externa de um repositório público durante os builds, faça o download e copie para a árvore de origem do projeto. Você tem mais controle sobre as dependências vendidas que usa, mas há várias desvantagens:
  • As dependências disponibilizadas por pacotes de terceiros aumentam o tamanho do repositório de origem e introduzem mais rotatividade.
  • Você precisa fornecer as mesmas dependências em cada aplicativo separado. Se o repositório de origem ou o processo de build não forem compatíveis com módulos de origem reutilizáveis, talvez seja necessário manter várias cópias das dependências.
  • Fazer upgrade das dependências disponibilizadas por pacotes de terceiros pode ser mais difícil.
Armazenar dependências em um registro particular

Um registro particular, como o Artifact Registry, oferece a conveniência da instalação de um repositório público, além do controle sobre suas dependências. Com o Artifact Registry, é possível:

  • Centralize os artefatos e dependências de build de todos os seus aplicativos.
  • Configure seus clientes de pacotes de linguagem e do Docker para interagir com repositórios particulares no Artifact Registry da mesma forma que fazem com repositórios públicos.
  • Tenha mais controle sobre suas dependências em repositórios particulares:

    • Restrinja o acesso a cada repositório com Identity and Access Management.
    • Use repositórios remotos para armazenar em cache dependências de fontes públicas upstream e verifique se há vulnerabilidades (prévia privada).
    • Use repositórios virtuais para agrupar repositórios remotos e particulares em um único endpoint. Defina uma prioridade em cada repositório para controlar a ordem de pesquisa ao baixar ou instalar um artefato (prévia privada).
  • Use o Artifact Registry com outros serviços do Google Cloud , incluindo Cloud Build, Cloud Run e Google Kubernetes Engine. Use a verificação automática de vulnerabilidades em todo o ciclo de vida de desenvolvimento de software, gere a origem da build, controle as implantações e confira insights sobre sua postura de segurança.

Sempre que possível, use um registro particular para suas dependências. Em situações em que não é possível usar um registro particular, considere vender suas dependências para ter controle sobre o conteúdo na cadeia de suprimentos de software.

Fixação de versão

A fixação de versão significa restringir uma dependência de aplicativo a uma versão ou um intervalo de versões específico. O ideal é fixar uma única versão de uma dependência.

Fixar a versão de uma dependência ajuda a garantir que os builds do aplicativo sejam reproduzíveis. No entanto, isso também significa que seus builds não incluem atualizações da dependência, incluindo correções de segurança, correções de bugs ou melhorias.

É possível reduzir esse problema usando ferramentas automatizadas de gerenciamento de dependências que monitoram dependências nos repositórios de origem em busca de novas versões. Essas ferramentas atualizam os arquivos de requisitos para fazer upgrade das dependências conforme necessário, incluindo informações do changelog ou outros detalhes.

A fixação de versão se aplica apenas a dependências diretas, não transitivas. Por exemplo, se você fixar a versão do pacote my-library, a fixação vai restringir a versão de my-library, mas não as versões do software de que my-library depende. É possível restringir a árvore de dependências de um pacote em alguns idiomas usando um arquivo de bloqueio.

Verificação de assinatura e hash

Há vários métodos que podem ser usados para verificar a autenticidade de um artefato que você está usando como uma dependência.

Verificação de hash

Um hash é um valor gerado para um arquivo que funciona como um identificador exclusivo. É possível comparar o hash de um artefato com o valor de hash calculado pelo provedor do artefato para confirmar a integridade do arquivo. A verificação de hash ajuda a identificar substituição, adulteração ou corrupção de dependências por um ataque man-in-the-middle ou uma violação do repositório de artefatos.

Para usar a verificação de hash, é necessário confiar que o hash recebido do repositório de artefatos não está comprometido.

Verificação de assinatura

A verificação de assinatura adiciona mais segurança ao processo de verificação. O repositório de artefatos, os mantenedores do software ou ambos podem assinar artefatos.

Serviços como o sigstore (link em inglês) oferecem uma maneira para os mantenedores assinarem artefatos de software e para os consumidores verificarem essas assinaturas.

A autorização binária pode verificar se as imagens de contêiner implantadas em ambientes de execução do Google Cloud estão assinadas com atestados para vários critérios.

Arquivos de bloqueio e dependências compiladas

Os arquivos de bloqueio são arquivos de requisitos totalmente resolvidos, especificando exatamente qual versão de cada dependência deve ser instalada para um aplicativo. Normalmente produzidos automaticamente por ferramentas de instalação, os arquivos de bloqueio combinam fixação de versão e verificação de assinatura ou hash com uma árvore de dependência completa para seu aplicativo.

As ferramentas de instalação criam árvores de dependência resolvendo totalmente todas as dependências transitivas downstream das dependências de nível superior e incluem a árvore de dependência no arquivo de bloqueio. Como resultado, apenas essas dependências podem ser instaladas, tornando os builds mais reproduzíveis e consistentes.

Como misturar dependências privadas e públicas

Os aplicativos nativos da nuvem modernos geralmente dependem de código de código aberto e de terceiros, bem como de bibliotecas internas de código fechado. Com o Artifact Registry, é possível compartilhar sua lógica de negócios em vários aplicativos e reutilizar as mesmas ferramentas para instalar bibliotecas externas e internas.

No entanto, ao misturar dependências privadas e públicas, sua cadeia de suprimentos de software fica mais vulnerável a um ataque de confusão de dependências. Ao publicar projetos com o mesmo nome do seu projeto interno em repositórios de código aberto, invasores podem aproveitar instaladores mal configurados para instalar o código malicioso deles em vez da sua dependência interna.

Para evitar um ataque de confusão de dependências, siga estas etapas:

  • Verifique a assinatura ou os hashes das suas dependências incluindo-as em um arquivo de bloqueio.
  • Separe a instalação de dependências internas e de terceiros em duas etapas distintas.
  • Faça o espelhamento explícito das dependências de terceiros necessárias no seu repositório privado, de forma manual ou com um proxy pull-through. Os repositórios remotos do Artifact Registry são proxies pull-through para repositórios públicos upstream.
  • Use repositórios virtuais para consolidar repositórios remotos e padrão do Artifact Registry em um único endpoint. É possível configurar prioridades para repositórios upstream para que as versões de artefatos particulares sejam sempre priorizadas em relação aos artefatos públicos com o mesmo nome.
  • Use fontes confiáveis para pacotes públicos e imagens de base.

Remover dependências não utilizadas

À medida que suas necessidades mudam e seu aplicativo evolui, você pode mudar ou parar de usar algumas das suas dependências. Continuar instalando dependências não utilizadas com seu aplicativo aumenta a pegada de dependência e o risco de você ser comprometido por uma vulnerabilidade nessas dependências.

Depois que o aplicativo estiver funcionando localmente, uma prática comum é copiar todas as dependências instaladas durante o processo de desenvolvimento para o arquivo de requisitos do aplicativo. Em seguida, implante o aplicativo com todas essas dependências. Essa abordagem ajuda a garantir que o aplicativo implantado funcione, mas também pode introduzir dependências desnecessárias em produção.

Tenha cuidado ao adicionar novas dependências ao seu aplicativo. Cada uma tem o potencial de introduzir mais código sobre o qual você não tem controle total. Como parte do seu pipeline regular de linting e testes, integre ferramentas que auditam seus arquivos de requisitos para determinar se você realmente usa ou importa suas dependências.

Algumas linguagens têm ferramentas para ajudar você a gerenciar suas dependências. Por exemplo, é possível usar o plug-in de dependência do Maven para analisar e gerenciar dependências do Java.

Verificação de vulnerabilidades

Responder rapidamente às vulnerabilidades nas suas dependências ajuda a proteger sua cadeia de suprimentos de software.

Com a verificação de vulnerabilidades, é possível avaliar de forma automática e consistente se as dependências estão introduzindo vulnerabilidades no aplicativo. As ferramentas de verificação de vulnerabilidades consomem arquivos de bloqueio para determinar exatamente de quais artefatos você depende e enviam notificações quando novas vulnerabilidades surgem, às vezes até com caminhos de upgrade sugeridos.

Por exemplo, o Artifact Analysis identifica vulnerabilidades de pacotes do SO em imagens de contêiner. Ele pode verificar imagens quando elas são enviadas ao Artifact Registry e monitorá-las continuamente para encontrar novas vulnerabilidades por até 30 dias após o envio da imagem.

Você também pode usar a verificação sob demanda para verificar vulnerabilidades de SO, Go e Java em imagens de contêineres localmente. Isso permite identificar vulnerabilidades no início para que você possa resolvê-las antes de armazená-las no Artifact Registry.

A seguir