1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
|
# @npmcli/metavuln-calculator
Calculate meta-vulnerabilities from package security advisories
This is a pretty low-level package to abstract out the parts of
[@npmcli/arborist](http://npm.im/@npmcli/arborist) that calculate
metavulnerabilities from security advisories. If you just want to get an
audit for a package tree, probably what you want to use is
`arborist.audit()`.
## USAGE
```js
const Calculator = require('@npmcli/metavuln-calculator')
// pass in any options for cacache and pacote
// see those modules for option descriptions
const calculator = new Calculator(options)
// get an advisory somehow, typically by POSTing a JSON payload like:
// {"pkgname":["1.2.3","4.3.5", ...versions], ...packages}
// to /-/npm/v1/security/advisories/bulk
// to get a payload response like:
// {
// "semver": [
// {
// "id": 31,
// "url": "https://npmjs.com/advisories/31",
// "title": "Regular Expression Denial of Service",
// "severity": "moderate",
// "vulnerable_versions": "<4.3.2"
// }
// ],
// ...advisories
// }
const arb = new Aborist(options)
const tree = await arb.loadActual()
const advisories = await getBulkAdvisoryReportSomehow(tree)
// then to get a comprehensive set of advisories including metavulns:
const set = new Set()
for (const [name, advisory] of Object.entries(advisories)) {
// make sure we have the advisories loaded with latest version lists
set.add(await calculator.calculate(name, {advisory}))
}
for (const vuln of set) {
for (const node of tree.inventory.query('name', vuln.name)) {
// not vulnerable, just keep looking
if (!vuln.testVersion(node.version))
continue
for (const { from: dep, spec } of node.edgesIn) {
const metaAdvisory = await calculator.calculate(dep.name, vuln)
if (metaAdvisory.testVersion(dep.version, spec)) {
set.add(metaAdvisory)
}
}
}
}
```
## API
### Class: Advisory
The `Calculator.calculate` method returns a Promise that resolves to a
`Advisory` object, filled in from the cache and updated if necessary with
the available advisory data.
Do not instantiate `Advisory` objects directly. Use the `calculate()`
method to get one with appropriate data filled in.
Do not mutate `Advisory` objects. Use the supplied methods only.
#### Fields
- `name` The name of the package that this vulnerability is about
- `id` The unique cache key for this vuln or metavuln. (See **Cache Keys**
below.)
- `dependency` For metavulns, the dependency that causes this package to be
have a vulnerability. For advisories, the same as `name`.
- `type` Either `'advisory'` or `'metavuln'`, depending on the type of
vulnerability that this object represents.
- `url` The url for the advisory (`null` for metavulns)
- `title` The text title of the advisory or metavuln
- `severity` The severity level info/low/medium/high/critical
- `range` The range that is vulnerable
- `versions` The set of available versions of the package
- `vulnerableVersions` The set of versions that are vulnerable
- `source` The numeric ID of the advisory, or the cache key of the
vulnerability that causes this metavuln
- `updated` Boolean indicating whether this vulnerability was updated since
being read from cache.
- `packument` The packument object for the package that this vulnerability
is about.
#### `vuln.testVersion(version, [dependencySpecifier]) -> Boolean`
Check to see if a given version is vulnerable. Returns `true` if the
version is vulnerable, and should be avoided.
For metavulns, `dependencySpecifier` indicates the version range of the
source of the vulnerability, which the module depends on. If not provided,
will attempt to read from the packument. If not provided, and unable to
read from the packument, then `true` is returned, indicating that the (not
installable) package version should be avoided.
#### Cache Keys
The cache keys are calculated by hashing together the `source` and `name`
fields, prefixing with the string `'security-advisory:'` and the name of
the dependency that is vulnerable.
So, a third-level metavulnerability might have a key like:
```
'security-advisory:foo:'+ hash(['foo', hash(['bar', hash(['baz', 123])])])
```
Thus, the cached entry with this key would reflect the version of `foo`
that is vulnerable by virtue of dependending exclusively on versions of
`bar` which are vulnerable by virtue of depending exclusively on versions
of `baz` which are vulnerable by virtue of advisory ID `123`.
Loading advisory data entirely from cache without hitting an npm registry
security advisory endpoint is not supported at this time, but technically
possible, and likely to come in a future version of this library.
### `calculator = new Calculator(options)`
Options object is used for `cacache` and `pacote` calls.
### `calculator.calculate(name, source)`
- `name` The name of the package that the advisory is about
- `source` Advisory object from the npm security endpoint, or a `Advisory`
object returned by a previous call to the `calculate()` method.
"Advisory" objects need to have:
- `id` id of the advisory or Advisory object
- `vulnerable_versions` range of versions affected
- `url`
- `title`
- `severity`
Fetches the packument and returns a Promise that resolves to a
vulnerability object described above.
Will perform required I/O to fetch package metadata from registry and
read from cache. Advisory information written back to cache.
## Dependent Version Sampling
Typically, dependency ranges don't change very frequently, and the most
recent version published on a given release line is most likely to contain
the fix for a given vulnerability.
So, we see things like this:
```
3.0.4 - not vulnerable
3.0.3 - vulnerable
3.0.2 - vulnerable
3.0.1 - vulnerable
3.0.0 - vulnerable
2.3.107 - not vulnerable
2.3.106 - not vulnerable
2.3.105 - vulnerable
... 523 more vulnerable versions ...
2.0.0 - vulnerable
1.1.102 - not vulnerable
1.1.101 - vulnerable
... 387 more vulnerable versions ...
0.0.0 - vulnerable
```
In order to determine which versions of a package are affected by a
vulnerability in a dependency, this module uses the following algorithm to
minimize the number of tests required by performing a binary search on each
version set, and presuming that versions _between_ vulnerable versions
within a given set are also vulnerable.
1. Sort list of available versions by SemVer precedence
2. Group versions into sets based on MAJOR/MINOR versions.
3.0.0 - 3.0.4
2.3.0 - 2.3.107
2.2.0 - 2.2.43
2.1.0 - 2.1.432
2.0.0 - 2.0.102
1.1.0 - 1.1.102
1.0.0 - 1.0.157
0.1.0 - 0.1.123
0.0.0 - 0.0.57
3. Test the highest and lowest in each MAJOR/MINOR set, and mark highest
and lowest with known-vulnerable status. (`(s)` means "safe" and `(v)`
means "vulnerable".)
3.0.0(v) - 3.0.4(s)
2.3.0(v) - 2.3.107(s)
2.2.0(v) - 2.2.43(v)
2.1.0(v) - 2.1.432(v)
2.0.0(v) - 2.0.102(v)
1.1.0(v) - 1.1.102(s)
1.0.0(v) - 1.0.157(v)
0.1.0(v) - 0.1.123(v)
0.0.0(v) - 0.0.57(v)
4. For each set of package versions:
1. If highest and lowest both vulnerable, assume entire set is
vulnerable, and continue to next set. Ie, in the example, throw out
the following version sets:
2.2.0(v) - 2.2.43(v)
2.1.0(v) - 2.1.432(v)
2.0.0(v) - 2.0.102(v)
1.0.0(v) - 1.0.157(v)
0.1.0(v) - 0.1.123(v)
0.0.0(v) - 0.0.57(v)
2. Test middle version MID in set, splitting into two sets.
3.0.0(v) - 3.0.2(v) - 3.0.4(s)
2.3.0(v) - 2.3.54(v) - 2.3.107(s)
1.1.0(v) - 1.1.51(v) - 1.1.102(s)
3. If any untested versions in Set(mid..highest) or Set(lowest..mid),
add to list of sets to test.
3.0.0(v) - 3.0.2(v) <-- thrown out on next iteration
3.0.2(v) - 3.0.4(s)
2.3.0(v) - 2.3.54(v) <-- thrown out on next iteration
2.3.54(v) - 2.3.107(s)
1.1.0(v) - 1.1.51(v) <-- thrown out on next iteration
1.1.51(v) - 1.1.102(s)
When the process finishes, all versions are either confirmed safe, or
confirmed/assumed vulnerable, and we avoid checking large sets of versions
where vulnerabilities went unfixed.
### Testing Version for MetaVuln Status
When the dependency is in `bundleDependencies`, we treat any dependent
version that _may_ be vulnerable as a vulnerability. If the dependency is
not in `bundleDependencies`, then we treat the dependent module as a
vulnerability if it can _only_ resolve to dependency versions that are
vulnerable.
This relies on the reasonable assumption that the version of a bundled
dependency will be within the stated dependency range, and accounts for the
fact that we can't know ahead of time which version of a dependency may be
bundled. So, we avoid versions that _may_ bundle a vulnerable dependency.
For example:
Package `foo` depends on package `bar` at the following version ranges:
```
foo version bar version range
1.0.0 ^1.2.3
1.0.1 ^1.2.4
1.0.2 ^1.2.5
1.1.0 ^1.3.1
1.1.1 ^1.3.2
1.1.2 ^1.3.3
2.0.0 ^2.0.0
2.0.1 ^2.0.1
2.0.2 ^2.0.2
```
There is an advisory for `bar@1.2.4 - 1.3.2`. So:
```
foo version vulnerable?
1.0.0 if bundled (can use 1.2.3, which is not vulnerable)
1.0.1 yes (must use ^1.2.4, entirely contained in vuln range)
1.0.2 yes (must use ^1.2.5, entirely contained in vuln range)
1.1.0 if bundled (can use 1.3.3, which is not vulnerable)
1.1.1 if bundled (can use 1.3.3, which is not vulnerable)
1.1.2 no (dep is outside of vuln range)
2.0.0 no (dep is outside of vuln range)
2.0.1 no (dep is outside of vuln range)
2.0.2 no (dep is outside of vuln range)
```
To test a package version for metaVulnerable status, we attempt to load the
manifest of the dependency, using the vulnerable version set as the `avoid`
versions. If we end up selecting a version that should be avoided, then
that means that the package is vulnerable by virtue of its dependency.
|