dependabot-npm_and_yarn 0.195.0 → 0.196.0
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 +4 -4
- data/helpers/lib/npm/index.js +3 -0
- data/helpers/lib/npm/vulnerability-auditor.js +206 -0
- data/helpers/package-lock.json +377 -229
- data/helpers/package.json +1 -0
- data/helpers/run.js +9 -14
- data/lib/dependabot/npm_and_yarn/update_checker/vulnerability_auditor.rb +136 -0
- data/lib/dependabot/npm_and_yarn/update_checker.rb +65 -4
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3461d61de14b92511754889ae9a9a5edafe20a6967037ec7946c5f6ec33e3615
|
4
|
+
data.tar.gz: f334af6509d4cb9e1afdcc9d06db1b5f200b597a5af4d2f617a0005922577677
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 880b9e022bca930c2b0f4b66d7e56c381739682893a5aa209f391d668da544dcab947d8ff5fd319fe39cdf1686864e47f191431d476cf8224ecea314689d3f5a
|
7
|
+
data.tar.gz: 322da33f31ae13dee139041931b7c5e6a7ac9df4947679e1d00d413920b2cd9aae7e041247400ec265a17103a77870f04177926ea51985237c59cb3412671617
|
data/helpers/lib/npm/index.js
CHANGED
@@ -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,206 @@
|
|
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
|
+
|
38
|
+
const arb = new Arborist({
|
39
|
+
path: directory,
|
40
|
+
auditRegistry: 'http://localhost:9999',
|
41
|
+
ca: caCerts,
|
42
|
+
force: true,
|
43
|
+
dryRun: true,
|
44
|
+
})
|
45
|
+
|
46
|
+
const scope = nock('http://localhost:9999')
|
47
|
+
.persist()
|
48
|
+
.post('/-/npm/v1/security/advisories/bulk')
|
49
|
+
.reply(200, convertAdvisoriesToRegistryBulkFormat(advisories))
|
50
|
+
|
51
|
+
if (!nock.isActive()) {
|
52
|
+
nock.activate()
|
53
|
+
}
|
54
|
+
|
55
|
+
try {
|
56
|
+
const name = advisories[0].dependency_name
|
57
|
+
const auditReport = await arb.audit()
|
58
|
+
const vuln = auditReport.get(name)
|
59
|
+
const version = [...vuln.nodes][0].version
|
60
|
+
const fixAvailable = vuln.fixAvailable
|
61
|
+
const response = {
|
62
|
+
dependency_name: name,
|
63
|
+
current_version: version,
|
64
|
+
fix_available: Boolean(fixAvailable),
|
65
|
+
fix_updates: [],
|
66
|
+
}
|
67
|
+
|
68
|
+
if (!Boolean(fixAvailable)) {
|
69
|
+
return response
|
70
|
+
}
|
71
|
+
|
72
|
+
const chains = buildDependencyChains(auditReport, name)
|
73
|
+
|
74
|
+
// In order to consider the vuln dependency in question fixable,
|
75
|
+
// all dependency chains originating from it must be fixable.
|
76
|
+
if (chains.some((chain) => !Boolean(chain.fixAvailable))) {
|
77
|
+
response.fix_available = false
|
78
|
+
return response
|
79
|
+
}
|
80
|
+
|
81
|
+
for (const chain of chains) {
|
82
|
+
const topMost = chain.items[0]
|
83
|
+
if (topMost.name === name) {
|
84
|
+
continue
|
85
|
+
}
|
86
|
+
response.fix_updates.push({
|
87
|
+
dependency_name: topMost.name,
|
88
|
+
current_version: topMost.version,
|
89
|
+
})
|
90
|
+
}
|
91
|
+
|
92
|
+
const fixTree = await arb.audit({
|
93
|
+
fix: true,
|
94
|
+
})
|
95
|
+
|
96
|
+
response.target_version = fixTree.children.get(name)?.version
|
97
|
+
|
98
|
+
for (const update of response.fix_updates) {
|
99
|
+
update.target_version =
|
100
|
+
fixTree.children.get(update.dependency_name)?.version
|
101
|
+
}
|
102
|
+
|
103
|
+
return response
|
104
|
+
} finally {
|
105
|
+
nock.cleanAll()
|
106
|
+
nock.restore()
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
function convertAdvisoriesToRegistryBulkFormat(advisories) {
|
111
|
+
// npm audit differentiates advisories by `id`. In order to prevent
|
112
|
+
// advisories from being clobbered, we maintain a counter so that each
|
113
|
+
// advisory gets a unique `id`.
|
114
|
+
let nextAdvisoryId = 1
|
115
|
+
|
116
|
+
return advisories.reduce((formattedAdvisories, advisory) => {
|
117
|
+
if (!formattedAdvisories[advisory.dependency_name]) {
|
118
|
+
formattedAdvisories[advisory.dependency_name] = []
|
119
|
+
}
|
120
|
+
let formattedVersions =
|
121
|
+
advisory.affected_versions.reduce((memo, version) => {
|
122
|
+
memo.push({
|
123
|
+
id: nextAdvisoryId++,
|
124
|
+
vulnerable_versions: version
|
125
|
+
})
|
126
|
+
return memo
|
127
|
+
}, [])
|
128
|
+
formattedAdvisories[advisory.dependency_name].push(...formattedVersions)
|
129
|
+
return formattedAdvisories
|
130
|
+
}, {})
|
131
|
+
}
|
132
|
+
|
133
|
+
/* Traverses all effects originating from the named dependency in the
|
134
|
+
* audit report and builds an array of all dependency chains,
|
135
|
+
* [
|
136
|
+
* {
|
137
|
+
* fixAvailable: true | false | object,
|
138
|
+
* items: [
|
139
|
+
* { name: 'foo', version: '1.0.0' },
|
140
|
+
* ...
|
141
|
+
* ]
|
142
|
+
* },
|
143
|
+
* ...
|
144
|
+
* ]
|
145
|
+
*
|
146
|
+
* The first item in the chain is always the top-most dependency affected by
|
147
|
+
* the vulnerable dependency in question. The `fixAvailable` field
|
148
|
+
* applies to the first item in the chain (if that item is fixable, then
|
149
|
+
* every item after it must be fixable, too).
|
150
|
+
*/
|
151
|
+
function buildDependencyChains(auditReport, name, chain = { items: [] }) {
|
152
|
+
const vuln = auditReport.get(name)
|
153
|
+
const version = [...vuln.nodes][0].version
|
154
|
+
const item = { name, version }
|
155
|
+
|
156
|
+
if (!vuln.effects.size) {
|
157
|
+
// If the current vuln has no effects, we've reached the end of this chain.
|
158
|
+
return [{ fixAvailable: vuln.fixAvailable, items: [item, ...chain.items] }]
|
159
|
+
}
|
160
|
+
|
161
|
+
return [...vuln.effects].reduce((chains, effect) => {
|
162
|
+
return chains.concat(
|
163
|
+
buildDependencyChains(
|
164
|
+
auditReport, effect.name, { items: [item, ...chain.items] }))
|
165
|
+
}, [])
|
166
|
+
}
|
167
|
+
|
168
|
+
async function loadNpmConfig() {
|
169
|
+
const configOutput = await exec('npm config ls --json')
|
170
|
+
return JSON.parse(configOutput.stdout)
|
171
|
+
}
|
172
|
+
|
173
|
+
// sourced from npm's cli/lib/utils/config/definitions.js for reading certs from the cafile option
|
174
|
+
const fs = require('fs')
|
175
|
+
const maybeReadFile = file => {
|
176
|
+
try {
|
177
|
+
return fs.readFileSync(file, 'utf8')
|
178
|
+
} catch (er) {
|
179
|
+
if (er.code !== 'ENOENT') {
|
180
|
+
throw er
|
181
|
+
}
|
182
|
+
return null
|
183
|
+
}
|
184
|
+
}
|
185
|
+
|
186
|
+
function loadCACerts(npmConfig) {
|
187
|
+
if (npmConfig.ca) {
|
188
|
+
return npmConfig.ca
|
189
|
+
}
|
190
|
+
|
191
|
+
if (!npmConfig.cafile) {
|
192
|
+
return
|
193
|
+
}
|
194
|
+
|
195
|
+
const raw = maybeReadFile(npmConfig.cafile)
|
196
|
+
if (!raw) {
|
197
|
+
return
|
198
|
+
}
|
199
|
+
|
200
|
+
const delim = '-----END CERTIFICATE-----'
|
201
|
+
return raw.replace(/\r\n/g, '\n').split(delim)
|
202
|
+
.filter(section => section.trim())
|
203
|
+
.map(section => section.trimStart() + delim)
|
204
|
+
}
|
205
|
+
|
206
|
+
module.exports = { findVulnerableDependencies }
|