Gestão de dependências

Este documento descreve as dependências das aplicações e as práticas recomendadas para as gerir, incluindo a monitorização de vulnerabilidades, a validação de artefactos, a redução da área de influência das dependências e o suporte de compilações reproduzíveis.

Uma dependência de software é um software que a sua aplicação requer para funcionar, como uma biblioteca de software ou um plug-in. A resolução de dependências pode ocorrer quando compila código, cria, executa, transfere ou instala o software.

As dependências podem incluir componentes criados por si, software de terceiros proprietário e software de código aberto. A abordagem que usa para gerir as dependências pode afetar a segurança e a fiabilidade das suas aplicações.

Os detalhes da implementação das práticas recomendadas podem variar consoante o formato do artefacto e as ferramentas que usa, mas os princípios gerais continuam a aplicar-se.

Dependências diretas e transitivas

As suas aplicações podem incluir dependências diretas e transitivas:

Dependências diretas
Componentes de software aos quais uma aplicação faz referência diretamente.
Dependências transitivas
Componentes de software que as dependências diretas de uma aplicação requerem funcionalmente. Cada dependência pode ter as suas próprias dependências diretas e indiretas, criando uma árvore recursiva de dependências transitivas que afetam a aplicação.

As diferentes linguagens de programação oferecem diferentes níveis de visibilidade das dependências e das respetivas relações. Além disso, alguns idiomas usam gestores de pacotes para resolver a árvore de dependências quando instalam ou implementam um pacote.

No ecossistema Node.js, os gestores de pacotes npm e yarn usam ficheiros de bloqueio para identificar as versões das dependências para criar um módulo e as versões das dependências que um gestor de pacotes transfere para uma instalação específica do módulo. Noutros ecossistemas de idiomas, como o Java, existe um suporte mais limitado para a introspeção de dependências. Além disso, os sistemas de compilação têm de usar gestores de dependências específicos para gerir as dependências de forma sistemática.

Por exemplo, considere o módulo npm glob versão 8.0.2. Declara dependências diretas para módulos npm no ficheiro package.json. No ficheiro package.json para glob, a secção dependencies lista as dependências diretas para o pacote publicado. A secção devDepdencies apresenta as dependências para o desenvolvimento e os testes locais por parte dos responsáveis pela manutenção e dos colaboradores do glob

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

  • Pode encontrar informações de dependência adicionais sobre glob no site 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. Por exemplo:

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

Sem visibilidade das dependências indiretas, é muito difícil identificar e responder a vulnerabilidades e outros problemas que têm origem num componente ao qual o seu código não faz referência diretamente.

Quando instala o pacote glob, o npm resolve toda a árvore de dependências e guarda a lista de versões específicas transferidas no ficheiro package.lock.json para ter um registo de todas as dependências. As instalações subsequentes no mesmo ambiente vão obter as mesmas versões.

Ferramentas para estatísticas de dependências

Pode usar as seguintes ferramentas para ajudar a compreender as suas dependências de código aberto e avaliar a postura de segurança dos seus projetos. Estas ferramentas fornecem informações em vários formatos de pacotes.

Estatísticas de segurança na Google Cloud consola
Google Cloud fornece estatísticas de segurança para os seus artefactos no Cloud Build, Cloud Run e GKE, incluindo vulnerabilidades, informações de dependências, lista de materiais de software (SBOM) e proveniência da compilação. Outros Google Cloud serviços também oferecem funcionalidades que melhoram a sua postura de segurança ao longo do ciclo de vida de desenvolvimento de software. Para saber mais, consulte a vista geral da segurança da cadeia de abastecimento de software.
Ferramentas de código aberto

Estão disponíveis várias ferramentas de código aberto, incluindo:

  • Open Source Insights: um Website 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 estes dados como um Google Cloud conjunto de dados. Pode usar o BigQuery para explorar e analisar os dados.

  • Base de dados de vulnerabilidades de código aberto: uma base de dados de vulnerabilidades pesquisável que agrega vulnerabilidades de outras bases de dados num único local.

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

  • Allstar: uma app GitHub que monitoriza continuamente as organizações ou os repositórios do GitHub para verificar a conformidade com as políticas configuradas. Por exemplo, pode aplicar uma política à sua organização do GitHub que verifique se existem colaboradores fora da organização com acesso de administrador ou de envio.

Abordagens para incluir dependências

Existem vários métodos comuns para incluir dependências na sua aplicação:

Instale diretamente a partir de fontes públicas
Instale dependências de código aberto diretamente de repositórios públicos, como o Docker Hub, o npm, o PyPI ou o Maven Central. Esta abordagem é conveniente porque não tem de manter as suas dependências externas. No entanto, uma vez que não controla estas dependências externas, a sua cadeia de abastecimento de software é mais suscetível a ataques à cadeia de abastecimento de código aberto.
Armazene cópias de dependências no seu repositório de origem
Esta abordagem também é conhecida como fornecimento. Em vez de instalar uma dependência externa de um repositório público durante as compilações, transfere-a e copia-a para a árvore de origem do projeto. Tem mais controlo sobre as dependências de fornecedores que usa, mas existem várias desvantagens:
  • As dependências de fornecedores aumentam o tamanho do repositório de origem e introduzem mais rotatividade.
  • Tem de fornecer as mesmas dependências em cada aplicação separada. Se o seu repositório de origem ou processo de compilação não suportar módulos de origem reutilizáveis, pode ter de manter várias cópias das suas dependências.
  • A atualização das dependências de fornecedores pode ser mais difícil.
Armazene dependências num registo privado

Um registo privado, como o Artifact Registry, oferece a conveniência da instalação a partir de um repositório público, bem como o controlo das suas dependências. Com o Artifact Registry, pode:

  • Centralize os seus artefactos de compilação e dependências para todas as suas aplicações.
  • Configure os clientes de pacotes de idiomas e do Docker para interagirem com repositórios privados no Artifact Registry da mesma forma que o fazem com repositórios públicos.
  • Tenha maior controlo sobre as suas dependências em repositórios privados:

    • Restrinja o acesso a cada repositório com a gestão de identidade e de acesso.
    • Use repositórios remotos para colocar em cache dependências de origens públicas a montante e analisá-las quanto a vulnerabilidades (pré-visualização privada).
    • Use repositórios virtuais para agrupar repositórios remotos e privados atrás de um único ponto final. Defina uma prioridade em cada repositório para controlar a ordem de pesquisa quando transfere ou instala um artefacto (pré-visualização privada).
  • Use o Artifact Registry com outros Google Cloud serviços, incluindo o Cloud Build, o Cloud Run e o Google Kubernetes Engine. Use a análise automática de vulnerabilidades ao longo do ciclo de vida de desenvolvimento de software, gere a proveniência da compilação, controle as implementações e veja estatísticas sobre a sua postura de segurança.

Sempre que possível, use um registo privado para as suas dependências. Em situações em que não pode usar um registo privado, considere usar os seus fornecedores de dependências para ter controlo sobre o conteúdo na sua cadeia de fornecimento de software.

Fixação de versões

A fixação de versões significa restringir uma dependência de aplicação a uma versão específica ou a um intervalo de versões. Idealmente, deve fixar uma única versão de uma dependência.

A fixação da versão de uma dependência ajuda a garantir que as compilações da sua aplicação são reproduzíveis. No entanto, também significa que as suas compilações não incluem atualizações à dependência, incluindo correções de segurança, correções de erros ou melhorias.

Pode mitigar este problema através de ferramentas de gestão de dependências automatizadas que monitorizam as dependências nos seus repositórios de origem para novas versões. Estas ferramentas atualizam os seus ficheiros de requisitos para atualizar as dependências conforme necessário, incluindo frequentemente informações do registo de alterações ou detalhes adicionais.

A fixação de versões aplica-se apenas a dependências diretas e não a dependências transitivas. Por exemplo, se fixar a versão do pacote my-library, a fixação restringe a versão de my-library, mas não restringe as versões do software de que my-library depende. Pode restringir a árvore de dependências de um pacote em alguns idiomas através de um ficheiro de bloqueio.

Validação da assinatura e do hash

Existem vários métodos que pode usar para validar a autenticidade de um artefacto que está a usar como dependência.

Validação de hash

Um hash é um valor gerado para um ficheiro que funciona como um identificador único. Pode comparar o hash de um artefacto com o valor hash calculado pelo fornecedor do artefacto para confirmar a integridade do ficheiro. A validação de hash ajuda a identificar a substituição, a adulteração ou a corrupção de dependências através de um ataque do tipo "man-in-the-middle" ou de uma violação do repositório de artefactos.

A utilização da validação de hash requer que confie que o hash que recebe do repositório de artefactos não está comprometido.

Validação de assinatura

A validação de assinatura adiciona segurança adicional ao processo de validação. O repositório de artefactos, os responsáveis pela manutenção do software ou ambos podem assinar artefactos.

