npm Shrinkwrap renovado: como bloquear dependências do npm com Package-Lock e Yarn.Lock
Assaf Hefetz
10 de janeiro de 2018
0 minutos de leituraEm 2016, o gerenciamento de dependências ganhou destaque no mundo todo quando um desenvolvedor desconhecido removeu do registro um pequeno pacote do Node.js chamado left-pad — o que quebrou milhares de projetos que dependiam dele e tirou do ar alguns dos maiores sites do mundo. A npm, Inc. precisou intervir e restaurar o pacote para acabar com o caos.
Essa história mostra o quanto as dependências são importantes. Se elas não estiverem disponíveis ou forem diferentes do esperado, problemas sérios podem acontecer.
Esqueça o ShrinkWrap: uma forma elegante de bloquear dependências no Node
Bloquear ou “fixar” dependências é uma prática recomendada amplamente adotada nos ecossistemas Ruby, Python e em outros. A ideia é congelar a versão de um pacote e de suas dependências para que, ao implantar um projeto, sempre seja instalada a mesma versão de cada dependência, tornando a instalação confiável e previsível.
No Node.js, essa prática era bem menos comum até recentemente. O npm Shrinkwrap era a solução, mas tinha vários problemas. Um deles é que o arquivo shrinkwrap.json, que contém informações confidenciais sobre suas dependências, pode ser incluído ao publicar um pacote. Outro é que o Shrinkwrap dificulta a adição de novas dependências. Além disso, o shrinkwrap apresenta um risco de segurança: pode ser vulnerável a ataques de execução remota de código quando não são usadas URLs HTTPS.
Recentemente, a comunidade npm comemorou uma solução mais nova e elegante, integrada ao npm 5: package-lock.json. Quem usa Yarn já contava com o yarn.lock, que oferece bloqueio de dependências integrado para Node. Vamos falar dessas duas soluções daqui a pouco.
Mas, antes, será que você deve bloquear as dependências?
Prós e contras de bloquear (fixar) suas dependências
Muitos profissionais, e até mesmo o guia do governo dos EUA para fabricantes de software, “Before you ship”, recomendam que os projetos fixem todas as dependências. O motivo:
Alterações incompatíveis — novas versões das dependências podem introduzir alterações incompatíveis com seu código ou com sua implementação específica.
Bugs ou problemas — uma nova versão pode apresentar problemas que passaram despercebidos pelos responsáveis pelo pacote. Como não é possível testar todas as dependências do seu código a cada atualização, é melhor usar por padrão uma versão que já tenha comprovado funcionar no seu ambiente.
Previsibilidade — o desenvolvimento ágil e a entrega contínua se baseiam na capacidade de implantar software automaticamente e de forma previsível. Atualizações de pacotes que podem causar comportamentos inconsistentes ou imprevisíveis vão contra essa previsibilidade.
Fixar pacotes também tem desvantagens:
Segurança — a Snyk oferece uma ferramenta que verifica vulnerabilidades em código aberto, por isso conhecemos bem os riscos inerentes às dependências. Nossos dados mostram que, pelo menos na comunidade Node.js, 76% dos projetos usam pacotes com vulnerabilidades conhecidas. Ao bloquear ou fixar suas dependências, você também mantém as vulnerabilidades. Se um problema de segurança for descoberto e o responsável pelo pacote lançar uma correção, você continuará usando a versão antiga e vulnerável.
Módulos de terceiros — se você é responsável por um módulo de terceiros que, por sua vez, é uma dependência de outros projetos, fixar suas dependências obrigará seus usuários a permanecer nas mesmas versões. A documentação de empacotamento do Python, por exemplo, diz que isso é restritivo demais e não é considerado uma boa prática. Agradecemos a Dustin Ingram por chamar nossa atenção para isso.
Embora o problema de segurança seja significativo, a maioria dos desenvolvedores não vai abrir mão dos benefícios de instalações e implantações previsíveis. Nossa recomendação: bloqueie as dependências, mas inclua a verificação de vulnerabilidades no seu processo de build.
Se você tiver testes e monitoramento de segurança em vigor, saberá quando uma dependência ficar desatualizada ou quando vulnerabilidades forem descobertas e poderá atualizá-la nesse momento. Em um mundo ideal, você atualizaria imediatamente todos os componentes de código aberto do seu projeto assim que novas versões fossem lançadas, para obter a máxima proteção contra falhas e vulnerabilidades. Atualizar pelo menos quando uma vulnerabilidade conhecida for descoberta parece ser uma segunda melhor opção aceitável.
Como bloquear dependências com package-lock
package-lock.json é um novo recurso do npm 5 que descreve a árvore de dependências exata gerada nas instalações anteriores. Assim, é possível gerar uma árvore de dependências idêntica nas instalações seguintes, independentemente de atualizações intermediárias das dependências.
O npm 5 cria automaticamente o arquivo package-lock, que deve ser incluído no controle de versão. Embora isso possa ser trabalhoso, há dois grandes benefícios:
Depois de adicionar package-lock ao seu repositório, você conta com uma camada extra de segurança. É possível usar valores de integridade SHA para confirmar que o que está sendo instalado é exatamente o que você esperava.
Adicionar package-lock ao repositório acelera o processo de instalação, pois o npm não precisa resolver os metadados dos pacotes instalados anteriormente.
O formato de package-lock.json é idêntico ao de npm-shrinkwrap.json. Se npm-shrinkwrap.json estiver presente, package-lock.json será completamente ignorado.
Estas são algumas das variáveis do arquivo package-lock.json:
name — package-lock.json define um bloqueio para um pacote específico; aqui, você informa o nome do pacote.
version — a versão que você quer bloquear.
lockfileVersion — versão do arquivo package-lock, começando em 1.
packageIntegrity — valor de integridade criado a partir de package.json. Pode ser gerado por módulos como ssri.
preserveSymlinks — indica se a instalação foi feita com a variável de ambiente NODE_PRESERVE_SYMLINKS.
dependencies — um mapeamento do nome do pacote para objetos de dependência. Os objetos de dependência têm as seguintes propriedades:
version — um identificador exclusivo deste pacote, que pode ser usado para buscar uma nova cópia dele. Consulte a documentação de package-lock para saber como especificar versões para diferentes fontes de pacotes.
bundled — indica se a dependência está incluída no pacote e, em caso afirmativo, se deve ser extraída do módulo pai em vez de instalada como uma dependência separada.
dev — indica se essa dependência é uma dependência de desenvolvimento ou uma dependência transitiva de uma delas.
Outras propriedades são integrity, resolved, optional e dependencies (subdependências desta dependência).
Acesse a documentação de package-lock.json para saber mais e confira o post de JiÅà PospÃÅ¡il‘s com boas práticas para bloquear arquivos no npm 5.
Como bloquear dependências com yarn.lock
O Yarn já vem com bloqueio de dependências integrado, como no Ruby. Para garantir instalações consistentes em diferentes máquinas, o Yarn precisa saber exatamente quais versões de cada dependência foram instaladas.
O Yarn fornece um arquivo yarn.lock gerado automaticamente, localizado na raiz do projeto. Assim como package-lock do npm, yarn.lock deve ser incluído no controle de versão.
O arquivo é parecido com este (exemplo retirado da documentação do yarn.Lock).
Quando você adiciona, atualiza ou remove dependências, o Yarn atualiza automaticamente o arquivo yarn.lock. Você não deve editá-lo diretamente. O Yarn considera apenas o arquivo yarn.lock no nível superior e ignora os arquivos yarn.lock dentro das dependências. O arquivo de nível superior inclui tudo o que é necessário para bloquear as versões de todos os pacotes na árvore de dependências.
Para saber mais, consulte a documentação do yarn.lock ou este guia detalhado da Pluralsight, que explica o Yarn, incluindo o recurso yarn.lock.
Conclusão
As dependências são muito importantes. Para evitar surpresas — e porque cada vez que você instala um pacote pode receber versões diferentes das dependências aninhadas —, a maioria dos desenvolvedores front-end bloqueia ou fixa suas dependências. Os usuários do npm ficaram para trás, mas isso mudou com o novo recurso package-lock do npm 5.
Agora há pelo menos duas maneiras práticas de bloquear pacotes no npm: o arquivo package-lock.json do npm 5, que substitui o shrinkwrap como uma solução mais elegante, e o mecanismo integrado de bloqueio de dependências do Yarn, conhecido como yarn.lock.
Nossa recomendação: bloqueie as dependências, mas não deixe de acompanhá-las. Configure um processo para monitorar continuamente as vulnerabilidades nas suas dependências usando uma ferramenta como a Snyk. De tempos em tempos, revise também sua árvore de dependências e atualize ativamente as que estiverem desatualizadas, para garantir a máxima proteção contra falhas e vulnerabilidades futuras.