Skip to main content

Las 5 dimensiones de una dependencia de npm

Escrito por

16 de junio de 2016

0 minutos de lectura

A menudo hablamos del creciente número de dependencias de npm y de cómo, por un lado, nos hacen más productivos y rápidos, pero, por otro, más vulnerables y potencialmente inseguros. Pero ¿qué es exactamente una dependencia de npm?

En Snyk, nuestro producto se enfoca en proteger las dependencias, así que primero tuvimos que definir exactamente qué es una dependencia. En esta publicación abordamos sus distintas dimensiones y compartimos lo que aprendimos al intentar definir una taxonomía sencilla que te ayude a entender cómo se pueden agrupar.

Definición básica: código del que dependes

En su definición más básica, una dependencia no es más que un paquete de código del que depende tu aplicación. Sin ese código, la aplicación no funcionará correctamente e incluso podría no compilar.

Por eso, sin importar cómo las organices, todas las dependencias afectan de algún modo la funcionalidad, la confiabilidad y la seguridad de tu aplicación. Con esto en mente, veamos cómo podemos clasificarlas.

Dimensión 1: desarrollo vs. producción

El primer tipo de dependencia, y el más explícito, es la distinción entre desarrollo y producción. En el archivo package.json, las dependencias de producción se enumeran explícitamente como dependencies, mientras que las que solo se usan durante el desarrollo se llaman devDependencies.

De forma predeterminada, el comando npm install instala las dependencias de desarrollo y de producción de la aplicación actual, pero solo instala las dependencias de producción de los paquetes que se descargan de npm.

Por ejemplo, el paquete util de npm usa las siguientes secciones de dependencias en su archivo package.json:

{
  "name": "util",
...
  "dependencies": {
    "inherits": "2.0.1"
  },
...
  "devDependencies": {
    "zuul": "~1.0.9"
  },
...
}

Si ejecutas npm install util, solo se instalará la dependencia de producción inherits. Sin embargo, si clonas su repositorio, node-util y ejecutas npm install en la carpeta clonada, se instalarán tanto inherits como zuul.

Como mencionamos, esta separación la establece explícitamente una aplicación y su lógica es bastante sencilla. Si la aplicación necesita una dependencia para ejecutarse, debe ser una dependencia de producción. Si solo la necesita para las pruebas o la compilación, debe ser una dependencia de desarrollo. Al buscar vulnerabilidades, las dependencias de desarrollo son menos relevantes, por lo que en Snyk probamos solo las dependencias de producción de forma predeterminada (aunque puedes cambiarlo con la opción --dev).

Ten en cuenta que peerDependencies y optionalDependencies también son dependencias de producción, aunque hay ciertas particularidades en cuándo y cómo se instalan. Hablaremos de ellas al abordar la dimensión lógica vs. disco.

Gráfico de composición de código de WP-Calypso: 15.67% código del proyecto, 61.98% código de dependencias y 22.35% código de dependencias de desarrollo

La imagen de arriba muestra una proporción de ejemplo entre dependencias de desarrollo y de producción, según bitHound.

Dimensión 2: directas vs. indirectas

Algunas de tus dependencias son directas (también llamadas primarias): se solicitan explícitamente en tu archivo package.json. Sin embargo, la mayoría son indirectas (también llamadas secundarias): las incorpora una dependencia directa (u otra indirecta) para completar su tarea. En la mayoría de las aplicaciones, las dependencias indirectas representan la gran mayoría de la lista total. Por ejemplo, muy pocas aplicaciones usan left-pad directamente, aunque algunas de las más conocidas (como babel y node) sí lo hacen. Esto convirtió a left-pad en una dependencia indirecta de muchísimas aplicaciones, por lo que su retirada del registro tuvo un impacto tan grande.

Cuando un paquete está muy alejado de tu aplicación y anidado en lo profundo del árbol de dependencias, es fácil no saber que existe o incluso olvidarse de él. Sin embargo, las dependencias remotas siguen siendo dependencias. La eliminación de left-pad interrumpió aplicaciones, incluso algunas que ni siquiera sabían que lo usaban; incumplir la licencia de una dependencia profunda aún puede causar problemas legales, y una vulnerabilidad en una dependencia indirecta distante todavía puede permitir que un atacante acceda a tu sistema.

Dimensión 3: paquete vs. versión

Supongamos que usas el paquete request en la versión 2.11.3. Entonces, ¿cuál es tu dependencia? ¿Es request, el paquete, o request@2.11.3, la versión específica?

La respuesta completa es: ambas. Está claro que dependes de request@2.11.3. Esta versión representa un programa inmutable que incorporaste y usas en tu código. Cualquier falla en este paquete, como esta vulnerabilidad de exposición de memoria remota, afectará tu código. Además, eres responsable de cumplir con la licencia MIT bajo la cual se publicó, entre otras cosas.

Sin embargo, también dependes del paquete request como proyecto. Si lo usas mediante un rango de semver, confías en que sus autores no publiquen un cambio incompatible en una versión menor de corrección. Desde el punto de vista de la seguridad, confías en que no filtren sus credenciales de npm o GitHub, que comprueben de antemano si hay problemas de seguridad y que corrijan rápidamente las vulnerabilidades divulgadas. Con el tiempo, también confías en que el proyecto y sus autores lo mantengan adecuadamente, corrijan errores y agreguen funciones a tiempo. Puedes reducir este riesgo usando shrinkwrap para fijar las versiones de los paquetes que usas, o incluyendo las dependencias en el paquete; pero, en ambos casos, dejarás de recibir nuevas funciones y correcciones de errores.