Serviços como o sigstore oferecem aos responsáveis pela manutenção uma forma de assinar artefactos de software e aos consumidores uma forma de validar essas assinaturas.

A autorização binária pode verificar se as imagens de contentores implementadas em Google Cloud ambientes de tempo de execução estão assinadas com atestações para uma variedade de critérios.

Bloquear ficheiros e dependências compiladas

Os ficheiros de bloqueio são ficheiros de requisitos totalmente resolvidos que especificam exatamente que versão de cada dependência deve ser instalada para uma aplicação. Normalmente, os ficheiros de bloqueio são produzidos automaticamente pelas ferramentas de instalação e combinam a fixação de versões e a validação de hash ou assinatura com uma árvore de dependências completa para a sua aplicação.

As ferramentas de instalação criam árvores de dependências resolvendo totalmente todas as dependências transitivas a jusante das suas dependências de nível superior e, em seguida, incluem a árvore de dependências no seu ficheiro de bloqueio. Como resultado, só é possível instalar estas dependências, o que torna as compilações mais reproduzíveis e consistentes.

Misturar dependências privadas e públicas

As aplicações nativas da nuvem modernas dependem frequentemente de código open source e de terceiros, bem como de bibliotecas internas de código fechado. O Artifact Registry permite-lhe partilhar a sua lógica empresarial em várias aplicações e reutilizar as mesmas ferramentas para instalar bibliotecas externas e internas.

No entanto, quando mistura dependências privadas e públicas, a sua cadeia de fornecimento 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, os atacantes podem tirar partido de instaladores configurados incorretamente para instalar o respetivo código malicioso em vez da sua dependência interna.

Para evitar um ataque de confusão de dependências, pode tomar várias medidas:

  • Valide a assinatura ou os hashes das suas dependências, incluindo-os num ficheiro de bloqueio.
  • Separe a instalação de dependências de terceiros e dependências internas em dois passos distintos.
  • Refletir explicitamente as dependências de terceiros de que precisa no seu repositório privado, quer seja manual ou com um proxy de obtenção. 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 atrás de um único ponto final. Pode configurar prioridades para repositórios a montante, de modo que as versões dos seus artefactos privados sejam sempre prioritárias em relação aos artefactos públicos com o mesmo nome.
  • Use fontes fidedignas para pacotes públicos e imagens de base.

Remover dependências não usadas

À medida que as suas necessidades mudam e a sua aplicação evolui, pode alterar ou deixar de usar algumas das suas dependências. Continuar a instalar dependências não usadas com a sua aplicação aumenta a área de cobertura das dependências e o risco de ser comprometido por uma vulnerabilidade nessas dependências.

Depois de ter a aplicação a funcionar localmente, uma prática comum é copiar todas as dependências que instalou durante o processo de desenvolvimento para o ficheiro requirements da sua aplicação. Em seguida, implementa a aplicação com todas essas dependências. Esta abordagem ajuda a garantir que a aplicação implementada funciona, mas também é provável que introduza dependências de que não precisa na produção.

Tenha cuidado ao adicionar novas dependências à sua aplicação. Cada um tem o potencial de introduzir mais código sobre o qual não tem controlo total. Como parte do seu pipeline de testes e linting regular, integre ferramentas que auditam os seus ficheiros de requisitos para determinar se usa ou importa realmente as suas dependências.

Alguns idiomas têm ferramentas que ajudam a gerir as suas dependências. Por exemplo, pode usar o plug-in de dependência do Maven para analisar e gerir dependências Java.

Análise de vulnerabilidades

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

A análise de vulnerabilidades permite-lhe avaliar de forma automática e consistente se as suas dependências estão a introduzir vulnerabilidades na sua aplicação. As ferramentas de análise de vulnerabilidades consomem ficheiros de bloqueio para determinar exatamente de que artefatos depende e enviam-lhe uma notificação quando surgem novas vulnerabilidades, por vezes, até com caminhos de atualização sugeridos.

Por exemplo, a análise de artefactos identifica vulnerabilidades de pacotes de SO em imagens de contentores. Pode analisar imagens quando são carregadas para o Artifact Registry e monitorizá-las continuamente para encontrar novas vulnerabilidades até 30 dias após o envio da imagem.

Também pode usar a análise a pedido para analisar localmente imagens de contentores quanto a vulnerabilidades de SO, Go e Java. Isto permite-lhe identificar vulnerabilidades antecipadamente para que possa resolvê-las antes de as armazenar no Artifact Registry.

O que se segue?