dependabot-npm_and_yarn 0.195.0 → 0.196.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 331f32c7de63c0e1d6fba3c5fa07440bd1a2ecbd7b4bf6eb9fc5bc7f65df961b
4
- data.tar.gz: b5ca5c033cab29f1bb7ad956777bf209825a9cba3632f2cf69ae2635f35408d1
3
+ metadata.gz: 17d091442f5535aee32eff806235c2c2fd5a003c1a0462020b66ed72877193c5
4
+ data.tar.gz: 0bc5a3ea0d891b2b146b576fa173e335b61e6495eb7d5246fffc9c7742dc2e55
5
5
  SHA512:
6
- metadata.gz: 35aea567a10c997e1c6b231da5d8f3042dd5ce41182a750de50ed9f3d9e5852f578bcb2d9e5c75d6bef4f9280d2d64d85852315634ab3c15d4e0c30fc7fa81c7
7
- data.tar.gz: c1ba801b5b44b4a813baad59f6e5b1de4e4df1112178bda5025efac571fadbf7fbb18149d0f58dcfe3e69ab7b9215f16dc6aa5551e27c8dd83bd3b41c15aefdb
6
+ metadata.gz: 488838fc133bb86735d241857ce6ce2825a2b00d555568999c435426e64b94a01f657f685f53b4f354daeb2437d257dcd319b4e935303bc8eb2237379ef25bd7
7
+ data.tar.gz: 239df30dd2eb4694e28f7382c1bfa6c75dc64b94d3d086c7831bbe893d1d06c5cb86244ad93635a192f0e96f6d3aa91aba6cefb535464f77492bad8796454c4b
@@ -1,6 +1,9 @@
1
1
  const conflictingDependencyParser = require("./conflicting-dependency-parser");
2
+ const vulnerabilityAuditor = require("./vulnerability-auditor");
2
3
 
3
4
  module.exports = {
4
5
  findConflictingDependencies:
5
6
  conflictingDependencyParser.findConflictingDependencies,
7
+ vulnerabilityAuditor:
8
+ vulnerabilityAuditor.findVulnerableDependencies,
6
9
  };
