Gerenciamento de dependências

Neste documento, descrevemos as dependências do aplicativo e as práticas recomendadas para gerenciá-las, incluindo monitoramento de vulnerabilidades, verificação de artefatos, redução do seu tamanho de dependência e compatibilidade com builds reproduzíveis.

Uma dependência de software é um software que o aplicativo precisa para funcionar, como uma biblioteca de software ou um plug-in. A resolução de dependências pode acontecer quando você está compilando código, criando, executando, fazendo o download ou instalando o software.

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

Os detalhes para implementar as práticas recomendadas podem variar de acordo com o formato de artefato e as ferramentas usadas, mas os princípios gerais ainda se aplicam.

Dependências diretas e transitivas

Os aplicativos podem incluir dependências diretas e transitivas:

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

Diferentes linguagens de programação oferecem diferentes níveis de visibilidade sobre dependências e os relacionamentos delas. 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 a introspecção de dependência é mais limitado. Além disso, os sistemas de compilação precisam usar gerenciadores de dependências específicos para gerenciá-las sistematicamente.

Como 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 para glob, a seção dependencies lista as dependências diretas para o pacote publicado. A seção devDepdencies lista as dependências para desenvolvimento e testes locais por mantenedores e colaboradores do glob.

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

  • Você pode encontrar mais informações sobre dependência 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 profundas na árvore de dependências. Exemplo:

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

Sem a visibilidade das dependências indiretas, é muito difícil identificar e responder a vulnerabilidades e outros problemas originados de um componente não referenciado diretamente pelo código.

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

Ferramentas para insights de dependência

Use as ferramentas a seguir para entender as 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 pacotes.

Software Delivery Shield (em inglês)
Uma solução totalmente gerenciada de segurança da cadeia de suprimentos de software no Google Cloud que permite visualizar insights de segurança para seus artefatos no Cloud Build, Cloud Run e GKE, incluindo vulnerabilidades, informações de dependência, lista de materiais de software (SBOM, na sigla em inglês) e procedência da criação. O Software Delivery Shield também oferece outros serviços e recursos para melhorar sua postura de segurança durante o ciclo de vida de desenvolvimento de software.
Ferramentas de código aberto

Há diversas ferramentas de código aberto disponíveis, como:

  • 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 softwares de código aberto. O projeto Open Source Insights também disponibiliza esses dados como um conjunto de dados do Google Cloud. É possível usar o BigQuery para explorar e analisar os dados.

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

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

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

Abordagens para inclusão de dependências

Há vários métodos comuns para incluir dependências com 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 código aberto.
Armazenar cópias de dependências no repositório de origem
Essa abordagem também é conhecida como fornecimento. Em vez de instalar uma dependência externa de um repositório público durante os builds, faça o download e copie a dependência para a árvore de origem do projeto. Você tem mais controle sobre as dependências fornecidas que usa, mas há várias desvantagens:
  • As dependências fornecidas aumentam o tamanho do repositório de origem e geram mais desligamentos.
  • Você precisa disponibilizar as mesmas dependências em cada aplicativo separado. Se o repositório de origem ou o processo de compilação não oferecer suporte a módulos de origem reutilizáveis, talvez seja necessário manter várias cópias das dependências.
  • O upgrade de dependências fornecidas pode ser mais difícil.
Armazenar dependências em um registro particular
Um registro particular, como o Artifact Registry, oferece a conveniência de instalação a partir de um repositório público, bem como o controle sobre suas dependências. Com o Artifact Registry, é possível:
  • Centralize os artefatos e as dependências do build para todos os aplicativos.
  • Configure os clientes do pacote do Docker e de linguagem para interagir com repositórios particulares no Artifact Registry da mesma maneira que com repositórios públicos.
  • Tenha mais controle sobre suas dependências em repositórios particulares:
  • Use o Identity and Access Management para restringir o acesso a cada repositório.
  • Use repositórios remotos para armazenar em cache dependências de fontes públicas upstream e verificar vulnerabilidades (pré-lançamento particular).
  • Use repositórios virtuais para agrupar repositórios remotos e particulares por trás de um único endpoint. Defina uma prioridade em cada repositório para controlar a ordem de pesquisa ao fazer o download ou instalar um artefato (visualização particular).
  • Use o Artifact Registry facilmente com outros serviços do Google Cloud no Software Delivery Shield, incluindo o Cloud Build, Cloud Run e Google Kubernetes Engine. Use a verificação automática de vulnerabilidades no ciclo de vida de desenvolvimento de software, gere procedência de build, controle implantações e confira insights sobre sua postura de segurança.

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

