PPxu

通过分析 NPM 上 165 万个版本的 Node.js 模块我学到了什么

原文链接: snyk.io

June 16, 2016

一个 npm 依赖的 5 个维度

Photo of Guy Podjarny

Guy Podjarny

Subscribe to RSS feed

我们经常谈到不断增长的 npm 依赖数目,以及它们是如何一方面提高了我们的生产力和开发速度,一方面又使得我们的产品变得脆弱和出现潜在的不安全性。不过一个 npm 依赖到底应该是什么样的?

在 Snyk,我们的产品关注点是安全的依赖,因此我们首先必须定义好一个依赖到底 _是_ 什么。这篇文章讨论了一个依赖的不同维度,分享我们在尝试定义一种简单的分类方法时的收获,并且帮助你们掌握如何对它们进行归类。

基本定义:你依赖的代码

在最简单的定义中,一个依赖仅仅是 —— 你的应用依赖的一个代码包。没有这段代码,你的应用就不能正确运行,甚至可能根本无法构建。

正因于此,无论你如何组织它们,每个依赖总是以某种方式影响你的应用的功能、可靠性和安全性。记住这些,下面我们来看看如何划分它们。

维度一:开发 vs 生产

第一条也是最明显的依赖类型是开发 vs 生产。在 package.json 文件中,生产环境的依赖被明确的列为 dependencies,同时只在开发环境用到的依赖被叫做 devDependencies

默认情况下,npm install 命令会同时安装当前应用的开发和生产依赖,但是对于任何从 npm 下载的包只会安装其生产依赖。

举个例子,npm 包 util 的 package.json 文件中有下述依赖片段:

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

当你运行 npm install util 的时候,只会安装 inherits (生产) 依赖。不过,如果你克隆它的仓库 node-util,然后在它的本地文件夹中运行 npm install,那么 inheritszuul 都会被安装。

如上所述,这种区别显然是由应用本身完成的,逻辑上也是非常直接。如果这个依赖是应用 _运行_ 时需要的,它就应当是一个生产依赖。如果它只是在测试或者构建时需要的,就应该是 _开发_ 依赖。在寻找漏洞的时候,开发依赖的影响很小,因此在 Snyk 我们默认只测试生产依赖 (当然你可以通过添加 --dev 后缀来改变)。

注意 peerDependenciesoptionalDependencies 也是 生产 依赖,解释它们何时以及如何安装有一点绕,我们将在讨论逻辑 vs 磁盘维度的时候来解释这些。

WP-Calypso 上生产和开发依赖的代码比例

开发和生产依赖的抽样比例,来自 bitHound

维度二:直接 vs 间接

你的一些依赖是直接的 (也叫做主要的),是在 package.json 文件中显式请求的。然而大部分的依赖是间接的 (也叫做次要的),是由一个直接依赖 (或者另一个间接依赖) 为了完成它的任务而引入的。对于大多数应用,间接依赖占据了大部分的依赖列表。比如说,很少有应用直接引用 left-pad,但是一些热门应用 (像是 babelnode) 都引用了。这意味着 left-pad 是非常多应用的 _间接_ 依赖,因此 把它下架是如此轰动

当一个嵌套在依赖树很深层的包从你的应用中被移除了,很难去意识到,甚至可能完全忘掉它。然而,再遥远的依赖也是依赖。 left-pad 的下架使得很多应用崩溃,包括一些完全不知道自己用到了这个包的应用;没有遵循一个深层依赖的许可证可能导致法律问题;一个遥远的间接依赖中的漏洞也可能被攻击者所利用。

维度三:包 vs 版本

如果你说你正在使用 2.11.3 版本的 request 包,那么你的依赖是什么?是 request 这个包,还是 `request@2.11.3` 这个特定的版本?

完整的答案是两者都有。显而易见的,你依赖了 `request@2.11.3`。这个版本代表了一个确定的程序,你可以在你的代码中引入并使用。这个包中的任何缺陷,比如 这个远程内存溢出漏洞 将会影响你的代码,你还需要遵守它使用的 MIT 许可证,等等。

不过,你同时也把 request 包作为一个项目来依赖。如果你在一个 语义化版本 的范围内使用它,你需要依赖它的作者不会在一个小的修复版本上做一个不兼容的改动。从安全性的角度来看,你依赖它们不要 泄露它们的 npm 或者 Github 凭证,提前测试可能的安全问题,并且快速修复已经发现的漏洞。同时,你还依赖这个项目和它的作者能够好好的维护它,及时修复bug,并且增加功能。你可以用 shrinkwrap 来冻结你正在使用的包版本来减少这种风险,或者通过 绑定依赖,但是这么做的同时你将无法继续获得新功能和bug修复。