@@ -0,0 +1,243 @@
1
+ /* Vulnerability auditor for npm
2
+ *
3
+ * Inputs:
4
+ * - path to directory containing a package.json and a package-lock.json
5
+ * - array of security advisories to audit for,
6
+ * [
7
+ * { dependency_name: string, affected_versions: [string, ...] },
8
+ * ...
9
+ * ]
10
+ *
11
+ * Outputs:
12
+ * - object indicating whether a fix is available and how to achieve it,
13
+ * {
14
+ * dependency_name: string,
15
+ * current_version: string,
16
+ * target_version: string,
17
+ * fix_available: boolean | object,
18
+ * fix_updates: [
19
+ * {
20
+ * dependency_name: string,
21
+ * current_version: string,
22
+ * target_version: string
23
+ * },
24
+ * ...
25
+ * ]
26
+ * }
27
+ */
28
+
29
+ const Arborist = require('@npmcli/arborist')
30
+ const nock = require('nock')
31
+ const { promisify } = require('util');
32
+ const exec = promisify(require('child_process').exec)
33
+
34
+ async function findVulnerableDependencies(directory, advisories) {
35
+ const npmConfig = await loadNpmConfig()
36
+ const caCerts = loadCACerts(npmConfig)
37
+ const registryOpts = extractRegistryOptions(npmConfig)
38
+ const registryCreds = loadNpmConfigCredentials(directory)
39
+
40
+ const arb = new Arborist({
41
+ path: directory,
42
+ auditRegistry: 'http://localhost:9999',
43
+ ca: caCerts,
44
+ force: true,
45
+ dryRun: true,
46
+ ...registryOpts,
47
+ ...registryCreds,
48
+ })
49
+
50
+ const scope = nock('http://localhost:9999')
51
+ .persist()
52
+ .post('/-/npm/v1/security/advisories/bulk')
53
+ .reply(200, convertAdvisoriesToRegistryBulkFormat(advisories))
54
+
55
+ if (!nock.isActive()) {
56
+ nock.activate()
57
+ }
58
+
59
+ try {
60
+ const name = advisories[0].dependency_name
61
+ const auditReport = await arb.audit()
62
+ const vuln = auditReport.get(name)
63
+ const version = [...vuln.nodes][0].version
64
+ const fixAvailable = vuln.fixAvailable
65
+ const response = {
66
+ dependency_name: name,
67
+ current_version: version,
68
+ fix_available: Boolean(fixAvailable),
69
+ fix_updates: [],
70
+ }
71
+
72
+ if (!Boolean(fixAvailable)) {
73
+ return response
74
+ }
75
+
76
+ const chains = buildDependencyChains(auditReport, name)
77
+
78
+ // In order to consider the vuln dependency in question fixable,
79
+ // all dependency chains originating from it must be fixable.
80
+ if (chains.some((chain) => !Boolean(chain.fixAvailable))) {
81
+ response.fix_available = false
82
+ return response
83
+ }
84
+
85
+ for (const chain of chains) {
86
+ const topMost = chain.items[0]
87
+ if (topMost.name === name) {
88
+ continue
89
+ }
90
+ response.fix_updates.push({
91
+ dependency_name: topMost.name,
92
+ current_version: topMost.version,
93
+ })
94
+ }
95
+
96
+ const fixTree = await arb.audit({
97
+ fix: true,
98
+ })
99
+
100
+ response.target_version = fixTree.children.get(name)?.version
101
+
102
+ for (const update of response.fix_updates) {
103
+ update.target_version =
104
+ fixTree.children.get(update.dependency_name)?.version
105
+ }
106
+
107
+ return response
108
+ } finally {
109
+ nock.cleanAll()
110
+ nock.restore()
111
+ }
112
+ }
113
+
114
+ function convertAdvisoriesToRegistryBulkFormat(advisories) {
115
+ // npm audit differentiates advisories by `id`. In order to prevent
116
+ // advisories from being clobbered, we maintain a counter so that each
117
+ // advisory gets a unique `id`.
118
+ let nextAdvisoryId = 1
119
+
120
+ return advisories.reduce((formattedAdvisories, advisory) => {
121
+ if (!formattedAdvisories[advisory.dependency_name]) {
122
+ formattedAdvisories[advisory.dependency_name] = []
123
+ }
124
+ let formattedVersions =
125
+ advisory.affected_versions.reduce((memo, version) => {
126
+ memo.push({
127
+ id: nextAdvisoryId++,
128
+ vulnerable_versions: version
129
+ })
130
+ return memo
131
+ }, [])
132
+ formattedAdvisories[advisory.dependency_name].push(...formattedVersions)
133
+ return formattedAdvisories
134
+ }, {})
135
+ }
136
+
137
+ /* Traverses all effects originating from the named dependency in the
138
+ * audit report and builds an array of all dependency chains,
139
+ * [
140
+ * {
141
+ * fixAvailable: true | false | object,
142
+ * items: [
143
+ * { name: 'foo', version: '1.0.0' },
144
+ * ...
145
+ * ]
146
+ * },
147
+ * ...
148
+ * ]
149
+ *
150
+ * The first item in the chain is always the top-most dependency affected by
151
+ * the vulnerable dependency in question. The `fixAvailable` field
152
+ * applies to the first item in the chain (if that item is fixable, then
153
+ * every item after it must be fixable, too).
154
+ */
155
+ function buildDependencyChains(auditReport, name, chain = { items: [] }) {
156
+ const vuln = auditReport.get(name)
157
+ const version = [...vuln.nodes][0].version
158
+ const item = { name, version }
159
+
160
+ if (!vuln.effects.size) {
161
+ // If the current vuln has no effects, we've reached the end of this chain.
162
+ return [{ fixAvailable: vuln.fixAvailable, items: [item, ...chain.items] }]
163
+ }
164
+
165
+ return [...vuln.effects].reduce((chains, effect) => {
166
+ return chains.concat(
167
+ buildDependencyChains(
168
+ auditReport, effect.name, { items: [item, ...chain.items] }))
169
+ }, [])
170
+ }
171
+
172
+ async function loadNpmConfig() {
173
+ const configOutput = await exec('npm config ls --json')
174
+ return JSON.parse(configOutput.stdout)
175
+ }
176
+
177
+ function extractRegistryOptions(npmConfig) {
178
+ const opts = []
179
+ for (const [key, value] of Object.entries(npmConfig)) {
180
+ if (key == "registry" || key.endsWith(":registry")) {
181
+ opts.push([key, value])
182
+ }
183
+ }
184
+ return Object.fromEntries(opts)
185
+ }
186
+
187
+ // loadNpmConfig doesn't return registry credentials so we need to manually extract them. If available,
188
+ // Dependabot will have written them to the project's .npmrc file.
189
+ const ini = require('ini')
190
+ const path = require('path')
191
+
192
+ const credKeys = ['token', '_authToken', '_auth']
193
+
194
+ function loadNpmConfigCredentials(projectDir) {
195
+ const projectNpmrc = maybeReadFile(path.join(projectDir, '.npmrc'))
196
+ if (!projectNpmrc) {
197
+ return {}
198
+ }
199
+
200
+ const credentials = []
201
+ const config = ini.parse(projectNpmrc)
202
+ for (const [key, value] of Object.entries(config)) {
203
+ if (credKeys.includes(key) || credKeys.some((credKey) => key.endsWith(':' + credKey))) {
204
+ credentials.push([key, value])
205
+ }
206
+ }
207
+ return Object.fromEntries(credentials)
208
+ }
209
+
210
+ // sourced from npm's cli/lib/utils/config/definitions.js for reading certs from the cafile option
211
+ const fs = require('fs')
212
+ const maybeReadFile = file => {
213
+ try {
214
+ return fs.readFileSync(file, 'utf8')
215
+ } catch (er) {
216
+ if (er.code !== 'ENOENT') {
217
+ throw er
218
+ }
219
+ return null
220
+ }
221
+ }
222
+
223
+ function loadCACerts(npmConfig) {
224
+ if (npmConfig.ca) {
225
+ return npmConfig.ca
226
+ }
227
+
228
+ if (!npmConfig.cafile) {
229
+ return
230
+ }
231
+
232
+ const raw = maybeReadFile(npmConfig.cafile)
233
+ if (!raw) {
234
+ return
235
+ }
236
+
237
+ const delim = '-----END CERTIFICATE-----'
238
+ return raw.replace(/\r\n/g, '\n').split(delim)
239
+ .filter(section => section.trim())
240
+ .map(section => section.trimStart() + delim)
241
+ }
242
+
243
+ module.exports = { findVulnerableDependencies }