Fixação de versão

Fixação de versões significa restringir uma dependência de aplicativo a uma versão ou 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 seu aplicativo sejam reproduzíveis. No entanto, isso também significa que as versões não incluem atualizações da dependência, incluindo correções de segurança, correções de bugs ou melhorias.

É possível mitigar esse problema usando ferramentas automatizadas de gerenciamento de dependências que monitoram dependências nos repositórios de origem para novas versões. Essas ferramentas fazem atualizações nos arquivos de requisitos para atualizar as dependências conforme necessário, geralmente incluindo informações do registro de alterações ou outros detalhes.

A fixação de versões se aplica apenas a dependências diretas, não a dependências transitivas. Por exemplo, se você fixar a versão do pacote my-library, o pin vai restringir a versão do my-library, mas não as versões do software em que my-library depende. É possível restringir a árvore de dependências para um pacote em algumas linguagens 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 dependência.

Verificação de hash

Um hash é um valor gerado para um arquivo que atua 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 meio de um ataque "man-in-the-middle" ou um comprometimento do repositório de artefatos.

O uso da verificação de hash requer a confiança de que o hash recebido do repositório de artefatos não esteja comprometido.

Verificação de assinatura

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

Serviços como o sigstore oferecem uma maneira para os mantenedores assinarem artefatos de software e para os consumidores verificarem essas assinaturas.

A autorização binária verifica se as imagens de contêiner implantadas nos ambientes de execução do Google Cloud são assinadas com atestados de acordo com diversos critérios.

Bloquear arquivos e dependências compiladas

Os arquivos de bloqueio são arquivos de requisitos totalmente resolvidos, que especificam exatamente qual versão de cada dependência precisa ser instalada para um aplicativo. Geralmente produzidos automaticamente por ferramentas de instalação, os arquivos de bloqueio combinam a fixação de versões e a verificação de assinatura ou hash com uma árvore de dependências completa para o app.

As ferramentas de instalação criam árvores de dependência resolvendo completamente todas as dependências transitivas downstream das dependências de nível superior e incluem a árvore de dependências 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

Aplicativos modernos nativos da nuvem geralmente dependem de código aberto e de terceiros, bem como bibliotecas internas de código fechado. O Artifact Registry permite 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ência. Ao publicar projetos com o mesmo nome do seu projeto interno em repositórios de código aberto, os invasores podem se aproveitar de instaladores configurados incorretamente para instalar o código malicioso em vez da dependência interna.

Para evitar um ataque de confusão de dependência, é possível seguir algumas etapas:

  • Verifique a assinatura ou os hashes das dependências incluindo-os em um arquivo de bloqueio.
  • Separe a instalação de dependências de terceiros e internas em duas etapas distintas.
  • Espelhe explicitamente as dependências de terceiros necessárias no repositório particular, manualmente ou com um proxy de pull. Os repositórios remotos do Artifact Registry são proxies pull para repositórios públicos upstream.
  • Use repositórios virtuais para consolidar os 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.

Como remover dependências não usadas

À medida que as necessidades mudam e o aplicativo evolui, é possível alterar ou parar de usar algumas das dependências. Continuar a instalar dependências não utilizadas com o aplicativo aumenta seu consumo de dependências e aumenta o risco de comprometimento por uma vulnerabilidade nessas dependências.

Depois de fazer o aplicativo funcionar localmente, uma prática comum é copiar todas as dependências instaladas durante o processo de desenvolvimento no arquivo de requisitos do aplicativo. Em seguida, você implanta o aplicativo com todas essas dependências. Essa abordagem ajuda a garantir que o aplicativo implantado funciona, mas também pode introduzir dependências que você não precisa na produção.

Tenha cuidado ao adicionar novas dependências ao aplicativo. Cada uma tem o potencial de introduzir mais códigos sobre os quais você não tem controle total. Como parte do seu pipeline normal de inspeção e teste, 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ências do Maven para analisar e gerenciar dependências do Java.

Verificação de vulnerabilidades

Responder rapidamente a vulnerabilidades nas dependências ajuda a proteger a cadeia de suprimentos de software.

A verificação de vulnerabilidades permite 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 notificam quando novas vulnerabilidades surgem, às vezes mesmo com caminhos de upgrade sugeridos.

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

Também é possível usar a verificação sob demanda para verificar localmente as imagens de contêiner em busca de vulnerabilidades do SO, Go e Java. Isso permite identificar vulnerabilidades com antecedência para que você possa resolvê-las antes de armazená-las no Artifact Registry.

A seguir