由于包和版本都是你的依赖,你需要用非常不同的方式来依赖它们,因此一个好的办法是分别起一个不同的名字。不幸的是,对于一个 包 + 版本 的组合并没有一个显而易见的名字,_包_ 这个词也可以被任何一方拿来使用。

在 Snyk,当我们说到 _依赖_ 我们通常是指一个 包 + 版本 的组合,比如 `request@2.11.3`。当我们想指定包的时候,我们会明确的说一个 依赖包。也就是说,基于这一点的分类很棘手,因此尽管我们试着去坚持这些原则,有时候我们也只是说 「包」,让读者根据上下文来判断我们的意图…

`request`模块的所有版本

类似 request 的包有许多版本。

你需要基于每个的质量,以及项目本身来管理好它们。

维度四:逻辑 vs 磁盘

目前为止我们讨论的所有东西都是指 _逻辑_ 依赖 —— 你的依赖树在概念上的呈现形式。然而,当依赖树实际被下载到磁盘上的时候可能有很大的变化。这有部分是由于 peeroptional 依赖导致的,会有一些稍微投机的安装方式,但是主要是被 npm3 的去复用影响的。

我们来看看 inflight 包。这是它的 逻辑依赖树

1
2
3
4
inflight@1.0.5
├─┬ once@1.3.3
│ └── wrappy@1.0.2
└── wrappy@1.0.2

其中依赖 `wrappy@1.0.2同时被作为一个直接依赖和once@1.3.3的一个间接依赖。如果我们克隆 inflight 的仓库并且运行npm install --productionnpm ls`,我们会得到:

1
2
3
inflight@1.0.5
├── once@1.3.3
└── wrappy@1.0.2

正如你看到的,`wrappy@1.0.2只出现了一次。这就是 npm3 的去复用实现的,它可以识别出重复的包,避免在磁盘上创建另一份拷贝。一些简单的去复用默认在 npm2 上也能运行,可以通过运行npm dedupe` 来显式的调用。

我们的例子看起来很简单,但是当你碰到一个范围的版本号和重复安装的npm时,事情会变得很麻烦而且难以预测。你可以在 snyk-resolve 上面查看你的逻辑和磁盘依赖,这一点 Remy 在他的博客上写过

对于这一维度,把握的关键是记住你的逻辑和磁盘依赖可能不同,磁盘依赖基于安装逻辑和顺序。确保要检查这个应用在整体上到底安装了什么,而不仅仅是基于每个单独的直接依赖的逻辑。

维度五:依赖路径 vs 唯一依赖

目前我们已经定义好了我们的依赖,最后一个维度处理如何对它们计数。考虑如下的逻辑依赖树:

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

观察这棵树,我们可以看到这个应用有 3 个依赖包 —— ABC。我们也可以看到它有 4 个磁盘依赖 (包括版本) —— `A@1.0.0,B@1.0.0,B@2.0.0C@1.0.0` —— 由于去复用机制避免了冗余。但是它到底有几个逻辑依赖呢?是 4 个 包 + 版本 组合的依赖,还是 5 个代表每个依赖树节点的依赖?

为了区别这两者,一个办法是指明这个应用有 4 个 _唯一_ 依赖,以及 5 个依赖 _路径_。举个例子,在 Snyk,如果 `B@2.0.0` 有一个已知的漏洞,我们会说它有一个已知的漏洞,但是是两个漏洞路径。

总结

总结一下,你的依赖集合有多重维度,对于不同的目的有不同的适合维度。当讨论依赖的时候,我们应当尽可能的尝试保持相同的分类,使得沟通更方便。

做一个归纳,这 5 个维度分别是:

  1. 开发 vs 生产:你的应用需要开发依赖用于构建和测试,生产依赖用于运行。

  2. 直接 vs 间接:你的应用只显式的要求直接依赖,但你的质量,法律和安全检查也应当覆盖到 (相当数量的) 间接依赖。

  3. 包 vs 版本:你部署的应用受到每个特定版本的依赖影响,但是你的项目需要每个依赖的包来保持运行。

  4. 逻辑 vs 磁盘:在安装到磁盘上的时候,你的应用的逻辑依赖树可能有很大的变化,你需要明确知道实际安装的版本。

  5. 路径 vs 唯一:当对你的依赖计数时,一定要把唯一依赖的数目从依赖路径中区分出来,这样可以正确的估计一个任务或问题的大小。

好了现在你已经知道了这些术语,你可以用 Snyk 来 测试你的应用,找出有多少有漏洞的产品依赖和依赖路径正在被使用。此外,你还可以在我们的 VulnDB 上搜索你的依赖包,来查看它们是否有历史安全问题。

对这篇文章有任何问题?请在Twitter @snyksec 上联系我们