As 5 dimensões de uma dependência npm
16 de junho de 2016
0 minutos de leituraFalamos com frequência sobre o número crescente de dependências npm e sobre como elas, por um lado, nos tornam mais produtivos e ágeis, mas, por outro, podem nos deixar vulneráveis e expostos a riscos. Mas o que exatamente é uma dependência npm?
Na Snyk, nosso produto se concentra em proteger dependências, então precisamos definir exatamente o que uma dependência é. Este post aborda as diferentes dimensões de uma dependência, compartilhando o que aprendemos ao tentar criar uma taxonomia simples e ajudar você a entender como elas podem ser agrupadas.
Definição básica: código do qual você depende
Na definição mais simples, uma dependência é apenas isso: um pacote de código do qual seu aplicativo depende. Sem esse código, seu aplicativo não funcionará corretamente e talvez nem sequer seja compilado.
Por isso, independentemente de como você as organiza, todas as dependências afetam de alguma forma a funcionalidade, a confiabilidade e a segurança do seu aplicativo. Com isso em mente, vamos ver como podemos classificá-las.
Dimensão 1: desenvolvimento vs. produção
O primeiro tipo de dependência, e o mais explícito, é a distinção entre desenvolvimento e produção. No arquivo package.json, as dependências de produção são listadas explicitamente como dependencies, enquanto as dependências usadas apenas durante o desenvolvimento são chamadas de devDependencies.
Por padrão, o comando npm install instala as dependências de desenvolvimento e de produção do aplicativo atual, mas instala apenas as dependências de produção de qualquer pacote baixado do npm.
Por exemplo, o pacote npm util usa as seguintes seções de dependências no arquivo package.json:
Se você executar npm install util, somente a dependência de produção inherits será instalada junto com ele. No entanto, se você clonar o repositório de node-util e executar npm install na pasta clonada, tanto inherits quanto zuul serão instalados.
Como mencionado, essa separação é definida explicitamente pelo aplicativo e segue uma lógica bastante simples. Se a dependência é necessária para o aplicativo funcionar, ela deve ser uma dependência de produção. Se for necessária apenas para testes ou compilação, deve ser uma dependência de desenvolvimento. Ao buscar vulnerabilidades, as dependências de desenvolvimento são menos relevantes; por isso, na Snyk, testamos apenas as dependências de produção por padrão (embora você possa mudar isso usando a flag --dev).
Vale observar que peerDependencies e optionalDependencies também são dependências de produção, com algumas particularidades sobre quando e como são instaladas. Vamos falar delas ao abordar a dimensão lógica vs. disco.

A imagem acima mostra uma proporção de exemplo entre dependências de desenvolvimento e de produção, criada pelo bitHound.
Dimensão 2: diretas vs. indiretas
Algumas de suas dependências são diretas (também chamadas de primárias), solicitadas explicitamente no arquivo package.json. A maioria, porém, é indireta (também chamada de secundária): é incluída por uma dependência direta (ou outra indireta) para que ela possa cumprir sua função. Na maioria dos aplicativos, as dependências indiretas representam a maior parte da lista total. Por exemplo, poucos aplicativos usam left-pad diretamente, mas alguns bastante conhecidos (como babel e node) usam. Isso fez de left-pad uma dependência indireta de um grande número de aplicativos e tornou tão impactante sua retirada do registro.
Quando um pacote está muito distante do seu aplicativo, aninhado profundamente na árvore de dependências, é fácil não saber que ele existe ou até esquecê-lo por completo. No entanto, dependências distantes continuam sendo dependências. A remoção de left-pad interrompeu o funcionamento de aplicativos, inclusive daqueles que nem sabiam que o utilizavam; não cumprir a licença de uma dependência profundamente aninhada ainda pode causar problemas jurídicos; e uma vulnerabilidade em uma dependência indireta distante ainda pode permitir a entrada de um invasor.
Dimensão 3: pacote vs. versão
Imagine que você esteja usando o pacote request na versão 2.11.3. Qual é, então, a sua dependência? É request, o pacote, ou request@2.11.3, a versão específica?
A resposta completa é: ambos.Você depende claramente de request@2.11.3. Essa versão representa um programa imutável, que você incorporou e está usando no seu código. Qualquer falha nesse pacote, como esta vulnerabilidade de exposição remota de memória, afetará seu código. Você também é responsável por cumprir a licença MIT sob a qual ele foi disponibilizado, entre outras coisas.
No entanto, você também depende do projeto do pacote request. Se você o usa com um intervalo semver, está confiando que os autores não lançarão uma alteração incompatível em uma versão menor de correção. Do ponto de vista da segurança, você confia que eles não vão vazar suas credenciais do npm ou do GitHub, que vão testar problemas de segurança com antecedência e corrigir rapidamente as vulnerabilidades divulgadas. Com o tempo, você também passa a depender do projeto e de seus autores para mantê-lo bem, corrigindo bugs e adicionando recursos no momento certo. Você pode reduzir esse risco usando shrinkwrap para fixar as versões dos pacotes que utiliza ou incluindo as dependências no pacote, mas, em ambos os casos, deixará de receber novos recursos e correções de bugs.
Embora tanto o pacote quanto a versão sejam suas dependências, você depende deles de maneiras bem diferentes; por isso, seria bom dar um nome distinto a cada um. Infelizmente, não há um termo específico para a combinação pacote+versão, e o termo pacote é usado indistintamente para os dois.
Na Snyk, quando dizemos dependência, geralmente nos referimos à combinação pacote+versão, por exemplo, request@2.11.3. Quando queremos nos referir ao pacote, dizemos explicitamente pacote dependente. Ainda assim, essa taxonomia é complicada. Embora tentemos seguir essas diretrizes, às vezes simplesmente dizemos “pacote” e deixamos que o leitor entenda o sentido pelo contexto…

Pacotes como request têm muitas versões. Você depende da qualidade de cada uma e do projeto para gerenciá-las bem.
Dimensão 4: lógico vs. disco
Tudo o que discutimos até aqui se referia às dependências lógicas — a representação conceitual da árvore de dependências. No entanto, a árvore pode mudar bastante quando os pacotes são efetivamente baixados para o disco. Isso se deve, em parte, às dependências peer e opcionais, cuja instalação é um tanto oportunista, mas o impacto é ainda maior por causa da deduplicação do npm3.
Vamos analisar o pacote inflight. Esta é a árvore lógica de dependências dele:
A dependência wrappy@1.0.2 é usada tanto como dependência direta quanto como dependência indireta por meio de once@1.3.3. Se clonarmos o repositório do inflight e executarmos npm install --production e npm ls, teremos o seguinte:
Como você pode ver, wrappy@1.0.2 aparece apenas uma vez. Isso é resultado da deduplicação do npm3, que identifica o pacote repetido e evita criar outra cópia no disco. O npm2 também executa uma deduplicação mínima por padrão, e ela pode ser acionada explicitamente executando npm dedupe.
Nosso exemplo é simples, mas as coisas podem ficar complicadas e menos previsíveis quando entram em cena intervalos semver e várias instalações do npm. Você pode ver suas dependências lógicas e as que estão no disco usando snyk-resolve, sobre o qual Remy escreveu neste blog.
O principal ponto desta dimensão é lembrar que as dependências lógicas e as que estão no disco podem ser diferentes, e que as dependências no disco dependem da lógica e da ordem de instalação. Confira o que foi realmente instalado para o aplicativo como um todo, e não apenas a lógica de cada dependência direta separadamente.
Dimensão 5: caminhos de dependência vs. dependências únicas
Agora que definimos nossas dependências, a última dimensão trata de como contá-las. Considere a seguinte árvore lógica de dependências:
Ao observar essa árvore, podemos dizer que o aplicativo tem 3 pacotes dependentes: A, B e C. Também podemos dizer que ele tem 4 dependências no disco (incluindo as versões): A@1.0.0, B@1.0.0, B@2.0.0 e C@1.0.0, pois a deduplicação evitaria redundâncias. Mas quantas dependências lógicas ele tem? São 4, uma para cada combinação de pacote+versão, ou 5, uma para cada nó da árvore de dependências?
Para distinguir as duas coisas, podemos dizer que este aplicativo tem 4 dependências únicas e 5 caminhos de dependência. Na Snyk, se, por exemplo, B@2.0.0 tiver uma vulnerabilidade conhecida, diremos que há uma vulnerabilidade conhecida, mas dois caminhos vulneráveis.
Resumo
Em resumo, há várias dimensões no conjunto de dependências, e cada uma é mais adequada para finalidades diferentes. Ao falar sobre dependências, devemos tentar manter a mesma taxonomia sempre que possível, para facilitar a comunicação.
Para consultar rapidamente, veja novamente as 5 dimensões:
Desenvolvimento vs. produção: seu aplicativo precisa de dependências de desenvolvimento para compilar e testar, e de dependências de produção para funcionar.
Diretas vs. indiretas: seu aplicativo exige explicitamente apenas dependências diretas, mas as análises de qualidade, jurídicas e de segurança também devem abranger as dependências indiretas, que são muito mais numerosas.
Pacote vs. versão: o impacto no aplicativo implantado depende da versão específica de cada dependência, mas seu projeto depende da continuidade do funcionamento de cada pacote dependente.
Lógico vs. disco: a árvore lógica de dependências do seu aplicativo pode mudar bastante quando os pacotes são instalados no disco. Confira quais versões foram realmente instaladas.
Caminhos vs. únicas: ao contar suas dependências, separe o número de dependências únicas do número de caminhos de dependência para estimar corretamente o tamanho de uma tarefa ou problema.
Observação: consulte também este artigo mais detalhado, com análises e informações sobre manifestos de pacotes npm e yarn e o funcionamento de arquivos de bloqueio para aplicativos e bibliotecas.
Agora que você conhece os termos, pode usar a Snyk para testar seu aplicativo e descobrir quantas dependências de produção vulneráveis e quantos caminhos de dependência ele pode estar usando. Além disso, você pode pesquisar os pacotes dos quais depende em nosso banco de dados de vulnerabilidades para ver se eles têm um histórico de falhas de segurança.
Comece a resolver desafios de capture the flag
Aprenda a resolver desafios de capture the flag assistindo sob demanda ao nosso workshop virtual introdutório.