Aunque tanto el paquete como la versión son dependencias tuyas, dependes de cada uno de formas muy distintas, así que sería conveniente darles nombres diferentes. Por desgracia, no existe un nombre explícito para la combinación paquete+versión, y el término paquete se usa indistintamente para referirse a cualquiera de los dos.

En Snyk, cuando decimos dependencia, normalmente nos referimos a una combinación de paquete+versión, por ejemplo, request@2.11.3. Cuando queremos referirnos al paquete, decimos explícitamente paquete del que se depende. Dicho esto, la taxonomía en este caso es complicada. Aunque intentamos seguir estas pautas, a veces simplemente decimos «paquete» y dejamos que el lector determine a qué nos referimos según el contexto…

Selector de versión del paquete que muestra «request», el rango «2.x» y una lista de versiones semánticas 2.x coincidentes.

Los paquetes como request tienen muchas versiones. Dependes de la calidad de cada una y de que el proyecto las gestione bien.

Dimensión 4: lógica vs. disco

Todo lo que hemos explicado hasta ahora se refiere a las dependencias lógicas: la estructura conceptual de tu árbol de dependencias. Sin embargo, el árbol puede cambiar considerablemente cuando se descarga al disco. Esto se debe en parte a las dependencias peer y opcionales, cuya instalación depende de ciertas condiciones, pero el factor que más influye es la deduplicación de npm3.

Veamos el paquete inflight. Este es su árbol lógico de dependencias:

inflight@1.0.5
├─┬ once@1.3.3
│ └── wrappy@1.0.2
└── wrappy@1.0.2

La dependencia wrappy@1.0.2 se usa tanto como dependencia directa como indirecta, a través de once@1.3.3. Si clonamos el repositorio de inflight y ejecutamos npm install --production y npm ls, obtendremos lo siguiente:

inflight@1.0.5
├── once@1.3.3
└── wrappy@1.0.2

Como puedes ver, wrappy@1.0.2 aparece una sola vez. Esto se debe a la deduplicación de npm3, que identifica el paquete repetido y evita crear otra copia en el disco. npm2 también realiza cierta deduplicación básica de forma predeterminada, y puedes ejecutarla explícitamente con npm dedupe.

Nuestro ejemplo es sencillo, pero las cosas pueden complicarse y volverse menos predecibles cuando se incluyen rangos semver y se ejecuta npm install repetidas veces. Puedes ver las dependencias lógicas y las que están en disco con snyk-resolve, sobre el que Remy escribió en este blog.

La conclusión clave de esta dimensión es que tus dependencias lógicas y las que están en disco pueden diferir, y que las dependencias en disco dependen de la lógica y el orden de instalación. Asegúrate de revisar lo que realmente se instaló para toda la aplicación, no solo la lógica de cada dependencia directa por separado.

Dimensión 5: rutas de dependencia vs. dependencias únicas

Ahora que definimos nuestras dependencias, la última dimensión tiene que ver con cómo contarlas. Considera el siguiente árbol lógico de dependencias:

app@1.2.3
├─┬ A@1.0.0
│ └── B@1.0.0
├─┬ C@1.0.0
│ └── B@2.0.0
└── B@2.0.0

Al observar este árbol, podemos decir que la aplicación tiene 3 paquetes de los que depende: A, B y C. También podemos decir que tiene 4 dependencias en disco (incluida la versión): A@1.0.0, B@1.0.0, B@2.0.0 y C@1.0.0, ya que la deduplicación evitaría las redundancias. Pero ¿cuántas dependencias lógicas tiene? ¿Tiene 4, una por cada combinación de paquete+versión, o 5, una por cada nodo del árbol de dependencias?

Para distinguirlas, podemos decir que esta aplicación tiene 4 dependencias únicas y 5 rutas de dependencia. En Snyk, por ejemplo, si se sabe que B@2.0.0 tiene una vulnerabilidad, diríamos que tiene una vulnerabilidad conocida, pero dos rutas vulnerables.

Resumen

En resumen, el conjunto de dependencias tiene varias dimensiones y cada una es más adecuada para distintos propósitos. Al hablar de dependencias, debemos procurar mantener la misma taxonomía siempre que sea posible para que la conversación fluya.

A modo de guía rápida, estas son las 5 dimensiones:

  1. Desarrollo vs. producción: Tu aplicación necesita dependencias de desarrollo para compilar y probar, y dependencias de producción para ejecutarse.

  2. Directas vs. indirectas: Tu aplicación solo requiere explícitamente dependencias directas, pero las revisiones de calidad, legales y de seguridad también deben abarcar las dependencias indirectas (que son muchas más).

  3. Paquete vs. versión: La versión específica de cada dependencia afecta a tu aplicación desplegada, pero tu proyecto depende de que cada paquete del que depende siga funcionando.

  4. Lógica vs. disco: El árbol lógico de dependencias de tu aplicación puede cambiar considerablemente al instalarse en el disco; asegúrate de verificar las versiones que realmente se instalaron.

  5. Ruta vs. única: Al contar tus dependencias, asegúrate de distinguir entre el número de dependencias únicas y las rutas de dependencia para calcular correctamente el alcance de una tarea o un problema.

Nota: puedes consultar un artículo más detallado con información y análisis sobre los manifiestos de paquetes de npm y yarn y sobre cómo funcionan los archivos de bloqueo para aplicaciones y bibliotecas.

Ahora que conoces la terminología, puedes usar Snyk para probar tu aplicación y averiguar cuántas dependencias de producción vulnerables y rutas de dependencia podría estar usando. Además, puedes buscar los paquetes de los que dependes en nuestra base de datos de vulnerabilidades para ver si tienen antecedentes de fallas de seguridad.

Empieza con los desafíos de Capture the Flag

Aprende a resolver desafíos de captura la bandera con nuestro taller virtual introductorio a pedido.

Publicado en: