dependabot-npm_and_yarn 0.195.0 → 0.196.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 }